描述器
对于Python的描述器的作用,我们可以先记住一句话:描述器是描述类的属性的。
描述器的魔术方法
先思考下面程序的执行流程:
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
class B:
x = A() # 定义(描述)类的属性
def __init__(self):
print('B.init')
print(B.x.a1) # 会输出什么?
b = B()
print(b.x.a1) # 会输出什么?
上例中,x作为B的类属性,但x并非是一个简单数据类型,而是A类的实例化对象。因此,访问属性x时,必然会实例化A。
因此,分别输出为:
print(B.x.a1) # 会输出
A.init
a1
---------------
b = B()
# 输出
A.init
B.init
print(b.x.a1) # 输出
a1
上例的代码,已经可以简单认为B类的属性x,被A类的实例来"描述"了。但Python中的描述器,有更为精确的定义,它要求必须实现某些魔术方法。
__get__
方法
__get__(self, instance, owner)
如果对上述A类,做一些改造。比如在A类中实现__get__
方法,看会发生什么事情。
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
def __get__(self, instance, owner): # A类增加了__get__方法
print("Class A.__get__{} {} {}".format(self,instance,owner))
class B:
x = A()
def __init__(self):
print('B.init')
print(B.x) # 会输出什么?
# 输出:
# A.init
# Class A.__get__<__main__.A object at 0x000001EDE1170A20> None <class '__main__.B'>
# None
b = B()
print(b.x) # 会输出什么?为什么?
# 输出:
# A.init
# B.init
# Class A.__get__<__main__.A object at 0x000001DDDF3A0A20> <__main__.B object at 0x000001DDDF3A0EF0> <class '__main__.B'>
# None
print(b.x.a1)
# 异常AttributeError: 'NoneType' object has no attribute 'a1'
# b.x竟然是None?
解释:因为定义了__get__
方法,A类就是一个描述器。
当对B类或者B类的实例的x属性读取时,变成对A类的实例的访问时,就会调用此方法。此方法的返回值,会作为A的实例化对象的返回值。
简单而言,B类的x属性,使用了一个A类去描述(定义)它。如果对x属性访问时,就会调用A类的__get__
方法(描述方法),其返回值会返回给x属性。
三个参数的解析:
参数 | 值 | 说明 |
self |
| 指当前实例,调用者 A(). 描述器实例自身 |
instance |
| 指代owner的实例 B(),描述器拥有者的实例对象 |
owner |
| 指属性所属的类 B,谁拥有这个描述器 |
正常情况下,__get__
魔术方法需要返回self,也就是A的实例,才能正常返回a1属性。
def __get__(self,instance,owner):
print("A.__get__{} {} {}".format(self,instance,owner))
return self
此时,我们可以总结描述器的定义了。
描述器的定义
描述器定义:
Python中,一个类实现了__get__,__set__,__delete__ ,三个方法中任何一个方法,就是描述器。
如果仅实现了__get__,就是非数据描述器non-data descriptor;
同时实现了__get__,__set__就是数据描述器 data descriptor;
因此,上述的例子,属于非数据描述器。
描述器一般都会实现__get__
方法,因为此方法的返回值就是作为被描述的类的属性的值。
因此,一个类的属性,完全可以使用另一个类的实例来描述它。由于类的实例完全可以自由定义,对于某个类属性,可以借助类,来支撑起更为强大的功能。
如果一个类的类属性设置为描述器,那么它被称为owner属主。比如上例中的B,它拥有了描述器A。
属性的访问顺序
为上例中的B类增加实例属性x
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
def __get__(self,instance,owner):
print("A.__get__{} {} {}".format(self,instance,owner))
return self
class B:
x = A() # 一定会输出A.init,因为定义必须有值,而该值必须是A的实例化。
def __init__(self):
print('B init')
self.x = 'b.x' # 增加实例属性x
print(B.x) # 输出:
# A.__get__<__main__.A object at 0x0000019BD760B400> None <class '__main__.B'>
# <__main__.A object at 0x0000019BD760B400> x就是A的实例self
print(B.x.a1) # 输出:
#A.__get__<__main__.A object at 0x000002929C0BB400> None <class '__main__.B'>
#a1
b = B()
print(b.x)
# A init
# B init
# b.x 访问了实例属性
print(b.x.a1) # AttributeError: 'str' object has no attribute 'a1'
b.x访问到了实例的属性,而不是描述器。
也就是说,对于非数据描述器,属性的访问顺序是符合一般预期的。
总结:
非数据描述器,只对类属性产生作用。当访问类属性时,将调用描述器的__get__
方法
__set__
方法
__set__(self,instance,value)
这里的instance是什么? value是什么?
继续修改代码,为类A增加__set__方法。
class A:
def __init__(self):
self.a1 = 'a1'
print('A.init')
def __get__(self,instance,owner):
print("A.__get__{} {} {}".format(self,instance,owner))
return self
def __set__(self,instance,value):
print('A.__set__ {} {} {}'.format(self,instance,value))
self.data = value
class B:
x = A() # 一定会输出A.init
def __init__(self):
print('B init')
self.x = 'b.x' # 增加实例属性x
#print(B.x)
#print(B.x.a1)
# 输出
# A.init
# A.__get__<__main__.A object at 0x00000276E5920B38> None <class '__main__.B'>
# <__main__.A object at 0x00000276E5920B38>
# A.__get__<__main__.A object at 0x00000276E5920B38> None <class '__main__.B'>
# a1
b = B()
# 输出:
# A.init
# B init
# A.__set__ <__main__.A object at 0x000002CE77C40B38> <__main__.B object at 0x000002CE77C40F60> b.x b实例属性的值传递到A上了。 看样子,调用了__set__
print(b.x) # 还是访问b实例属性吗?还是说变成了被描述器描述的属性?
print(b.x.a1)
# 输出:
# A.__get__<__main__.A object at 0x0000023250DE0B38> <__main__.B object at 0x0000023250DE0F60> <class '__main__.B'>
# <__main__.A object at 0x0000023250DE0B38>
# A.__get__<__main__.A object at 0x0000023250DE0B38> <__main__.B object at 0x0000023250DE0F60> <class '__main__.B'>
# a1 访问b.x.a1,返回了描述器的数据
如果当b进行属性赋值时(self.x = ‘b.x’),此时调用了set了。注意,此属性必须是被描述器描述过的类属性。
此时instance为b,values为b.x 。
一般都会使用实例来调用,而不是类来调用如B.x,因为B.x就不会调用__set__
方法。
当使用了__set__
方法后,属性的访问顺序发生了变化:
数据描述器 -->
实例的__dict__ -->
非数据描述器
数据描述器属性访问优先于实例的_dict_
实例的__dict__优先于非数据描述器。
__delete__方法在删除相关属性时触发,具有同样的效果,有了这个方法,就是数据描述器。
本质
观察数据描述器和非数据描述器时,属性字典的变化:
b.__dict__和 B.__dict__
屏蔽了__set__
方法结果如下:(非数据描述器)
class A:
n = 'A.x'
def __get__(self, instance, owner):
return self
# def __set__(self, instance, value):
# print('Call A __set__')
class B:
x = A()
def __init__(self):
self.x = 'b.x'
self.y = 'b.y'
print(B.__dict__)
# {'__module__': '__main__', 'x': <__main__.A object at 0x000001C9262370F0>, '__init__': <function B.__init__ at 0x000001C9262D7AE8>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
print(B().__dict__)
# {'x': 'b.x', 'y': 'b.y'} # 实例字典符合一般预期
不屏蔽__set__
方法结果如下:(数据描述器)
print(B.__dict__)
# {'__module__': '__main__', 'x': <__main__.A object at 0x000001D02F1570F0>, '__init__': <function B.__init__ at 0x000001D02F1F7B70>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
print(B().__dict__)
# Call A __set__
# {'y': 'b.y'} # x属性不见了
因此,是数据描述器时,把实例属性从属性字典中去除了,此时当然先访问数据描述器了。
为什么b.x不存在b的字典中?按以前的理解,应该会添加一个key value。但由于它被描述了,行为就不同了。
注意,因为b.y不是描述器,则它的行为还是如常表现,加入到字典中。
而B的dict也不变,因为实例的赋值不会影响类属性。
因此,如果类的属性是一个描述器,此时实例属性赋值的行为就改变了。但是对于用户而言,并不需要关心它是否为描述器。因此,描述器的行为就必须表现得和普通的属性赋值一样。
因此,需要做以下处理:
def __set__(self,instance,value):
print('A.__set__ {} {} {}'.format(self,instance,value))
instance.__dict__['x'] = value
此时,描述器进行赋值时,对类属性的读或者写会调用描述器的__get__
或者__set__
方法。
而instance为b,此时在set方法中把‘x’的值手动添加一个kv到instance的dict中。如此再访问b.x的时候,注意是访问描述器,然后到__get__
方法中。
因为b.x一定会访问描述器,而不是b.__dict__
。此时拦截了访问字典的路径了。
如果b.x需要返回’b.x’,可以使用直接的方式:
def __get__(self,instance,owner):
print("A.__get__{} {} {}".format(self,instance,owner))
return instance.__dict__['x']
总结
数据描述器,针对类属性和实例属性都产生作用。当访问或者修改此属性时,将调用相应的__get__
或者__set__
方法
我们在《浅谈Python中的反射》这篇文章中知道,当定义了魔术方法__setattr__
时,当对属性进行设置时,会阻止设置实例属性加入实例字典的行为,而调用此方法。
而魔术方法__set__
中又得知,当对属性进行设置时,会调用此方法。那么,这两个方法的访问优先级是如何的?
看下面的例子:
class A:
n = 'A.x'
def __init__(self,name):
self.name = name
def __get__(self, instance, owner):
print('Call A __get__')
return self
def __set__(self, instance, value):
print('Call A __set__')
class B:
x = A('x')
def __init__(self):
self.x = 'b.x'
def __setattr__(self, key, value):
print('Call B __setattr__')
b = B()
b.x = 'b.x1'
print(b.x)
# 输出
# Call B __setattr__
# Call B __setattr__
# Call A __get__
# <__main__.A object at 0x0000020BC64E0A20>
以上结果可知,当设置实例属性时,先调用实例本身的__setattr__
方法,当读取属性时调用了描述器的__get__
方法
因此,我们可以更为准确得出实例属性访问顺序:
实例调用__getattribute__ -->
__setattr__ 拦截 -->
(数据描述器) -->
instance.__dict__ -->
(非数据描述器) -->
instance.__class__.__dict__ -->
继承的祖先类(直到object).__dict__ -->
找不到 -->
调用__getattr__
Python中描述器的应用
描述器在Python中应用非常广泛。
Python的方法,普通的实例方法,包括staticmethod
和classmethod
,都实现为非数据描述器。因此,实例可以重新定义和覆盖方法,表现为正常的字典属性的访问和修改 。这允许单个实例获取与同一类的其他实例不同的行为。
property()函数实现为一个数据描述器,因此,实例不能覆盖属性的行为。
class A:
@classmethod # 非数据描述器
def foo(cls):
pass
@staticmethod # 非数据描述器
def bar():
pass
@property # 数据描述器
def z(self):
return 5
def getfoo(self): # 非数据描述器
return self.foo
def __init__(self): # 非数据描述器
self.foo = 100
self.bar = 200
#self.z = 300 # AttributeError: can't set attribute 无法覆盖数据描述器的定义
a = A()
print(a.__dict__) # {'foo': 100, 'bar': 200} 看不到z属性
print(A.__dict__) # {'__module__': '__main__', 'foo': <classmethod object at 0x0000017674C6B7B8>, 'bar': <staticmethod object at 0x0000017674C6B7F0>, 'z': <property object at 0x0000017674BCD598>, 'getfoo': <function A.getfoo at 0x0000017674C58C80>, '__init__': <function A.__init__ at 0x0000017674C58D08>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
@staticmethod的描述器版本实现
class Staticmethod:
def __init__(self,fn):
self._fn = fn
def __get__(self, instance, owner):
return self._fn
class Foo:
@Staticmethod # show = Staticmethod(show) 相当于类属性show,被类Staticmethod描述了。返回值为__get__的返回值。
def show():
print('I am staticmethod')
f = Foo()
f.show()
@classmethod的描述器版本实现
from functools import partial
class ClassMethod:
def __init__(self,fn):
self._fn = fn
def __get__(self, instance, owner):
return partial(self._fn,owner)
class Bar:
@ClassMethod # show = ClassMethod(show)
def show(cls):
print(cls.__name__)
Bar.show()
参数类型检查功能
描述器方式实现
class Typed:
def __init__(self,arg,typed):
self.arg = arg
self.typed = typed
def __get__(self, instance, owner):
if instance is not None:
return instance.__dict__[self.name]
return self
def __set__(self, instance, value):
if not isinstance(value,self.typed):
raise Exception('type error')
instance.__dict__[self.arg] = value
import inspect
def typeassert(cls):
obj = inspect.signature(cls).parameters
for name,value in obj.items():
if value.annotation != value.empty:
setattr(cls,name,Typed(name, value.annotation))
return cls
@typeassert # Person = typeassert(Person)
class Person:
#name = Typed('name',str)
#age = Typed('age',int)
def __init__(self,name:str, age:int):
self.name = name
self.age = age
tom = Person('tom',100)
print(tom.__dict__)
类装饰器方式实现
class Typed:
def __init__(self,arg,typed):
self.arg = arg
self.typed = typed
def __get__(self, instance, owner):
if instance is not None:
return instance.__dict__[self.name]
return self
def __set__(self, instance, value):
if not isinstance(value,self.typed):
raise Exception('type error')
instance.__dict__[self.arg] = value
import inspect
# def typeassert(cls):
# obj = inspect.signature(cls).parameters
# for name,value in obj.items():
# if value.annotation != value.empty:
# setattr(cls,name,Typed(name, value.annotation))
# return cls
class TypeAssert:
def __init__(self,cls):
obj = inspect.signature(cls).parameters
for name,value in obj.items():
if value.annotation != value.empty:
setattr(cls,name,Typed(name,value.annotation))
self.cls = cls
def __call__(self, name, age):
return self.cls(name,age)
@TypeAssert # Person = TypeAssert(Person)
class Person:
#name = Typed('name',str)
#age = Typed('age',int)
def __init__(self,name:str, age:int):
self.name = name
self.age = age
tom = Person('tom',100)
print(tom.__dict__)