Python 属性管理(整理转自《Python学习手册》)

插入在属性访问时运行的代码

1.__getattr__ 和 __setattr__ 方法,把未定义的属性获取和所有的属性赋值指向通用
的处理器方法。

2.__getattribute__ 方法,把所有属性获取都指向Python 2.6的新式类和Python 3.0的
所有类中的一个泛型处理器方法。

3.property内置函数,把特定属性访问定位到get和set处理器函数,也叫做特性
(Property)。

4.描述符协议,把特定属性访问定位到具有任意get和set处理器方法的类的实例。

正如我们将要看到的,所有4种技术在某种程度上具有同样的目标,并且通常可能对于
给定的问题使用任何一种技术来编写代码。然而它们确实存在某些重要的不同。例如,
这里列出的最后两种技术适用于特定属性,而前两种则足够通用,可以用于那些必须把
任意属性指向包装的对象的、基于委托的类。我们将会看到,所有4种方法在复杂性和
优雅性上也都有所不同,在使用中,我们必须通过实际应用来自行判断。

特性

特性协议允许我们把一个特定属性的get和set操作指向我们所提供的函数或方法,使得我
们能够插入在属性访问的时候自动运行的代码,拦截属性删除,并且如果愿意的话,还
可为属性提供文档。

通过 property 内置函数来创建特性并将其分配给类属性,就像方法函数一样。同样,可
以通过子类和实例继承属性,就像任何其他类属性一样。它们的访问拦截功能通过self
实例参数提供,该参数确保了在主体实例上访问状态信息和类属性是可行的。

一个特性管理一个单个的、特定的属性;尽管它不能广泛地捕获所有的属性访问,它允
许我们控制访问和赋值操作,并且允许我们自由地把一个属性从简单的数据改变为一个
计算,而不会影响已有的代码。正如你将看到的,特性和描述符有很大的关系,它们基
本上是描述符的一种受限制的形式。

基础知识
可以通过把一个内置函数的结果赋给一个类属性来创建一个特性:
attribute = property(fget, fset, fdel, doc)

这个内置函数的参数都不是必需的,并且如果没有传递参数的话,所有都取默认值
None 。这样的操作是不受支持的,并且尝试使用默认值将会引发一个异常。当使用它们
的时候,我们向 fget 传递一个函数来拦截属性访问,给 fset 传递一个函数进行赋值,并
且给 fdel 传递一个函数进行属性删除; doc 参数接收该属性的一个文档字符串,如果想
要的话(否则,该特性会赋值 fget 的文档字符串,如果提供了 fget 的文档字符串的话,
其默认值为 None )。 fget 返回计算过的属性值,并且 fset 和 fdel 不返回什么(确实是
None )。

这个内置的函数调用返回一个特性对象,我们将它赋给了在类的作用域中要管理的属性
的名称,正是在类的作用域中每个实例都继承了类。

第一个例子
为了说明如何把这些转换成有用的代码,如下的类使用一个特性来记录对一个名为 name
的属性的访问,实际存储的数据名为 _name ,以便不会和特性搞混了:

class Person: # Use (object) in 2.6
    def __init__(self, name):
        self._name = name
    def getName(self):
        print('fetch...')
        return self._name
    def setName(self, value):
        print('change...')
        self._name = value
    def delName(self):
        print('remove...')
        del self._name
    name = property(getName, setName, delName, "name property docs")

bob = Person('Bob Smith') # bob has a managed attribute
print(bob.name) # Runs getName
bob.name = 'Robert Smith' # Runs setName
print(bob.name)
del bob.name # Runs delName
print('-'*20)
sue = Person('Sue Jones') # sue inherits property too
print(sue.name)
print(Person.name.__doc__)  # Or help(Person.name)

Output:

fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
fetch...
Sue Jones
name property docs

Python 2.6和Python 3.0中都可以使用特性,但是,它们要求在Python 2.6中派生一个新式
对象,才能使赋值正确地工作——为了在Python 2.6中运行代码,这里把对象添加为一个
超类(我们在Python 3.0中也可以使用超类,但是,这是暗含的,并且不是必需的)。
这个特定的特性所做的事情并不多——它只是拦截并跟踪了一个属性,这里将它作为展
示协议的一个例子。当这段代码运行的时候,两个实例继承了该特性,就好像它们是附
加到其类的另外两个属性一样。然而,捕获了它们的属性访问:

In [1]: class Person:
   ...:     def __init__(self, name):
   ...:         self._name = name
   ...:     @property
   ...:     def name(self): # name = property(name)
   ...:         "name property docs"
   ...:         print('fetch...')
   ...:         return self._name
   ...:     @name.setter
   ...:     def name(self, value): # name = name.setter(name)
   ...:         print('change...')
   ...:         self._name = value
   ...:     @name.deleter
   ...:     def name(self): # name = name.deleter(name)
   ...:         print('remove...')
   ...:         del self._name
   ...:

In [2]: bob = Person('Bob Smith') # bob has a managed attribute
   ...: print(bob.name)  # Runs name getter (name 1)
   ...: bob.name = 'Robert Smith' # Runs name setter (name 2)
   ...: print(bob.name)
   ...: del bob.name # Runs name deleter (name 3)
   ...: print('-'*20)
   ...: sue = Person('Sue Jones') # sue inherits property too
   ...: print(sue.name)
   ...: print(Person.name.__doc__)
   ...:
fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
--------------------
fetch...
Sue Jones
name property docs

minghu6:真容易令人误解迷惑, 此name非彼name,有点a(a=a)的感觉
和 property 手动赋值的结果相比,这个例子中,使用装饰器来编写特性只需要3行额外
的代码(这是无法忽视的差别)。就像替代工具的通常情况一样,在这两种技术之间的
选择很大程度上与个人爱好有关。

描述符

描述符提供了拦截属性访问的一种替代方法;它们与前面小节所讨论的特性有很大的关
系。实际上,特性是描述符的一种——从技术上讲, property 内置函数只是创建一个特
定类型的描述符的一种简化方式,而这种描述符在属性访问时运行方法函数。

从功能上讲,描述符协议允许我们把一个特定属性的get和set操作指向我们提供的一个单
管理属性

独类对象的方法:它们提供了一种方式来插入在访问属性的时候自动运行的代码,并且
它们允许我们拦截属性删除并且为属性提供文档(如果愿意的话)。

描述符作为独立的类创建,并且它们就像方法函数一样分配给类属性。和任何其他的类
属性一样,它们可以通过子类和实例继承。通过为描述符自身提供一个 self ,以及提供
客户类的实例,都可以提供访问拦截方法。因此,它们可以自己保留和使用状态信息,
以及主体实例的状态信息。例如,一个描述符可能调用客户类上可用的方法,以及它所
定义的特定于描述符的方法。

和特性一样,描述符也管理一个单个的、特定的属性。尽管它不能广泛地捕获所有的属
性访问,但它提供了对获取和赋值访问的控制,并且允许我们自由地把简单的数据修改
为计算值从而改变一个属性,而不会影响已有的代码。特性实际上只是创建一种特定描
述符的方便方法,并且,正如我们所见到的,它们可以直接作为描述符编写。

然而,特性的应用领域相对狭窄,描述符提供了一种更为通用的解决方案。例如,由于
它们编码为常规类,所以描述符拥有自己的状态,可能参与描述符继承层级,可以使用
复合来聚合对象,并且为编写内部方法和属性文档字符串提供一种自然的结构。
基础知识
正如前面所提到的,描述符作为单独的类编写,并且针对想要拦截的属性访问操作提供
特定命名的访问器方法——当以相应的方式访问分配给描述符类实例的属性时,描述符
类中的获取、设置和删除等方法自动运行:

class Descriptor:
    "docstring goes here"
    def __get__(self, instance, owner): ... # Return attr value
    def __set__(self, instance, value): ... # Return nothing (None)
    def __delete__(self, instance): ... # Return nothing (None)

带有任何这些方法的类都可以看作是描述符,并且当它们的一个实例分配给另一个类的
属性的时候,它们的这些方法是特殊的——当访问属性的时候,会自动调用它们。如果
这些方法中的任何一个空缺,通常意味着不支持相应类型的访问。然而,和特性不同,
省略一个 set 意味着允许这个名字在一个实例中重新定义,因此,隐藏了描述符——
要使得一个属性是只读的,我们必须定义set来捕获赋值并引发一个异常。
描述符方法参数
在进行任何真正的编程之前,先来回顾一些基础知识。前面小节介绍的所有3种描
述符方法,都传递了描述符类实例(self )以及描述符实例所附加的客户类的实例
instance )。

__get__访问方法还额外地接收一个 owner 参数,指定了描述符实例要附加到的类。其
instance 参数要么是访问的属性所属的实例(用于instance.attr ),要么当所访问的
属性直接属于类的时候是None(用于class.attr)。前者通常针对实例访问计算一个
值;如果描述符对象访问是受支持的,后者通常返回 self

例如,在下面的例子中,当获取 X.attr 的时候,Python自动运行 Descriptor 类的
__get__ 方法, Subject.attr 类属性分配给该方法(和特性一样,在Python 2.6中,要
在这里使用描述符,我们必须派生自对象;在Python 3.0中,这是隐式的,但无伤大
雅):

>>> class Descriptor(object):
... def __get__(self, instance, owner):
... print(self, instance, owner, sep='\n')
...
>>> class Subject:
... attr = Descriptor() # Descriptor instance is class attr
...
>>> X = Subject()
>>> X.attr
<__main__.Descriptor object at 0x0281E690>
<__main__.Subject object at 0x028289B0>
<class '__main__.Subject'>
>>> Subject.attr
<__main__.Descriptor object at 0x0281E690>
None
<class '__main__.Subject'>

注意在第一个属性获取中自动传递到 __get__方法中的参数,当获取X.attr的时候,就好
像发生了如下的转换(尽管这里的 Subject.attr 没有再次调用 get ):
X.attr -> Descriptor.__get__(Subject.attr, X, Subject)
当描述符的实例参数为 None 的时候,该描述符知道将直接访问它。
只读描述符
正如前面提到的,和特性不同,使用描述符直接忽略__set__ 方法不足以让属性成为只读
的,因为描述符名称可以赋给一个实例。在下面的例子中,对X.a的属性赋值在实例对
象 X 中存储了 a ,由此,隐藏了存储在类 C 中的描述符:

>>> class D:
... def __get__(*args): print('get')
...
>>> class C:
... a = D()
...
管理属性 | 947
>>> X = C()
>>> X.a # Runs inherited descriptor __get__
get
>>> C.a
get
>>> X.a = 99 # Stored on X, hiding C.a
>>> X.a
99
>>> list(X.__dict__.keys())
['a']
>>> Y = C()
>>> Y.a # Y still inherits descriptor
get
>>> C.a
get

这就是Python中所有实例属性赋值工作的方式,并且它允许在它们的实例中类选择性地
覆盖类级默认值。要让基于描述符的属性成为只读的,捕获描述符类中的赋值并引发一
个异常来阻止属性赋值——当要赋值的属性是一个描述符的时候,Python有效地绕过了
常规实例层级的赋值行为,并且把操作指向描述符对象:

>>> class D:
... def __get__(*args): print('get')
... def __set__(*args): raise AttributeError('cannot set')
...
>>> class C:
... a = D()
...
>>> X = C()
>>> X.a # Routed to C.a.__get__
get
>>> X.a = 99 # Routed to C.a.__set__
AttributeError: cannot set

注意: 还要注意不要把描述符__delete__方法和通用的 __del__方法搞混淆了。调用前者是试图
删除所有者类的一个实例上的管理属性名称;后者是一种通用的实例析构器方法,当任
何类的一个实例将要进行垃圾回收的时候调用。 __delete__与我们将要在本章后面遇到的
__delattr__ 泛型属性删除方法关系更近。参见本书第29章了解关于操作符重载的更多内

特性和描述符是如何相关的

正如前面提到的,特性和描述符有很强的相关性—— property 内置函数只是创建描述符
的一种方便方式。既然已经知道了二者是如何工作的,我们应该能够看到,可以使用如
下的一个描述符类来模拟 property 内置函数:

class Property:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel # Save unbound methods
        self.__doc__ = doc # or other callables
    def __get__(self, instance, instancetype=None):
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError("can't get attribute")
        return self.fget(instance)  # Pass instance to self

    # in property accessors
    def __set__(self, instance, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(instance, value)
    def __delete__(self, instance):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(instance)

class Person:
    def getName(self): ...
    def setName(self, value): ...
    name = Property(getName, setName) # Use like property()

这个 Property类捕获了带有描述符协议的属性访问,并且把请求定位到创建类的时候在
描述符状态中传入和保存的函数或方法。例如,属性获取从 Person 类指向 Property 类的
__get__方法,再回到 Person 类的 getName 。有了描述符,这“恰好可以工作”。
注意,尽管这个描述符类等同于只是处理基本的特性用法,使用 @decorator语法也只是
指定了设置和删除操作,但是我们的 Property 类也必须用 setterdeleter 方法扩展,
这可能会节省装饰的访问器函数并且返回特性对象( self 应该足够了)。既然 property
内置函数已经做了这些,这里,我们将省略这一扩展的正式编码。
还要注意,描述符用来实现Python的__slots__ ,使用存储在类级别的描述符来截取slot
名称,从而避免了实例属性字典。

__getattr____getattribute__

到目前为止,我们已经学习了特性和描述符——管理特定属性的工具。__getattr__
__getattribute__操作符重载方法提供了拦截类实例的属性获取的另一种方法。就像特
性和描述符一样,它们也允许我们插入当访问属性的时候自动运行的代码。然而,我们
将会看到,这两个方法有更广泛的应用。

属性获取拦截表现为两种形式,可用两个不同的方法来编写:
__getattr__针对未定义的属性运行——也就是说,属性没有存储在实例上,或者没
有从其类之一继承。

__getattribute__ 针对每个属性,因此,当使用它的时候,必须小心避免通过把属
性访问传递给超类而导致递归循环。

这两个方法是一组属性拦截方法的代表,这些方法还包括 __setattr____delattr__
由于这些方法具有相同的作用,我们在这里将它们通常作为一个单独话题。

与特性和描述符不同,这些方法是Python的操作符重载协议的一部分——是类的特殊命
名的方法,由子类继承,并且当在隐式的内置操作中使用实例的时候自动调用。和一个
类的所有方法一样,它们每一个在调用的时候都接收第一个 self 参数,访问任何请求的
实例状态信息或该类的其他方法。
__getattr____getattribute__方法也比特性和描述符更加通用——它们可以用来拦截
对任何(几乎所有的)实例属性的获取,而不仅仅只是分配给它们的那些特定名称。因
此,这两个方法很适合于通用的基于委托的编码模式——它们可以用来实现包装对象,
该对象管理对一个嵌套对象的所有属性访问。相反,我们必须为想要拦截的每个属性都
定义一个特性或描述符。

最后,这两种方法比我们前面考虑的替代方法的应用领域更专注集中一些:它们只是
拦截属性获取,而不拦截属性赋值。要捕获赋值对属性的更改,我们必须编写一个
__setattr__方法——这是一个操作符重载方法,只对每个属性获取运行,必须小心避免
由于通过实例命名空间字典指向属性赋值而导致的递归循环。

尽管很少用到,我们还是可以编写一个 __delattr__ 重载方法(必须以同样的方式避免循
环)来拦截属性删除。相反,特性和描述符通过设计捕获访问、设置和删除操作。
基础知识
__getattr____setattr__ 在本书第29章和第31章介绍,并且第31章简单地提到了
__getattribute__ 。简而言之,如果一个类定义了或继承了如下方法,那么当一个实例
用于后面的注释所提到的情况时,它们将自动运行:

def __getattr__(self, name): # On undefined attribute fetch [obj.name]
def __getattribute__(self, name): # On all attribute fetch [obj.name]
def __setattr__(self, name, value): # On all attribute assignment [obj.name=value]
def __delattr__(self, name): # On all attribute deletion [del obj.name]

管理属性
所有这些之中, self 通常是主体实例对象, name 是将要访问的属性的字符串名, value
是要赋给该属性的对象。两个get方法通常返回一个属性的值,另两个方法不返回什么
( None )。例如,要捕获每个属性获取,我们可以使用上面的前两个方法;要捕获属性
赋值,可以使用第三个方法:

class Catcher:
    def __getattr__(self, name):
        print('Get:', name)
    def __setattr__(self, name, value):
        print('Set:', name, value)

X = Catcher()
X.job # Prints "Get: job"
X.pay # Prints "Get: pay"
X.pay = 99 # Prints "Set: pay 99"

这样的代码结构可以用来实现我们在委托设计模式。由于所有的属性通常
都指向我们的拦截方法,所以我们可以验证它们并将其传递到嵌入的、管理的对象中。
例如,下面的类跟踪了对传递给包装类的另一个对象的每一次属性获
取:

class Wrapper:
    def __init__(self, object):
        self.wrapped = object # Save object
    def __getattr__(self, attrname):
        print('Trace:', attrname) # Trace fetch
        return getattr(self.wrapped, attrname) # Delegate fetch

特性和描述符没有这样的类似功能,做不到对每个可能的包装对象中每个可能的属性编
写访问器。
避免属性拦截方法中的循环
这些方法通常都容易使用,它们唯一复杂的部分就是潜在的循环(即递归)。由于
__getattr__仅针对未定义的属性调用,所以它可以在自己的代码中自由地获取其他属
性。然而,由于 __getattribute____setattr__ 针对所有的属性运行,因此,它们的代
码要注意在访问其他属性的时候避免再次调用自己并触发一次递归循环。

例如,在一个__getattribute__ 方法代码内部的另一次属性获取,将会再次触发
__getattribute__ ,并且代码将会循环直到内存耗尽:

def __getattribute__(self, name):
    x = self.other # LOOPS!

要解决这个问题,把获取指向一个更高的超类,而不是跳过这个层级的版本—— object
类总是一个超类,并且它在这里可以很好地起作用:

def __getattribute__(self, name):
    x = object.__getattribute__(self, 'other') # Force higher to avoid me

对于__setattr__,情况是类似的。在这个方法内赋值任何属性,都会再次触发
__setattr__并创建一个类似的循环:

def __setattr__(self, name, value):
    self.other = value # LOOPS!

要解决这个问题,把属性作为实例的 __dict__命名空间字典中的一个键赋值。这样就避
免了直接的属性赋值:

def __setattr__(self, name, value):
    self.__dict__['other'] = value  # Use atttr dict to avoid me

尽管这种方法比较少用到,但 __setattr__ 也可以把自己的属性赋值传递给一个更高的超
类而避免循环,就像__getattribute__ 一样:

def __setattr__(self, name, value):
    object.__setattr__(self, 'other', value) # Force higher to avoid me

相反,我们不能使用 dict 技巧在 getattribute 中避免循环:

def __getattribute__(self, name):
    x = self.__dict__['other'] # LOOPS!

获取 dict 属性本身会再次触发 getattribute ,导致一个递归循环。很奇怪(因为_setattr_并不是这样),但
确实如此
__delattr__ 方法实际中很少用到,但是,当用到的时候,它针对每次属性删除而调用
(就像针对每次属性赋值调用 __setattr__ 一样)。因此,我们必须小心,在删除属性的
时候要避免循环,通过使用同样的技术:命名空间字典或者超类方法调用。

__getattr____getattribute__比较
为了概括 __getattr____getattribute__ 之间的编码区别,下面的例子使用了这两者来
实现3个属性—— attr1 是一个类属性, attr2 是一个实例属性, attr3 是一个虚拟的管理
属性,当获取时计算它:

class GetAttr:
    attr1 = 1
    def __init__(self):
        self.attr2 = 2
    def __getattr__(self, attr): # On undefined attrs only
        print('get: ' + attr) # Not attr1: inherited from class
        return 3 # Not attr2: stored on instance

X = GetAttr()
print(X.attr1)
print(X.attr2)
print(X.attr3)
print('-'*40)

class GetAttribute(object): # (object) needed in 2.6 only
    attr1 = 1
    def __init__(self):
        self.attr2 = 2
    def __getattribute__(self, attr): # On all attr fetches
        print('get: ' + attr) # Use superclass to avoid looping here
        if attr == 'attr3':
            return 3
        else:
            return object.__getattribute__(self, attr)

X = GetAttribute()
print(X.attr1)
print(X.attr2)
print(X.attr3)

运行时, __getattr__版本拦截对 attr3 的访问,因为它是未定义的。另一方面,
__getattribute__版本拦截所有的属性获取,并且必须将那些没有管理的属性访问指向
超类获取器以避免循环:

1
2
get: attr3
3
----------------------------------------
get: attr1
1
get: attr2
2
get: attr3
3

尽管__getattribute__可以捕获比__getattr__更多的属性获取,但是实际上,它们只是
一个主题的不同变体——如果属性没有物理地存储,二者具有相同的效果。

管理技术比较

为了概括我们在本章介绍的4种属性管理方法之间的编码区别,让我们快速地来看看使
用每种技术的一个更全面的计算属性的示例。如下的版本使用特性来拦截并计算名为
square 和 cube 的属性。注意它们的基本值是如何存储到以下划线开头的名称中的,因
此,它们不会与特性本身的名称冲突:

#dynamically computed attributes with properties

管理属性(Property)

class Powers:
    def __init__(self, square, cube):
        self._square = square # _square is the base value
        self._cube = cube # square is the property name

    def getSquare(self):
        return self._square ** 2

    def setSquare(self, value):
        self._square = value

    square = property(getSquare, setSquare)

    def getCube(self):
        return self._cube ** 3

    cube = property(getCube)

X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25

要用描述符做到同样的事情,我们用完整的类定义了属性。注意,描述符把基础值存储
为实例状态,因此,它们必须再次使用下划线开头,以便不会与描述符的名称冲突(正
如我们将在本章最后的示例中见到的,我们可以通过把基础值存储为描述符状态,从而
避免必须重新命名):

* Same, but with descriptors*
class DescSquare:
    def __get__(self, instance, owner):
        return instance._square ** 2
    def __set__(self, instance, value):
        instance._square = value

class DescCube:
    def __get__(self, instance, owner):
        return instance._cube ** 3

class Powers: # Use (object) in 2.6
        square = DescSquare()
        cube = DescCube()
    def __init__(self, square, cube):
        self._square = square # "self.square = square" works too,

X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25

要使用 __getattr__访问拦截来实现同样的结果,我们再次用下划线开头的名称存储基础
值,这样对被管理的名称访问是未定义的,并且由此调用我们的方法。我们还需要编写
一个 __setattrr__ 来拦截赋值,并且注意避免其潜在的循环:

Same, but with generic __getattr__ undefined attribute interception
class Powers:
    def __init__(self, square, cube):
        self._square = square
        self._cube = cube

    def __getattr__(self, name):
        if name == 'square':
            return self._square ** 2
        elif name == 'cube':
            return self._cube ** 3
        else:
            raise TypeError('unknown attr:' + name)

    def __setattr__(self, name, value):
        if name == 'square':
            self.__dict__['_square'] = value
        else:
            self.__dict__[name] = value

X = Powers(3, 4)
print(X.square) # 3 ** 2 = 9
print(X.cube) # 4 ** 3 = 64
X.square = 5
print(X.square) # 5 ** 2 = 25

最后一个选项,使用__getattribute__来编写,类似于前一个版本。由于我们现在捕获
了每一个属性,因此必须把基础值获取指向超类以避免循环:

Same, but with generic _getattribute_ all attribute interception
class Powers:
    def __init__(self, square, cube):
        self._square = square
        self._cube = cube

    def __getattribute__(self, name):
        if name == 'square':
            return object.__getattribute__(self, '_square') ** 2
        elif name == 'cube':
            return object.__getattribute__(self, '_cube') ** 3
        else:
            return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        if name == 'square':
            self.__dict__['_square'] = value
        else:
            self.__dict__[name] = value

    X = Powers(3, 4)
    print(X.square) # 3 ** 2 = 9
    print(X.cube) # 4 ** 3 = 64
    X.square = 5

管理属性
print(X.square) # 5 ** 2 = 25
正如你所见到的,每种技术的编码形式都有所不同,但是,所有4种方法在运行的时候
都产生同样的结果:

9
64
25

要了解如何比较这些替代方案以及其他编码选项的更多内容,在本章后面“示例:属性
验证”节的属性验证示例中,我们会更多地尝试它们的实际应用。在此之前,我们需要
先学习和这些工具中的两种相关的一个缺点。
拦截内置操作属性

在介绍 __getattr____getattribute__ 的时候,我说它们分别拦截未定义的以及所有的
属性获取,这使得它们很适合用于基于委托的编码模式。尽管对于常规命名的属性来说
是这样,但它们的行为需要一些额外的澄清:对于隐式地使用内置操作获取的方法名属
性,这些方法可能根本不会运行。这意味着操作符重载方法调用不能委托给被包装的对
象,除非包装类自己重新定义这些方法。

例如,针对 __str____add____getitem__ 方法的属性获取分别通过打印、+表达式和
索引隐式地运行,而不会指向Python 3.0中的类属性拦截方法。特别是:
在Python 3.0中, __getattr____getattribute__都不会针对这样的属性而运行。
在Python 2.6中,如果属性在类中未定义的话, __getattr__会针对这样的属性运
行。
在Python 2.6中,__getattribute__只对于新式类可用,并且在Python 3.0中也可以

##########################################################################
##最后的事例
##########################################################################

示例:属性验证

为了结束本章的内容,让我们来看一个更实际的示例,以所有的4种属性管理方案来编
写代码。我们将要使用的这个示例定义了一个 CardHolder 对象,它带有4个属性,其中3
个属性是要管理的。管理的属性在获取或存储的时候要验证或转换值。对于同样的测试
代码,所有4个版本都产生同样的结果,但是,它们以不同的方式实现了它们的属性。
这个示例包含了很大一部分需要自学的内容。然而我们不会详细介绍其代码,因为它们
都使用了我们在本章中已经介绍过的概念。

使用特性来验证
我们的第一段代码使用了特性来管理3个属性。与通常一样,我们可以使用简单的方法
而不是管理属性,但是,如果我们在已有的代码中已经使用了属性,特性就能帮忙了。
特性根据属性访问自动运行代码,但是关注属性的一个特定集合,它们不会用来广泛地
拦截所有属性。

要理解这段代码,关键是要注意到,__init__构造函数方法内部的属性赋值也触发了特
性的 setter 方法。例如,当这个方法分配给 self.name 时,它自动调用 setName 方法,该
方法转换值并将其赋给一个叫做 __name 的实例属性,以便它不会与特性的名称冲突。
这一重命名(有时候叫做名称压缩)是必要的,因为特性使用公用的实例状态并且没有
自己的实例状态。存储在一个属性中的数据叫做 __name ,而叫做 name 的属性总是特性,
而非数据。

最后,这个类管理了叫做 name 、 age 和 acct 的属性;允许直接访问属性 addr ,并且提供
了一个名为 remain 的只读属性,该属性完全是虚拟的并且根据需要计算。为了进行比
较,这个基于特性的程序包含了39行代码:

class CardHolder:
    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger prop setters too
        self.age = age # __X mangled to have class name
        self.addr = addr # addr is not managed

    def getName(self):
        return self.__name

    def setName(self, value):
        value = value.lower().replace(' ', '_')
        self.__name = value

    name = property(getName, setName)
    def getAge(self):
        return self.__age

    def setAge(self, value):
        if value < 0 or value > 150:
            raise ValueError('invalid age')
        else:
            self.__age = value
            age = property(getAge, setAge)

    def getAcct(self):
        return self.__acct[:-3] + '***'

    def setAcct(self, value):
        value = value.replace('-', '')
        if len(value) != self.acctlen:
            raise TypeError('invald acct number')
        else:
            self.__acct = value

    acct = property(getAcct, setAcct)
    def remainGet(self): # Could be a method, not attr
        return self.retireage - self.age # Unless already using as attr

    remain = property(remainGet)

self测试代码
如下的代码测试我们的类;将这段代码添加到文件的底部,或者把类放到一个模块中并
先导入它。我们将对这个例子的所有4个版本使用这段同样的测试代码。当它运行的时
候,我们创建了管理的属性类的两个实例,并且获取和修改其各种属性。期待失效的操
作包装在 try 语句中:

bob = CardHolder('1234-5678', 'Bob Smith', 40, '123 main st')
print(bob.acct, bob.name, bob.age, bob.remain, bob.addr, sep=' / ')

bob.name = 'Bob Q. Smith'
bob.age = 50
bob.acct = '23-45-67-89'
print(bob.acct, bob.name, bob.age, bob.remain, bob.addr, sep=' / ')

sue = CardHolder('5678-12-34', 'Sue Jones', 35, '124 main st')
print(sue.acct, sue.name, sue.age, sue.remain, sue.addr, sep=' / ')

try:
    sue.age = 200
except:
print('Bad age for Sue')
try:
    sue.remain = 5
except:
    print("Can't set sue.remain")
try:
    sue.acct = '1234567'
except:
    print('Bad acct for Sue')

如下是我们的self测试代码的输出。再一次说明,这对这个示例的所有版本都是一样
的。分析这段代码,看看类的方法是如何调用的。账户显示出来,其中一些数字隐藏
了,名称转换为一种标准格式,并且使用一个类属性访问拦截的时候,截止到退休的剩
余时间就计算了出来:

12345*** / bob_smith / 40 / 19.5 / 123 main st
23456*** / bob_q._smith / 50 / 9.5 / 123 main st
56781*** / sue_jones / 35 / 24.5 / 124 main st
Bad age for Sue
Can't set sue.remain
Bad acct for Sue

使用描述符验证

现在,让我们使用描述符而不是使用特性来重新编写示例。正如我们已经看到的,描述
符在功能和角色上与特性类似。实际上,特性基本上是描述符的一种受限制的形式。和
特性一样,描述符设计来处理特定的属性访问,而不是通用的属性访问。和特性不同,
描述符有自己的状态,并且它们是一种更为通用的方案。

要理解这段代码,注意 __init__ 构造函数方法内部的属性赋值触发了描述符的 __set__
作符方法,这一点还是很重要。例如,当构造函数方法分配给 self.name 时,它自动调
Name.__set__() 方法,该方法转换值,并且将其赋给了叫做 name 的一个描述符属性。
和前面基于特性的变体不同,在这个例子中,实际的 name 值附加到了描述符对象,而不
是客户类实例。尽管我们可以把这个值存储在实例状态或描述符状态中,后者避免了需
要用下划线压缩名称以避免冲突。在 CardHolder 客户类中,名为 name 的属性总是一个描
述符对象,而不是数据。

最后,这个类实现了和前面的版本同样的属性:它管理名为 name 、 age 和 acct 的属性。
允许直接访问属性 addr ,并且提供一个名为 remain 的只读属性, remain 是完全虚拟的并
且根据需要计算。注意我们为何在其描述符中捕获对 remain 的赋值,并引发一个异常。
正如我们前面所介绍的,如果没有这么做,对一个实例这一属性的赋值,将会默默地创
建一个实例属性而隐藏了类属性描述符。为了进行比较,这个基于描述符的代码占了45
行:

class CardHolder:

    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger __set__ calls too
        self.age = age # __X not needed: in descriptor
        self.addr = addr # addr is not managed

class Name:
    def __get__(self, instance, owner): # Class names: CardHolder locals
        return self.name

    def __set__(self, instance, value):
        value = value.lower().replace(' ', '_')
        self.name = value

    name = Name()

class Age:
    def __get__(self, instance, owner):
        return self.age # Use descriptor data

    def __set__(self, instance, value):
        if value < 0 or value > 150:
            raise ValueError('invalid age')
        else:
            self.age = value

    age = Age()

class Acct:
    def __get__(self, instance, owner):
        return self.acct[:-3] + '***'

    def __set__(self, instance, value):
        value = value.replace('-', '')
        if len(value) != instance.acctlen: # Use instance class data
            raise TypeError('invald acct number')
        else:
            self.acct = value

    acct = Acct()

class Remain:
    def __get__(self, instance, owner):
        return instance.retireage - instance.age  # Triggers Age.__get__
    def __set__(self, instance, value):
        raise TypeError('cannot set remain')  # Else set allowed here

    remain = Remain()

使用__getattr__来验证

正如我们已经见到的,__getattr__ 方法拦截所有未定义的属性,因此,它可能比使用特
性或描述符更为通用。在我们的例子中,当获取一个管理的属性的时候,我们通过直接
测试属性名来获知。其他的属性物理地存储在实例中,因而无法达到__getattr__。尽管
这种方法比使用特性或描述符更为通用,但需要额外的工作来模拟专门关注属性的其他
工具。我们需要在运行时检查名称,并且必须编写一个__setattr__ 以拦截并验证属性赋
值。

对于这个例子的特性和描述符版本,注意 __init__ 构造函数方法中的属性赋值触发了类
__setattr__ 方法,这还是很关键的。例如,当这个方法分配给 self.name 时,它自动
地调用 setattr 方法,该方法转换值,并将其分配给一个名为 name 的实例属性。通过
在该实例上存储 name ,它确保了未来的访问不会触发__getattr__ 。相反, acct 存储为
_acct ,因此随后对 acct 的访问会调用 __getattr__

最后,像前两个例子中的情况一样,这个类管理名为 name 、 age 和 acct 的属性。允许直
接访问属性 addr ;并且提供一个名为 remain 的只读属性,它是完全虚拟的并且根据需要
计算。

为了进行比较,这个替代方法有32行代码——比基于特性的版本少了7行,比使用描述符
的版本少了13行。当然,清晰与否比代码大小更重要,但额外的代码有时候意味着额外
的开发和维护工作。可能这里更重要的是角色:像__getattr__这样的通用工具可能更适
合于通用委托,而特性和描述符更直接是为了管理特定属性而设计。

还要注意,当设置未管理的属性(例如, addr )的时候,这里的代码引发额外调用,然
而获取未管理的属性并不会引发额外调用,因为它们是定义了的。尽管这可能对大多数
程序都会导致不可忽视的额外开销,但只有当访问管理的属性的时候,特性和描述符才
会引发额外调用。

下面是代码的__getattr__ 版本:

class CardHolder:

    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger __setattr__ too
        self.age = age # _acct not mangled: name tested
        self.addr = addr # addr is not managed


    def __getattr__(self, name):
        if name == 'acct': # On undefined attr fetches
            return self._acct[:-3] + '***' # name, age, addr are defined
        elif name == 'remain':
            return self.retireage - self.age # Doesn't trigger __getattr__
        else:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        if name == 'name': # On all attr assignments
            value = value.lower().replace(' ', '_') # addr stored directly
        elif name == 'age': # acct mangled to _acct
            if value < 0 or value > 150:
                raise ValueError('invalid age')
        elif name == 'acct':
            name = '_acct'
            value = value.replace('-', '')
            if len(value) != self.acctlen:
                raise TypeError('invald acct number')
        elif name == 'remain':
            raise TypeError('cannot set remain')

        self.__dict__[name] = value # Avoid looping

使用__getattribute__验证

最后的变体使用__getattribute__在需要的时候拦截属性获取并管理它们。这里,每
次属性获取都会捕获,因此,我们测试属性名称来检测管理的属性,并将所有其他的
属性指向超类以实现常规的获取过程。这个版本和前面的版本一样,使用了同样的
__setattr__来捕获赋值。

这段代码的工作和__getattr__ 版本很相似,因此,我不想在这里重复整个介绍。然而,
注意,由于每个属性获取都指向了 __getattribute__,所以这里我们不需要压缩名称以
拦截它们( acct 存储为 acct )。另一方面,这段代码必须负责把未压缩的属性获取指向
一个超类以避免循环。

还要注意,对于设置和获取未管理的属性(例如, addr ),这个版本都会引发额外调
用。如果速度极为重要,这个替代方法可能会是所有方案中最慢的。为了进行比较,这
个版本也有32行代码,和前面的版本一样:

class CardHolder:
    acctlen = 8 # Class data
    retireage = 59.5

    def __init__(self, acct, name, age, addr):
        self.acct = acct # Instance data
        self.name = name # These trigger __setattr__ too
        self.age = age # acct not mangled: name tested
        self.addr = addr # addr is not managed

    def __getattribute__(self, name):
        superget = object.__getattribute__ # Don't loop: one level up
        if name == 'acct': # On all attr fetches
            return superget(self, 'acct')[:-3] + '***'
        elif name == 'remain':
            return superget(self, 'retireage') - superget(self, 'age')
        else:
            return superget(self, name) # name, age, addr: stored

    def __setattr__(self, name, value):
        if name == 'name': # On all attr assignments
            value = value.lower().replace(' ', '_') # addr stored directly
        elif name == 'age':
            if value < 0 or value > 150:
                raise ValueError('invalid age')
        elif name == 'acct':
            value = value.replace('-', '')
            if len(value) != self.acctlen:
                raise TypeError('invald acct number')
        elif name == 'remain':
            raise TypeError('cannot set remain')

    self.__dict__[name] = value # Avoid loops, orig names
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值