二十、属性描述符
实现了 __get__
、__set__
或 __delete__
方法的类是描述符。
描述符的用法是,创建一个实例,作为另一个类的类属性。
描述符类:实现描述符协议的类。
托管类:把描述符实例类属性的类。
描述符实例:描述符类的各个实例,声明为托管类的类属性。
In [22]: class Q: # 基于描述符协议实现。
...: def __init__(self,storage_name):
...: self.storage_name = storage_name # 托管实例中存储值的属性名称
# 尝试为托管属性赋值时,会调用 __set__ 方法。
# self 是描述符实例,即 L.weight 或 L.price
# instance 是托管实例 即 L的实例,value 是要设定的值
...: def __set__(self, instance, value):
...: if value > 0:
...: instance.__dict__[self.storage_name] = value # 防止无限递归
...: else:
...: raise ValueError('value must be > 0')
# 读值不需要特殊处理,所以不需要自定义 __get__ 方法。
In [23]: class L:
...: weight = Q('weight')
...: price = Q('price')
...: def __init__(self, des, weight, price):
...: self.des = des
...: self.weight = weight
...: self.price = price
...: def subtotal(self):
...: return self.weight * self.price
应该把具体值存储在每个托管对象中,而不是描述符对象中
为了理解错误的原因,可以想想 __set__
方法前两个参数(self 和 instance)的意思。这里,self 是描述符实例,它其实是托管类的类属性。同一时刻,内存中可能有几千个 LineItem 实例,不过只会有两个描述符实例:LineItem.weight 和 LineItem.price。因此,存储在描述符实例中的数据,其实会变成 LineItem 类的类属性,从而由全部 LineItem 实例共享。
问题:需要重复输入托管属性的名称,如果这样写就好了:但是,不太行。(其实我觉得重复写也挺好,,,)
class LineItem:
weight = Quantity()
price = Quantity()
自动获取存储属性的名称
为了生成 storage_name,我们以 _Quantity#
为前缀,然后在后面拼接一个整数: Quantity.__counter
类属性的当前值,每次把一个新的 Quantity 描述符实例依附到类上,都会递增这个值。在前缀中使用井号能避免 storage_name 与用户使用点号创建的属性冲突,因为 nutmeg._Quantity#0 是无效的 Python 句法。但是,内置的 getattr和 setattr 函数可以使用这种“无效的”标识符获取和设置属性,此外也可以直接处理实例属性 __dict__
。
In [25]: class QQ:
...: __counter = 0 # 统计 QQ 描述符数量
...: def __init__(self):
...: cls = self.__class__ # cls是 QQ 类的引用
...: prefix = cls.__name__
...: index = cls.__counter
...: self.storage_name = '_{}#{}'.format(prefix,index) # 每个描述符名称都是唯一的
...: cls.__counter += 1
...: def __get__(self,instance,owner): # 托管属性名称与实际存储的属性名称不同
...: return getattr(instance,self.storage_name)
...: def __set__(self,instance,value):
...: if value>0:
...: setattr(instance,self.storage_name, value)
...: else:
...: raise ValueError('value must be > 0')
In [26]: class L:
...: weight = QQ() # 不需要再重复写 weight 了
...: price = QQ()
...: def __init__(self,des , weight, price):
...: self.des = des
...: self.weight = weight
...: self.price = price
...: def subtotal(self):
...: return self.weight*self.price
这里可以使用内置的高阶函数 getattr
和 setattr
存取值,无需使用 instance.__dict__
,因为托管属性和储存属性的名称不同,所以把储存属性传给 getattr
函数不会触发描述符,不会像示例 20-1 那样出现无限递归。
如果想使用 Python 矫正名称的约定方式(例如 _L__quantity0
),要知道托管类(即 L)的名称,可是,解释器要先运行类的定义体才能构建类,因此创建描述符实例时得不到那个信息。不过,对这个示例来说,为了防止不小心被子类覆盖,不用包含托管类的名称,因为每次实例化新的描述符,描述符类的 __counter 属性都会递增,从而确保每个托管类的每个储存属性的名称都是独一无二的。
__get__
方法的三个参数:self,instance,owner。owner 参数托管类 L 的引用,通过描述符从托管类中获取属性时用得到。
如果使用类 L.weight
获取托管属性,描述符的 __get__
方法 instance 参数是 None。会抛出异常。
为了给用户提供内省和其他元编程技术支持,通过类访问托管属性时,返回描述符实例。
class Quantity:
__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):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
将特性工厂函数实现不用属性名称
def quantity(): # 没有 storage_name 参数
try:
quantity.counter += 1 # 一切皆对象,定义为 函数对象的属性,用于共享并不断累加
except AttributeError: # 捕捉没有属性异常
quantity.counter = 0
# 生成的属性的闭包作用域的变量
storage_name = '_{}:{}'.format('quantity', quantity.counter)
def qty_getter(instance): ➎
return getattr(instance, storage_name)
def qty_setter(instance, value):
if value > 0:
setattr(instance, storage_name, value)
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
描述符类与工厂函数
1 工厂函数只能粘贴,而描述符类可以继承
2 工厂函数的闭包对比,描述符类更容易理解
一种新型描述符
问题:如果商品描述信息为空,导致无法下单。
解决:创建一个 NonBlank 描述符。校验描述是否为空。
一个模板方法用一些抽象的操作定义一个算法,而子类将重定义这些操作以提供具体的行为。
私有类属性
In [3]: class A:
...: __counter = 0 # 私有类属性
...: def __init__(self):
...: cls = self.__class__
...: prefix = cls.__name__
...: index = cls.__counter
...: self.storage_name = '_{}#{}'.format(prefix, index)
...:
In [4]: a = A()
In [5]: a.__counter
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-8bd0ca4838da> in <module>
----> 1 a.__counter
AttributeError: 'A' object has no attribute '__counter'
In [6]: A.__counter
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-95bc9c924f7c> in <module>
----> 1 A.__counter
AttributeError: type object 'A' has no attribute '__counter'
In [7]: A.__dict__
Out[7]:
mappingproxy({'__module__': '__main__',
'_A__counter': 0,
'__init__': <function __main__.A.__init__(self)>,
'__dict__': <attribute '__dict__' of 'A' objects>,
'__weakref__': <attribute '__weakref__' of 'A' objects>,
'__doc__': None})
实现 Q 和 N 描述符
In [8]: class A: # 提供描述符的基础功能
...: __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)
...:
In [9]: class V(abc.ABC, A): # 继承自基础描述符类
...: def __set__(self,instance,value): # 重写 __set__ 方法
...: value = self.validate(instance,value) # 增加验证设置值的有效性
...: super().__set__(instance,value)
...: @abc.abstractmethod
...: def validate(self,instance,value): # 抽象方法,接口
...: """return validated value or raise ValueError"""
...:
In [10]: class Q(V):
...: def validate(self,instance,value):
...: if value <= 0:
...: raise ValueError('value must be > 0')
...: return value
...:
In [12]: class N(V):
...: def validate(self,instance,value):
...: value = value.strip()
...: if not value:
...: raise ValueError('value cannot be empty or blank')
...: return value
使用 Q 和 N 描述符
In [23]: class L:
...: des = N()
...: w = Q()
...: p = Q()
...: def __init__(self,des,w,p):
...: self.des = des
...: self.w = w
...: self.p = p
...: def subtotal(self):
...: return self.w * self.p
描述符的典型用途:管理数据属性。这种描述符也叫覆盖型描述符。
覆盖型与非覆盖型描述符对比
如前所述,Python 存取属性的方式特别不对等。通过实例读取属性时,通常返回的是实例中定义的属性;但是,如果实例中没有指定的属性,那么会获取类属性。而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类。
In [30]: class O: # 有 __get__ 和 __set__ 方法的典型 覆盖型描述符
...: def __get__(self,instance,owner):
...: print('get',self, instance,owner)
...: def __set__(self,instance,value):
...: print('set',self,instance,value)
...:
In [31]: class O1: # 没有 __set__ 方法的非覆盖型描述符,非数据描述符,遮盖型描述符
...: def __get__(self,instance,owner):
...: print('get',self,instance,owner)
...:
In [32]: class O2: # 没有 __get__ 方法的覆盖型描述符
...: def __set__(self,instance,value):
...: print('set',self,instance,value)
...:
In [33]: class M: # 托管类
...: o = O()
...: o1 = O1()
...: o2 = O2()
...: def spam(self):
...: print(self)
覆盖型描述符:实现 __set__
方法的描述符。描述符是类属性,会覆盖对实例属性的赋值操作。
特性 是覆盖型描述符,如果没有设值函数,property 的 __set__
方法会抛出 AttributeError 异常,指明属性为只读。
覆盖型描述符
In [43]: m = M()
In [44]: m.o
get <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> <class '__main__.M'>
In [45]: M.o # 类触发,instance 为 None
get <__main__.O object at 0x000001E40ACFE2E8> None <class '__main__.M'>
In [46]: m.o = 1 # 赋值
set <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> 1
In [47]: m.o # 读取仍会触发描述符 __get__ 方法
get <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> <class '__main__.M'>
In [49]: vars(m)
Out[49]: {}
In [50]: m.__dict__['aaa'] = 1 # 跳过描述符
In [51]: m.aaa
Out[51]: 1
In [52]: vars(m)
Out[52]: {'aaa': 1}
In [55]: m.__dict__['o'] = 3 # 尝试赋值 o
In [56]: vars(m)
Out[56]: {'aaa': 1, 'o': 3}
In [57]: m.o # 描述符优先级高于对象属性
get <__main__.O object at 0x000001E40ACFE2E8> <__main__.M object at 0x000001E40ACFE2B0> <class '__main__.M'>
没有 __get__
方法的覆盖型描述符
只实现了 __set__
方法,也是覆盖型描述符,因此,只有写操作由描述符处理。通过实例读取描述符会返回描述符对象本身,因为没有处理读操作的 __get__
方法。如果直接通过实例的 __dict__
属性创建同名实例属性,以后,设置,仍会经过 __set__
方法,读取,直接从实例中返回新赋予的值,而不会返回描述符对象。
In [1]: class O:
...: def __get__(self,instance,owner):
...: print('get',self,instance,owner)
...: def __set__(self,instance,value):
...: print('set',self,instance,value)
...:
In [2]: class O_get:
...: def __get__(self,instance,owner):
...: print('get',self,instance,owner)
...:
In [3]: class O_set:
...: def __set__(self,instance,value):
...: print('set',self,instance,value)
...:
In [4]: class T:
...: o = O()
...: o_get = O_get()
...: o_set = O_set()
...: def p(self):
...: print(self)
...:
In [5]: t = T()
In [6]: t.o_set # 这个覆盖型描述符没有 __get__ 方法,因此从类中获取描述符实例
Out[6]: <__main__.O_set at 0x16dff9d0828>
In [7]: T.o_set # 直接读取描述符实例
Out[7]: <__main__.O_set at 0x16dff9d0828>
In [8]: t.o_set = 111 # 触发 __set__ 方法
set <__main__.O_set object at 0x0000016DFF9D0828> <__main__.T object at 0x0000016DFFA093C8> 111
In [9]: t.o_set
Out[9]: <__main__.O_set at 0x16dff9d0828>
In [10]: t.__dict__['o_set'] = 111 # 跳过 __set__ 方法
In [11]: t.o_set # 实例属性覆盖了描述符,读操作
Out[11]: 111
In [12]: t.o_set = 2 # 仍然触发 __set__
set <__main__.O_set object at 0x0000016DFF9D0828> <__main__.T object at 0x0000016DFFA093C8> 2
In [13]: t.o_set
Out[13]: 111
非覆盖型描述符
没有实现 __set__
方法的描述符是 非覆盖型描述符。如果设置了同名的实例属性,描述符会被覆盖,致使描述符无法处理那个实例的那个属性。
In [14]: t.o_get # 触发 __get__ 方法
get <__main__.O_get object at 0x0000016DFF9D0EF0> <__main__.T object at 0x0000016DFFA093C8> <class '__main__.T'>
In [15]: t.o_get = 111 # 没有 __set__ 方法,成功赋值对象属性
In [16]: t.o_get # 成功 覆盖了 __get__ 方法
Out[16]: 111
In [17]: T.o_get
get <__main__.O_get object at 0x0000016DFF9D0EF0> None <class '__main__.T'>
In [18]: del t.o_get # 删除实例属性
In [19]: t.o_get
get <__main__.O_get object at 0x0000016DFF9D0EF0> <__main__.T object at 0x0000016DFFA093C8> <class '__main__.T'>
覆盖型描述符也叫数据描述符或强制描述符。非覆盖型描述符也叫非数据描述符或遮盖型描述符。
在类中覆盖描述符
不管描述符是不是覆盖型,为类属性赋值都能覆盖描述符。
In [21]: T.o = 2
In [22]: t.o
Out[22]: 2
In [23]: del T.o
In [24]: t.o
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-24-a0deb82dff1e> in <module>
----> 1 t.o
AttributeError: 'T' object has no attribute 'o'
示例揭示了读写属性的另一种不对等:读类属性的操作可以由依附在托管类上定义
有 __get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有
__set__ 方法的描述符处理。
若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。
默认情况下,对用户定义的类来说,其元类是 type,而我们不能为 type 添加属
性。
方法是描述符
在类中定义的函数属于绑定方法(bound method),因为用户定义的函数都有 __get__
方法,所以依附到类上时,就相当于描述符。
In [25]: t.p
Out[25]: <bound method T.p of <__main__.T object at 0x0000016DFFA093C8>>
In [26]: T.p
Out[26]: <function __main__.T.p(self)>
In [27]: t.p = 3 # 函数没有实现 __set__ 方法,因此是非覆盖型描述符
In [28]: t.p
Out[28]: 3
t.p
返回的是绑定方法对象:一种可调用的对象,包装了函数,并把托管实例绑定给函数的第一个参数(self),这与 functools.partial
函数的行为一致。
T.p
返回函数的 __get__
方法返回自身的引用。
In [29]: import collections
In [31]: class Text(collections.UserString):
...: def __repr__(self):
...: return 'Text({!r})'.format(self.data)
...: def reverse(self):
...: return self[::-1]
...:
In [32]: w = Text('abc')
In [33]: w
Out[33]: Text('abc')
In [34]: w.reverse()
Out[34]: Text('cba')
In [35]: type(Text.reverse) # 类调用:函数
Out[35]: function
In [36]: type(w.reverse) # 对象调用:方法
Out[36]: method
In [37]: Text.reverse(w)
Out[37]: Text('cba')
In [38]: list(map(Text.reverse,['fdg',(1,2,3),Text('kjl')])) # 函数可以处理实例之外的对象
Out[38]: ['gdf', (3, 2, 1), Text('ljk')]
In [39]: Text.reverse.__get__(None,Text) # 得到函数本身
Out[39]: <function __main__.Text.reverse(self)>
In [40]: Text.reverse.__get__(w) # 得到绑定方法
Out[40]: <bound method Text.reverse of Text('abc')>
In [41]: w.reverse # 实际调用了 w.reverse.__get__(w)
Out[41]: <bound method Text.reverse of Text('abc')>
In [42]: w.reverse.__self__ # 绑定方法对象调用实例对象引用
Out[42]: Text('abc')
In [44]: w.reverse.__func__ # 原始函数引用
Out[44]: <function __main__.Text.reverse(self)>
In [45]: Text.reverse
Out[45]: <function __main__.Text.reverse(self)>
In [46]: w.reverse.__set__ # 没有 __set__ 的描述符
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-46-571c9c95520e> in <module>
----> 1 w.reverse.__set__
AttributeError: 'function' object has no attribute '__set__'
In [47]: w.reverse.__get__ # 非覆盖型描述符
Out[47]: <method-wrapper '__get__' of method object at 0x0000016DFF92EEC8>
绑定方法对象有 __call__
方法,用于被调用是触发。这个方法会调用 __func__
属性引用的原始函数,并把第一个参数设置为__self__
属性。这就是形参 self 的隐式绑定方式。
函数会变成绑定方法,这是 Python 语言底层使用描述符的最好例证。
描述符用法建议
使用特性以保持简单
只读描述符必须有 __set__
方法:否则,会被同名属性覆盖。
用于验证的描述符可以只定义 __set__
方法
仅有 __get__
方法的描述符可以实现高速缓存:非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。之后,都不会触发 __get__
方法了。
非特殊方法可以被实例属性覆盖:由于函数和方法只实现了 __get__
方法,会被覆盖。然而,特殊方法不受影响。解释器只会在类中寻找特殊方法,例如:repr(x)
执行的是 x.__class__.__repr__(x)
,因此对象中的同名属性不会覆盖特殊方法。
特殊方法,类方法,静态方法,特性 不能被实例属性覆盖。
描述符文档字符串和覆盖删除操作
描述符类的文档字符串用于注解托管类中的各个描述符实例。帮助界面:help(L.weight)
描述符:__get__
__set__
__delete__