描述器
對於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 | <__main__.A object at 0x000001DDDF3A0A20> |
指當前實例,調用者 A(). 描述器實例自身 |
instance | <__main__.B object at 0x000001DDDF3A0EF0> |
指代owner的實例 B(),描述器擁有者的實例對象 |
owner | <class '__main__.B'> |
指屬性所屬的類 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__)