From 流畅的Python 人民邮电出版社 19章动态属性和特性

特性

数据的属性和处理数据的方法统称为属性,其实方法只是可调用的属性。然后还有特性:property(fget=None, fset=None, fdel=None, doc=None)

经典的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self):
return self.__weight
def set_weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
# 构建property对象,然后赋值给公开的类属性 self.weight
weight = property(get_weight, set_weight)

新式使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')

特性都是类属性,但是特性管理的是实例属性的存取。特性是一种描述符,所以肯定是类属性,管理的是实例属性。

实例属性会遮盖类的数据属性,但是实例属性不会遮盖类特性,就是即使存在同样的实例属性名和特性属性名,再访问,也只会访问特性,而不会访问实例属性。如果删除这个类的特性,再去访问,就会是实例属性了。这种特性是覆盖型描述符:obj.attr会首先从obj.__class__开始,当类中没有名为attr的特性时候,Python才会在obj实例中寻找。

特性的doc是在@property装饰的函数上的,经典的特性是doc参数property(fget=None, fset=None, fdel=None, doc=None)

特性工厂函数

赋值语句的右边先计算

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
def quantity(storage_name):
# 获取属性的方法
def qty_getter(instance):
# 跳过特性,直接访问实例属性
return instance.__dict__[storage_name]
# 设置属性的方法
def qty_setter(instance, value):
if value > 0:
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0')
# 返回一个特性,这样类中的一个属性就变成了特性
return property(qty_getter, qty_setter)
class LineItem:
# weight类属性,现在是一个特性
weight = quantity('weight')
price = quantity('price')
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
# 特性会覆盖实例属性
return self.weight * self.price

特性会覆盖实例属性,所以可以直接存取__dict__属性来跳过特性的处理逻辑

处理属性的重要属性和函数

  • __class__ 对象所属类的引用,obj.__class__type(obj),以及特殊方法只在对象的类中寻找,而不在实例中寻找
  • __dict__ 存储对象或者类的可写属性,有__dict__的对象,可以随意设置新属性
  • __slots__限制实例能有哪些属性,定义了类中所有的实例属性,让解释器在元组中存储实例属性,而不用字典,节约内存!

处理属性的内置函数

  • dir(obj) 列出对象的大多数属性,可以在有__dict__和没有__dict__属性的对象上使用
  • getattr(object, name) 从object对象中获取name字符串对应的属性
  • hasattr(object, name) 如果object中存在指定的属性,则返回True
  • setattr(object, name, value) 把object对象指定属性的值设为value
  • vars([object]) 返回object对象的__dict__属性,如果没有参数,返回表示本地作用域的字典,如果没有__dict__属性,返回TypeError

处理属性的特殊方法

直接访问实例的__dict__属性,就不会触发这些特殊方法

  • __setattr__(self, name, value)

尝试设置指定的属性时候总会调用这个方法。这种方式被成为代替正常机制(也就是在实例字典种存储值)。name是属性名字,值就是要设置的值。

如果__setattr__()尝试给实例属性赋值,它应该调用基类方法,比如object.__setattr__(self, name, value),object已经实现了这个方法

  • __delattr__(self, name)

删除属性会调用这个,如果del obj.name对象是有意义的才会去实现这个方法

  • __dir__(self)

在对象上调用dir函数时候被调用,返回一个列表。dir()转换并且返回一个排序的列表

  • __getattribute__(self, name)

无条件调用来实现类中实例的属性获取。如果类中同时定义了__getattr__,后者将会在__getattribute__显示调用的时候或者引发了一个AttributeError时候被调用。

这个方法应该返回一个计算后的属性值或者引发一个AttributeError异常。为了防止在这个方法中无限循环,它的实现应该总是调用基类方法来获取它需要的任何属性,比如object.__getattribute__(self, name)

这个方法还是可能被忽略的,当查询一些特殊方法的时候。因为它们会被语法或者内置函数隐式调用。比如__len__方法(len函数)

  • __getattr__(self, name)

在获取指定的属性失败时候调用,在obj, Class和超类查找之后调用。

如果正常机制可以找到属性的话,就不会调用这个方法。这个方法和setattr是不对称的。一方面是效率原因,另一方面因为要不然的话__getattr__就没法去获取实例的其他属性了。

查询特殊方法

对于自定义类,特殊方法的隐式调用只有在定义在object的类中才会保证正确,而不是在对象的实例字典中。

1
2
3
4
5
6
7
8
9
>>> class C:
... pass
...
>>> c = C()
>>> c.__len__ = lambda: 5
>>> len(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'C' has no len()

这种行为后面的基本原理在于几个特殊方法,比如被所有对象都实现的__hash__()__repr__(),包括type对象。如果这些方法的隐式查询使用传统的查询过程,那么在type对象上调用这些方法的时候就会失败。

1
2
3
4
5
6
>>> 1 .__hash__() == hash(1)
True
>>> int.__hash__() == hash(int)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: descriptor '__hash__' of 'int' object needs an argument

使用这种不正确的调用未绑定方法的方式有时被称为metaclass confusion,并在查询特殊方法的时候忽略实例来避免这种情况。

1
2
3
4
>>> type(1).__hash__(1) == hash(1)
True
>>> type(int).__hash__(int) == hash(int)
True

除了绕过任何实例属性,隐式的特殊方法查询一般也会忽略__getattribute__()方法即使对象是元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> class Meta(type):
... def __getattribute__(*args):
... print("Metaclass getattribute invoked")
... return type.__getattribute__(*args)
...
>>> class C(object, metaclass=Meta):
... def __len__(self):
... return 10
... def __getattribute__(*args):
... print("Class getattribute invoked")
... return object.__getattribute__(*args)
...
>>> c = C()
>>> c.__len__() # Explicit lookup via instance
Class getattribute invoked
10
>>> type(c).__len__(c) # Explicit lookup via type
Metaclass getattribute invoked
10
>>> len(c) # Implicit lookup
10

绕过__getattribute__()机制在解释器中提供了意义重大的速度提升,当然在处理一些特殊方法上失去了灵活性(为了能够被解释器一致的调用,特殊方法必须在类中设置)