从attribute到property, 现在又出现了descriptor. 刚读到Fluent Python里的这一章(Chapter 20)时, 内心阴影面积着实很大, 要理清它们的关系确实不那么容易.
什么是descriptor?
一种协议, 协议方法有仨: __get__, __set__, __delete__
. 如果一个class实现了其中一个或多个, 那这个class就是一个descriptor. 大部分情况下只需要关注前两个方法.
有什么用?
用作class attribute时可控制instance attribute的读写操作, 可用于控制读写, 属性值合法性验证.
与property是什么关系?
它的作用明显与property重合. 其实, 它们就是一家人: property实现了descriptor的所有方法. 说白了, property是为便于使用而实现buit-in descriptor.
在实例属性解析顺序中处于什么地位?
首位. 之前在Python对象的属性访问过程一文中描述过没有property/descriptor时的解析顺序. 如果将descriptor也考虑进去, descriptor 的优先级永远最高. (有点无语了, 这么个大Boss, 居然放到倒数第二章才讲)
overiding, non-overriding descriptor
实现__set__
的descriptor
仍然使用从attribute到property的场景:
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name
def __get__(self, instance, cls):
print('\t{}.__get__ is called'.format(self.__class__.__name__))
return instance.__dict__[self.storage_name]
def __set__(self, instance, value):
print('\t{}.__set__ is called'.format(self.__class__.__name__))
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError('value must be > 0')
class Item:
count = Quantity('count')
price = Quantity('price')
def __init__(self, category, count, price):
self.category = category
print('Setting attributes...')
self.count = count
self.price = price
print('Setting attributes OK.')
def subtotal(self):
return self.count * self.price
item = Item('Book', 10, 20)
print('\nGetting attributes.')
print(item.count, item.price)
print('Getting attributes OK.')
try:
item = Item('Book', -10, 20)
item = Item('Book', 10, -20)
except ValueError:
print('Invalid items')
输出:
Setting attributes...
Quantity.__set__ is called
Quantity.__set__ is called
Setting attributes OK.
Getting attributes.
Quantity.__get__ is called
Quantity.__get__ is called
10 20
Getting attributes OK.
Setting attributes...
Quantity.__set__ is called
Invalid items
可以看到, 因为count
与price
被设置为Quantity
, 它的读写操作都被Quantity
的__get__, __set__
代理了.
当上面的storage_name
与属性名相同时, 只实现__get__
实现与否都可正常获取属性值.
class D:
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
instance.__dict__[self.storage_name] = value
class Foo:
name = D('name')
def __init__(self, name):
self.name = name
foo = Foo('foo')
print(foo.name)
输出:
foo
没实现__set__
的descriptor
上两种情况里, __set__
方法都被实现了, 这时, 对应的class attribute是无法通过instance attribute覆盖的, 因为对应属性的所有赋值操作都会被这个__set__
拦截下来. 但如果没有__set__
, 那么可以将instance的这个descriptor attribute 覆盖掉, 当然, class的对应属性仍然为descriptor:
class D:
def __get__(self, instance, cls):
if instance is not None:
return 'name from descriptor'
else:
return self
class Foo:
name = D()
foo = Foo()
print('descriptor works by now:',foo.name)
foo.name = 3
print('no descriptor in the instance:', foo.name)
print('descriptor in the class:', Foo.name)
输出:
descriptor works by now: name from descriptor
no descriptor in the instance: 3
descriptor in the class: <__main__.D object at 0x7f4b14e76400>
所以, 实现了__set__
的descriptor也称为overriding descriptor
, 反之则称为non-overriding descriptor
.
类的方法也是descriptor
了解到这的时候着实感叹了一把, 连类的method也实现了descriptor协议, 可见在python底层中, descriptor的用途是极为广泛的. 不过呢, 对框架和库的纯使用者而言, 不知道它的存在也不会影响使用Python.
回到正题, method也是descriptor:
class Foo:
def method(self):
pass
print(hasattr(Foo.method, '__get__'))
print(hasattr(Foo.method, '__set__'))
输出:
True
False
method有__get__
没__set__
, 也就是说, 它是non-overriding descriptor
, 是可以被覆盖的:
class Foo:
def method(self):
pass
foo = Foo()
print(foo.method)
foo.method = 'overrided'
print(foo.method)
输出:
<bound method Foo.method of <__main__.Foo object at 0x7f4b14601320>>
overrided
总结
- descriptor用途很广
- descriptor在attribute 解析过程中优先级最高.
- 实现了
__set__
方法的为overriding descriptor
, 不会被instance attribute覆盖. 反之则为non-overriding descriptor
, 可覆盖. - 类的方法是
non-overriding descriptor
.