7-2-管理属性

1. 为什么管理属性

通常,属性只是对象的名称

1
2
person.name           # 获取属性值
person.name = value # 改变属性值

大多数情况下,属性位于对象自身之中,或者继承自对象所派生自的一个类。

假设编写了一个程序来直接使用一个 name 属性,但是随后需要修改,编写一个方法来管理对属性值的访问,这是很直接的。

然而,这需要在整个程序中用到了名称的所有地方都进行修改。此外,这种方法需要程序知道如何导出值:是作为简单的名称或是作为调用的方法。

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

一个更好的解决方案是,如果需要的话,在属性访问时自动运行代码。

  • __getattr____setattr__ 方法 ,把未定义的属性获取和所有的属性赋值指向通用的处理器方法。
  • __getattribute__ 方法,把所有属性获取都指向 Python 的所有类中的一个泛型处理器方法。
  • property 内置函数,把特定属性访问定位到 get 和 set 处理器函数,也叫做特性。
  • 描述符协议,把特定属性访问定位到具有任意 get 和 set 处理器方法的类的实例。

3. 特性

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

通过 property 内置函数来创建特性并将其分配给类属性,就像方法函数一样。

可以通过子类和实例继承属性。

访问拦截功能通过 self 实例参数提供,该参数确保了在主体实例上访问状态信息和类属性是可行的。

一个特性管理一个单个的、特定的属性。它允许我们控制访问的赋值操作,并且允许我们自由地把一个属性从简单的数据改变为一个计算,而不会影响已有的代码。

3.1 基础知识

可以通过把一个内置函数的结果赋给一个类属性来创建一个特性:

1
attribute = property(fget, fset, fdel, doc)

这个内置函数的参数都不是必需的,并且如果没有传递参数的话,所有都取默认值 None。对于前三个,None 意味着对应的操作是不支持的,并且尝试使用默认值将会引发一个异常。

当使用它们的时候,我们向 fget 传递一个函数来拦截属性访问,给 fset 传递一个函数进行赋值,并且给 fdel 传递一个函数进行属性删除。

从技术上讲,这三个参数都接受任何可调用的参数,包括类的方法,第一个参数接收被限定的实例。当稍后被调用时,fget 函数返回计算出的属性值,fset 和 fdel 不返回任何东西(实际上是一个 None),并且这三种方法都可能引发异常来拒绝访问请求。

如果想要的话,doc 参数接收该属性的一个文档字符串,否则,该特性会赋值 fget 函数的文档字符串,通常默认值为 None。

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

3.2 第一个例子

使用一个特性来记录对一个名为 name 的属性的访问,实际存储的数据名为 _name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person:
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 有一个管理属性
print(bob.name) # 运行 getName
bob.name = 'Robert Smith' # 运行 setName
print(bob.name)
del bob.name # 运行 delName

print('-'*20)
sue = Person('Sue Jones') # sue 也继承了属性
print(sue.name)
print(Person.name.__doc__) # 或者 help(Person.name)
change...
fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
--------------------
change...
fetch...
Sue Jones
name property docs

实例和较低的子类都继承特性。

3.3 计算的属性

例如,当获取属性的时候,动态地计算属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PropSquare:
def __init__(self, start):
self.value = start
def getX(self):
return self.value ** 2
def setX(self, value):
self.value = value
X = property(getX, setX)

P = PropSquare(3) # 两个有特性的类实例
Q = PropSquare(32) # 每个都有不同的状态信息

print(P.X) # 3 ** 2
P.X = 4
print(P.X) # 4 ** 2
print(Q.X) # 32 ** 2
9
16
1024

定义了一个 X 属性,将其当做静态数据一样访问,实际运行的代码在获取该属性的时候计算了它的值。

3.4 使用装饰器编写特性

内置函数 property 可以充当一个装饰器,来定义一个函数,当获取一个属性的时候自动运行该函数:

1
2
3
class Person:
@property
def name(self): ... # 重绑定:name = property(name)

运行的时候,装饰的方法自动传递给 property 内置函数的第一个参数。

Setter 和 deleter 装饰器
property 对象也有 getter、setter 和 deleter 方法,这些方法指定相应的特性访问器方法赋值并且返回特性自身的一个副本。也可以使用这些方法,通过装饰常规方法来指定特性的组成部分,getter 部分通常由创建特性自身的行为自动填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

bob = Person('Bob Smith')
print(bob.name) # 运行 name getter
bob.name = 'Robert Smith' # 运行 name setter
del bob.name # 运行 name deleter
fetch...
Bob Smith
change...
remove...

这段代码等同于第一个示例,装饰只是编写特性的一种替代方法。

4. 描述符

描述符提供了拦截属性访问的一种替代方法。

特性是描述符的一种,从技术上讲,property 内置函数只是创建一个特定类型的描述符的一种简化方式,而这种描述符在属性访问时运行方法函数。

从功能上讲,描述符协议允许我们把一个特定属性的 get 和 set 操作指向我们提供的一个单独类对象的方法:它们提供了一种方式来插入在访问属性的时候自动运行的代码,并且它们允许我们拦截属性删除并且为属性提供文档。

描述符作为独立的类创建。可以通过子类和实例继承。描述符提供了对获取和赋值访问的控制,并且允许我们自由地把简单的数据修改为计算值从而改变一个属性,而不会影响已有的代码。

4.1 基础知识

1
2
3
4
5
class Descriptor:
"docstring goes here"
def __get__(self, instance, owner):... # 返回属性值
def __set__(self, instance, value):... # 返回 None
def __delete__(self, instance):... # 返回 None

带有任何这些方法的类都可以看作是描述符,并且当它们的一个实例分配给另一个类的属性的时候,它们的这些方法是特殊的——当访问属性的时候,会自动调用它们。

省略一个 __set__ 意味着允许这个名字在一个实例中重新定义。因此,隐藏了描述符——要使得一个属性是只读的,必须定义 __set__ 来捕获赋值并引发一个异常。

带有 __set__ 的描述符在形式上称为数据描述符,它优先于由常规继承规则定位的其他名称。例如,名称 __class__ 的继承描述符覆盖实例名称空间字典中的相同名称。这也可以确保你在自己的类中编写的数据描述符优先于其他类。

描述符方法参数
__get__ 访问方法额外地接收一个 owner 参数,指定了描述符实例要附加到的类。instance 参数要么是访问的属性所属的实例(用于 instance.attr),要么当所访问的属性直接属于类的时候是None(用于 class.attr)。

1
2
3
4
5
6
7
8
9
class Descriptor:
def __get__(self, instance, owner):
print(self, instance, owner, sep='\n')

class Subject:
attr = Descriptor() # Descriptor 实例是类属性

X = Subject()
X.attr
<__main__.Descriptor object at 0x00000263457C3C50>
<__main__.Subject object at 0x00000263457C3C88>
<class '__main__.Subject'>
1
Subject.attr
<__main__.Descriptor object at 0x00000263457C3C50>
None
<class '__main__.Subject'>

当获取 X.attr 的时候,Python 自动运行 Descriptor 类的 __get__ 方法,Subject.attr 类属性分配给该方法。好像发生了如下转换:

X.attr -> Descriptor.__get__(Subject.attr, X, Subject)

只读描述符
使用描述符直接忽略 __set__ 方法不足以让属性成为只读的,因为描述符名称可以赋给一个实例。

1
2
3
4
5
6
7
8
9
class D:
def __get__(*args):
print('get')

class C:
a = D()

X = C()
X.a # 运行继承的描述符 __get__
get
1
C.a
get
1
2
X.a = 99                    # 保存在 X 中,隐藏了 C.a
X.a
99
1
list(X.__dict__.keys())
['a']
1
2
Y = C()                
Y.a # Y 依旧继承了描述符
get
1
C.a
get

要让基于描述符的属性成为只读的,捕获描述符类中的赋值并引发一个异常来阻止属性赋值——当要赋值的属性是一个描述符的时候,Python 有效地绕过了常规实例层级的赋值行为,并且把操作指向描述符对象:

1
2
3
4
5
6
7
8
9
10
11
class D:
def __get__(*args):
print('get')
def __set__(*args):
raise AttributeError('cannot set')

class C:
a = D()

X = C()
X.a # 路由到 C.a.__get__
get
1
X.a = 99                   # 路由到 C.a.__set__
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-11-93d9f4bcef68> in <module>
----> 1 X.a = 99                   # 路由到 C.a.__set__

<ipython-input-10-de8bfdf7e74f> in __set__(*args)
      3         print('get')
      4     def __set__(*args):
----> 5         raise AttributeError('cannot set')
      6 
      7 class C:

AttributeError: cannot set

不要把描述符 __delete__ 方法和通用的 __del__ 方法搞混淆。调用前者是试图删除所有者类的一个实例上的管理属性名称;后者是一种通用的实例析构器方法,当任何类的一个实例将要进行垃圾回收的时候调用。

4.2 第一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Name:
'name descriptor docs'
def __get__(self, instance, owner):
print('fetch...')
return instance._name
def __set__(self, instance, value):
print('change...')
instance._name = value
def __delete__(self, instance):
print('remove...')
del instance._name

class Person:
def __init__(self, name):
self._name = name
name = Name() # 将描述符赋值为属性

bob = Person('Bob Smith') # bob 有一个管理属性
print(bob.name) # 运行 Name.__get__
bob.name = 'Robert Smith' # 运行 Name.__set__
print(bob.name)
del bob.name # 运行 Name.__delete__
print(Name.__doc__)
fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...
name descriptor docs

实际上,我们必须像这样把描述符赋给一个类属性——如果赋给一个 self 实例属性,它将无法工作。当描述符的 __get__ 方法运行的时候,它传递了 3 个对象来定义其上下文:

  • self 是 Name 类实例
  • instance 是 Person 类实例
  • owner 是 Person 类

描述符类实例是一个类属性,并且由客户类和任何子类的所有实例所继承。

当一个描述符在客户类之外无用的话,将描述符的定义嵌入客户类之中,这在语法上是完全合理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person:
def __init__(self, name):
self._name = name

class Name: # 使用嵌套类
'name descriptor docs'
def __get__(self, instance, owner):
print('fetch...')
return instance._name
def __set__(self, instance, value):
print('change...')
instance._name = value
def __delete__(self, instance):
print('remove...')
del instance._name
name = Name()

print(Person.Name.__doc__)
name descriptor docs

4.3 计算的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DescSquare:
def __init__(self, start): # 每个 desc 有自己的状态
self.value = start
def __get__(self, instance, owner): # 属性获取
return self.value ** 2
def __set__(self, instance, value): # 属性赋值
self.value = value # 删除或 docs

class Client1:
X = DescSquare(3) # 将描述符实例赋值为类属性

class Client2:
X = DescSquare(32)

c1 = Client1()
c2 = Client2()

print(c1.X) # 3 ** 2
c1.X = 4
print(c1.X) # 4 ** 2
print(c2.X) # 32 ** 2
9
16
1024

4.4 在描述符中使用状态信息

  • 描述符状态用来管理内部用于描述符工作的数据。
  • 实例状态记录了和客户类相关的信息,以及可能由客户类创建的信息。

描述符状态是每个描述符的数据,实例状态是每个客户实例的数据。

描述符方法也可以使用状态形式,但是描述符状态常常使得不必要使用特定的命名惯例,以避免存储在一个实例上的描述符数据的名称冲突。

例如,描述符把信息附加到自己的实例,因此,它不会与客户类的实例上的信息冲突。

对描述符存储或使用附加到客户类的实例的一个属性,而不是自己的属性,这也是可行的。

描述符和实例状态都有各自的用途。实际上,这是描述符优于特性的一个通用优点——因为它们都有自己的状态,所以可以很容易地在内部保存数据,而不用将数据添加到客户实例对象的命名空间中。

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

可以使用如下的一个描述符类来模拟 property 内置函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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):
print('getName...')
def setName(self, value):
print('setName...')
name = Property(getName, setName)

x = Person()
x.name
x.name = 'Bob'
del x.name
getName...
setName...

---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-19-39d314b0f201> in <module>
     34 x.name
     35 x.name = 'Bob'
---> 36 del x.name

<ipython-input-19-39d314b0f201> in __delete__(self, instance)
     21     def __delete__(self, instance):
     22         if self.fdel is None:
---> 23             raise AttributeError("can't delete attribute")
     24         self.fdel(instance)
     25 

AttributeError: can't delete attribute

属性获取从 Person 类指向 Property 类的 __get__ 方法,再回到 Person 类的 getName。

5. __getattr____getattribute__

__getattr____getattribute__ 操作符重载方法提供了拦截类实例的属性获取的另一种方法。

属性获取拦截表现为两种形式,可用两个不同的方法来编写:

  • __getattr__ 针对未定义的属性运行——也就是说,属性没有存储在实例上,或者没有从其类之一继承。
  • __getattribute__ 针对每个属性,因此,当使用它的时候,必须小心避免通过把属性访问传递给超类而导致递归循环。

这些方法是 Python 的操作符重载协议的一部分——是类的特殊命名的方法,由子类继承,并且当在隐式的内置操作中使用实例的时候自动调用。

它们可以用来拦截对任何(几乎所有的)实例属性的获取,而不仅仅只是分配给它们的那些特定名称。

它们只是拦截属性获取,而不拦截属性赋值。要捕获赋值对属性的更改,我们必须编写一个 __setattr__ 方法——这是一个操作符重载方法,只对每个属性获取运行,必须小心避免由于通过实例命名空间字典指向属性赋值而导致的递归循环。

5.1 基础知识

如果一个类定义了或继承了如下方法,那么当一个实例用于后面的注释所提到的情况时,它们将自动运行:

1
2
3
4
def __getattr__(self, name):             # 获取未定义属性 [obj.name]
def __getattribute__(self, name): # 获取所有属性 [obj.name]
def __setattr__(self, name, value): # 所有属性赋值 [obj.name=value]
def __delattr__(self, name): # 所有属性删除 [del obj.name]

两个 get 方法通常返回一个属性的值,另两个方法不返回什么(None)。

例如,要捕获每个属性获取,我们可以使用上面的前两个方法;要捕获属性赋值,可以使用第三个方法:

1
2
3
4
5
6
7
8
9
10
class Catcher:
def __getattr__(self, name):
print('Get:',name)
def __setattr__(self, name, value):
print('Set:', name, value)

X = Catcher()
X.job # 打印 'Get:job'
X.pay # 打印 'Get:pay'
X.pay = 99 # 打印 'Set:pay 99'
Get: job
Get: pay
Set: pay 99

在特定情况下,使用 __getattribute__ 有相同效果:

1
2
3
4
5
6
7
8
9
10
class Catcher:
def __getattribute__(self, name):
print('Get:',name) # 可能导致循环
def __setattr__(self, name, value):
print('Set:', name, value)

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

避免属性拦截方法中的循环

由于 __getattribute____setattr__ 针对所有的属性运行,因此,它们的代码要注意在访问其他属性的时候避免再次调用自己并触发一次递归循环。

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

1
2
def __getattribute__(self, name):
x = self.other

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

1
2
def __getattribute__(self, name):
x = object.__getattribute__(self, 'other')

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

1
2
def __setattr__(self, name, value):
self.__dict__['other'] = value

__setattr__ 也可以把自己的属性赋值传递给一个更高的超类而避免循环,就像 __getattribute__ 一样。

相反,我们不能使用 __dict__ 技巧在 __getattribute__ 中避免循环。获取 __dict__ 属性本身会再次触发 __getattribute__ ,导致一个递归循环。

5.2 第一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person:
def __init__(self, name):
self._name = name # 触发 __setattr__

def __getattr__(self, attr): # [obj.undefined]
if attr == 'name': # 拦截 name:不保存
print('fetch...')
return self._name # 不循环
else:
raise AttributeError(attr)

def __setattr__(self, attr, value): # [obj.any = value]
if attr == 'name':
print('change...')
attr = '_name'
self.__dict__[attr] = value # 避免循环

def __delattr__(self, attr):
if attr == 'name':
print('remove...')
attr = '_name'
del self.__dict__[attr]

bob = Person('Bob Smith')
print(bob.name) # 运行 __getattr__
bob.name = 'Robert Smith' # 运行 __setattr__
print(bob.name)
del bob.name # 运行 __delattr__
fetch...
Bob Smith
change...
fetch...
Robert Smith
remove...

__init__ 构造函数中的属性赋值也触发了 __setattr__ ,这个方法捕获了每次属性赋值,即便是类自身之中的那些。

与特性和描述符不同,这里没有为属性直接声明指定的文档,管理的属性存在于我们拦截方法的代码之中,而不是在不同的对象中。

**使用 __getattribute__

要实现与 __getattribute__ 相同的结果,用下面的代码替换示例中的 __getattr__,由于它会捕获所有的属性获取,因此必须通过把新的获取传递到超类来避免循环,并且通常不能假设未知的名称是错误:

1
2
3
4
5
6
7
# 将 __getattr__ 替换如下

def __getattribute__(self, attr):
if attr == 'name':
print('fetch...')
attr = '_name'
return object.__getattribute__(self, attr)

运行时会得到 _setattr__ 中的获取中的 __getattribute__ 调用。

在只有单个的属性要管理的情况下,特性和描述符可能会做得更好。

5.3 计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AttrSquare:
def __init__(self, start):
self.value = start # 触发 __setattr__

def __getattr__(self, attr): # 未定义属性获取
if attr == 'X':
return self.value ** 2 # 值未定义
else:
raise AttributeError(attr)

def __setattr__(self, attr, value):
if attr == 'X':
attr = 'value'
self.__dict__[attr] = value

A = AttrSquare(3)
B = AttrSquare(32)

print(A.X) # 3 ** 2
A.X = 4
print(A.X) # 4 ** 2
print(B.X) # 32 ** 2
9
16
1024

使用 __getattribute__

通过使用直接超类方法调用而不是 __dict__ 键来修改 __setattr__ 赋值方法从而避免循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AttrSquare:
def __init__(self, start):
self.value = start # 触发 __setattr__

def __getattribute__(self, attr): # 未定义属性获取
if attr == 'X':
return self.value ** 2 # 值未定义
else:
return object.__getattribute__(self, attr)

def __setattr__(self, attr, value):
if attr == 'X':
attr = 'value'
object.__setattr__(self, attr, value)

A = AttrSquare(3)
B = AttrSquare(32)

print(A.X) # 3 ** 2
A.X = 4
print(A.X) # 4 ** 2
print(B.X) # 32 ** 2
9
16
1024

隐式的指向在类的方法内部进行:

  • 构造函数中的 self.value=start 触发 __setattr__
  • __getattribute__self.value 再次触发 __getattribute__

每次我们获取属性 X 的时候,__getattribute__ 都运行了两次。这并没有在 __getattr__ 版本中发生,因为 value 属性没有定义。

5.4 __getattr____getattribute__ 比较

attr1 是一个类属性,attr2 是一个实例属性,attr3 是一个虚拟的管理属性,当获取时计算它:

1
2
3
4
5
6
7
8
9
10
11
12
13
class GetAttr:
attr1 = 1
def __init__(self):
self.attr2 = 2

def __getattr__(self, attr): # 仅对未定义属性
print('get: ' + attr) # 不对 attr1:从类继承
return 3 # 不对 attr2:保存在实例中

X = GetAttr()
print(X.attr1)
print(X.attr2)
print(X.attr3)
1
2
get: attr3
3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GetAttribute:
attr1 = 1
def __init__(self):
self.attr2 = 2

def __getattribute__(self, attr): # 对所有属性获取
print('get: ' + attr) # 使用超类避免循环
if attr == 'attr3':
return 3
else:
return object.__getattribute__(self, attr)

X = GetAttribute()
print(X.attr1)
print(X.attr2)
print(X.attr3)
get: attr1
1
get: attr2
2
get: attr3
3

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

5.5 管理技术比较

5.6 拦截内置操作属性

对于隐式地使用内置操作获取的方法名属性,这些方法可能根本不会运行。这意味着操作符重载方法调用不能委托给被包装的对象,除非包装类自己重新定义这些方法。

换句话说,在 Python 3 的类中,没有直接的方法来通用地拦截像打印和加法这样的内置操作。

包装类可以通过在自身中重新定义所有相关的操作符重载方法,从而委托调用以解决这一约束。

这个问题只适用于 __getattr____getattribute__。由于特性和描述符只针对特定属性定义,所以它们根本不能真正应用于基于代理的类——单个特性或描述符不能用于拦截任意属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class GetAttr:
eggs = 88 # 类中存储 eggs,实例中存储 spam
def __init__(self):
self.spam = 77
def __len__(self): # 使用 len,否则 __getattr__ 由 __len__ 调用
print('__len__: 42')
return 42
def __getattr__(self, attr): # 提供 __str__
print('getattr: ' + attr)
if attr == '__str__':
return lambda *args: '[Getattr str]'
else:
return lambda *args: None

class GetAttribute:
eggs = 88
def __init__(self):
self.spam = 77
def __len__(self):
print('__len__: 42')
return 42
def __getattribute__(self, attr):
print('getattribute: ' + attr)
if attr == '__str__':
return lambda *args: '[GetAttribute str]'
else:
return lambda *args: None

for Class in GetAttr, GetAttribute:
print('\n' + Class.__name__.ljust(50, '='))

X = Class()
X.eggs # 类属性
X.spam # 实例属性
X.other # 未定义属性
len(X) # 显式定义 __len__

try:
X[0] # __getitem__?
except:
print('fail []')

try:
X + 99 # __add__?
except:
print('fail +')

try:
X() # __call__? (通过内置隐式调用)
except:
print('fail ()')

X.__call__() # __call__? (显式调用,非继承)
print(X.__str__()) # __str__? (显式调用,从 type 继承)
print(X) # __str__? (通过内置隐式调用)
GetAttr===========================================
getattr: other
__len__: 42
fail []
fail +
fail ()
getattr: __call__
<__main__.GetAttr object at 0x00000000051933C8>
<__main__.GetAttr object at 0x00000000051933C8>

GetAttribute======================================
getattribute: eggs
getattribute: spam
getattribute: other
__len__: 42
fail []
fail +
fail ()
getattribute: __call__
getattribute: __str__
[GetAttribute str]
<__main__.GetAttribute object at 0x00000000051934A8>

当通过内置操作获取属性的时候,没有隐式运行的操作符重载方法会触发哪个属性拦截方法。

  • __str__ 访问有两次未能被 __getattr__ 捕获:一次是针对内置打印,一次是针对显式获取,因为从该类继承了一个默认方法。
  • __str__ 只有一次未能被 __getattribute__ 捕获,在内置打印操作中,显式获取绕过了继承的版本。
  • __call__ 在 Python 3 中用于内置调用表达式的两次都没有捕获,但是,当显式获取的时候,它两次都拦截到了;和 __str__ 不同,没有继承的 __call__ 默认版本能够超越 __getattr__
  • __len__ 被两个类都捕获了,直接原因是,它在类自身中是一个显式定义的方法——它的名称指明了,在 Python 3 中,如果我们删除了类的 __len__ 方法,它不会指向 __getattr____getattribute__
  • 所有其他的内置操作在 Python 3 中都没有被两种方案拦截。

6. 示例:属性验证

6.1 使用特性来验证

特性根据属性访问自动运行代码,但是关注属性的一个特定集合,它们不会用来广泛地拦截所有属性。

__init__ 构造函数方法内部的属性赋值也触发了特性的 setter 方法。

特性使用公用的实例状态并且没有自己的实例状态。存储在一个属性中的数据叫做 __name,而叫做 name 的属性总是特性,而非数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class CardHolder:
acctlen = 8 # 类数据
retireage = 59.5

def __init__(self, acct, name, age, addr):
self.acct = acct # 实例数据
self.name = name # 这里也会触发 setter
self.age = age
self.addr = addr

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('invalid acct number')
else:
self.__acct = value
acct = property(getAcct, setAcct)

def remainGet(self): # 方法,而不是属性
return self.retireage - self.age # 除非已经作为属性使用
remain = property(remainGet)

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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')
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

6.2 使用描述符验证

特性基本上是描述符的一种受限制的形式。和特性不同,描述符有自己的状态,并且它们是一种更为通用的方案。

实际的 name 值附加到了描述符对象,而不是客户类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class CardHolder:
acctlen = 8
retireage = 59.5

def __init__(self, acct, name, age, addr):
self.acct = acct # 实例数据
self.name = name # 这里也会触发 __set__ 调用
self.age = age # 描述符中不需要 __X
self.addr = addr

class Name:
def __get__(self, instance, owner): # 类名称:CardHolder 本地变量
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 # 使用描述符数据
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: # 使用实例类数据
raise TypeError('invalid acct number')
else:
self.acct = value
acct = Acct()

class Remain:
def __get__(self, instance, owner):
return instance.retireage - instance.age # 触发 Age.__get__
def __set__(self, instance, value):
raise TypeError('cannot set remain')
remain = Remain()

6.3 使用 __getattr__ 来验证

__getattr__ 这样的通用工具可能更适合于通用委托,而特性和描述符更直接是为了管理特定属性而设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CardHolder:
acctlen = 8
retireage = 59.5

def __init__(self, acct, name, age, addr):
self.acct = acct # 实例数据
self.name = name # 这里也会触发 __setattr__ 调用
self.age = age
self.addr = addr

def __getattr__(self, name):
if name == 'acct': # 对未定义属性获取
return self._acct[:-3] + '***' # name, age, addr 已定义
elif name == 'remain':
return self.retireage - self.age # 未触发 __getattr__
else:
raise AttributeError(name)

def __setattr__(self, name, value):
if name == 'name': # 所有属性赋值
value = value.lower().replace(' ', '_') # addr 直接存储
elif name == 'age':
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('invalid acct number')
elif name == 'remain':
raise TypeError('cannot set remain')
self.__dict__[name] = value # 避免循环

6.4 使用 __getattribute__ 验证

由于每个属性获取都指向了 __getattribute__,所以这里我们不需要压缩名称以拦截它们(acct 存储为 acct)。

对于设置和获取未管理的属性(例如, addr ),这个版本都会引发额外调用。如果速度极为重要,这个替代方法可能会是所有方案中最慢的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CardHolder:
acctlen = 8
retireage = 59.5

def __init__(self, acct, name, age, addr):
self.acct = acct # 实例数据
self.name = name # 这里也会触发 __setattr__ 调用
self.age = age
self.addr = addr

def __getattribute__(self, name):
superget = object.__getattribute__ # 避免循环:对所有属性获取使用上一级的 __getattribute__
if name == 'acct':
return superget(self, 'acct')[:-3] + '***'
elif name == 'remain':
return superget(self, 'retireage') - superget(self, 'age')
else:
return superget(self, name)

def __setattr__(self, name, value):
if name == 'name': # 所有属性赋值
value = value.lower().replace(' ', '_') # addr 直接存储
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('invalid acct number')
elif name == 'remain':
raise TypeError('cannot set remain')
self.__dict__[name] = value # 避免循环
1