From 流畅的Python 人民邮电出版社 20章属性描述符

描述符是对多个属性运用相同存取逻辑的一种方式,比如特性就是一种描述符,将特性的读取和设置代理到了描述符相应的__get____set__方法上,因此可以对属性的读取和设置进行控制。描述符的实例能用作托管类的属性,通过子类共享代码,构件具有部分相同功能的专用描述符。

描述符是实现了特定协议的类,这个协议包括__get__, __set____delete__方法。使用描述符的Python功能还有方法以及classmethod和staticmethod装饰器。

基本用法

描述符的用法是,创建一个描述符实例,作为另外一个类的类属性,这样就会托管该类实例的同名属性,所以实例的属性和使用描述符的类属性是分离的。描述符属性通过将属性存储到相应实例的字典中来区分不同实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0')
def __get__(self, instance, owner):
return instance.__dict__[self.storage_name]
class LineItem:
weight = Quantity('weight')
def __init__(self, weight):
self.weight = weight

这里设置实例的属性的时候,会将设置代理到描述符的__set__,这样就将其设置到了实例的属性字典里,__set__中self是描述符实例,而instance是托管类的实例,应该将属性放在实例中,这样可以区分不同实例的同名属性。对于__get__方法,owner是instance对应的托管类,必须通过实例字典的方式拿出属性,这样才不会无限循环。

如果通过类来访问托管属性,instance就是None,然后就会产生AttributeError异常,所以可以进行判断,让__get__方法返回描述符实例

1
2
3
4
5
6
def __get__(self, instance, owner):
if instance is None:
# self是描述符实例
return self
else:
return instance.__dict__[self.storage_name]

利用继承扩展描述符

通常描述符会作为一个工具来控制很多类的属性管理,而且基于面向对象的设计,可以利用继承来重用代码创建新的描述符。比如下面的描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AutoStorage:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
setattr(instance, self.storage_name, value)

这个描述符实现了属性的存取,并且可以实现多个属性的托管而不用传递任何参数,因为内部已经做好名称处理了,所以也可以直接使用getattr以及setattr来对属性进行存取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Validated(abc.ABC, AutoStorage):
def __set__(self, instance, value):
value = self.validate(instance, value)
# super的使用方法
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value):
class Quantity(Validated):
def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value
class NonBlank(Validated):
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value

通过继承基础的AutoStorage,Validated会在set的时候进行检查,具体怎么检查,委托给validate的子类实现,这样就形成了代码复用,然后制造一堆描述符。

覆盖型描述符和非覆盖型描述符

实现__set__方法的描述符是覆盖型描述符,因为这样会覆盖对实例属性的赋值操作。比如特性如果没有提供设值的函数,那么就会引发AttributeError

如果只实现了__set__,写操作是由描述符处理,如果通过实例读取描述符会返回描述符对象本身。但是如果设置了实例的同名实例属性(通过__dict__实现)的话,再读取的话,就会直接从实例中返回__dict__的属性值。而写仍然会被描述符覆盖。

没有实现__set__的描述符是非覆盖型描述符,如果设置了同名实例属性,那么描述符会被覆盖。类中的方法就是非覆盖型描述符实现的,如果重新设置就会被覆盖。

对类属性赋值都能覆盖描述符。如果想控制类属性的操作,要把描述符依附在类的类上,即依附在元类上。

方法是描述符

用户在类中定义的函数都有__get__方法,所以依附在类上,就是描述符,而且是非覆盖型描述符,可以被实例属性重新覆盖。

比如obj.test就是绑定方法,即函数的__get__方法会返回绑定方法对象,相当于functools.partial将obj绑定给了函数第一个参数,就是self。

但是Test.test就是一个函数,即函数的__get__方法会返回自身的引用。

Test.test.__get__(obj)就是一个绑定方法对象,而Test.test.__get__(None, Test)就是一个函数对象,返回函数自身的引用。

绑定方法对象有一个__call__方法,来处理真正的调用过程,会调用__func__属性引用的原始函数,把函数的第一个参数设为绑定方法的__self__属性,这就是self的隐式绑定。所以函数会变成绑定方法。

描述符的使用

  • 只读描述符必须有__set__方法

这样才可以控制属性的赋值,要不然实例的同名属性会覆盖描述符。在__set__方法只需要跑出AttributeError异常就可以了。

  • 验证的描述符可以只实现__set__方法

这是一种覆盖型描述符,在set的时候检查属性值,设置在实例的__dict__,然后get的时候,就会从实例中拿值,比较快。因为设置了实例属性,就会将get给覆盖掉,而set不会被覆盖。

  • 只实现__get__可以实现缓存

在第一次的时候调用__get__进行计算,然后给实例__dict__设置同名的值,此后实例同名属性就会将描述符给覆盖了,所以就不用计算了。

  • 非特殊方法可以被实例属性覆盖

因为解释器会在类中寻找特殊方法,所以就不会受到描述符的影响,也不会因为实例中设置了相同的方法而影响。