資料描述符,屬性查詢優先順序
如果在一個類中定義了 __get__()
, __set__()
, __delete__()
這三種方法之一,那麼這個類是一個描述符。
描述符分成兩種:
- 如果這種類只定義了
__get__
方法,那麼就是一個非資料描述符, - 定義了
__get__()
和__set__()
的資料描述符。
描述符的用處就是,當一個物件的某個屬性是一個描述符時,你訪問這個描述符型別的屬性,就會呼叫這個描述符的方法。譬如你獲取描述符的值時,會呼叫它的__get__()
.
我們先看一下這三個方法的docstring:
def __delete__(self, *args, **kwargs): # real signature unknown
""" Delete an attribute of instance. """
# 刪除一個例項的屬性
def __set__(self, *args, **kwargs): # real signature unknown
""" Set an attribute of instance to value. """
# 給例項的屬性設定一個值
def __get__(self, *args, **kwargs): # real signature unknown
""" Return an attribute of instance, which is of type owner. """
# 返回例項的屬性,該例項是 `owner` 型別的
例項:
class A(object):
def __init__(self):
self.value = None
def __set__(self, instance, value): # self:類A的例項,也是類B的屬性a;instance:類 B 的例項 b;value:透過b.a的賦值
print('set: self,instance,value',self,instance,value)
self.value = value
return self.value
def __get__(self, instance, owner):# instance:類B的例項b;owner:類B
print('get: self,instance,owner',self,instance,owner)
return self.value
class B(object):
a = A()
def __init__(self):
self.val = 20
1.上述程式碼中,有兩個類,A,B。先看類 B,有一個類屬性 a , 且 a 是類 A 的例項,我們先來例項化一下類 B ,看一下 類 B 和例項 b 的屬性:
b = B()
print(b.__dict__)
print(B.__dict__)
"""
{'val': 20}
{'__module__': '__main__', 'a': <__main__.A object at 0x0163FD70>, '__init__': <function B.__init__ at 0x07845078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
"""
可以看出,例項 b 的屬性中,只有一個 val 屬性 ;類 B 的屬性中,則有一個 a ,且 a 是類 A 的一個物件。
2.接下來,我們呼叫一下例項 a:
b = B()
b.a
B.a
"""
get: self,instance,owner <__main__.A object at 0x03458E68> <__main__.B object at 0x03458F28> <class '__main__.B'>
get: self,instance,owner <__main__.A object at 0x03458E68> None <class '__main__.B'>
"""
我們看一下什麼意思:
-
當呼叫 b.a 時,程式會自動去呼叫
b.__getattribute__('a')
, 也就是b.__dict__['a']
, 即透過物件 b 的字典去查詢屬性,但是在第一步我們已經知道, 物件 b 只有一個屬性 {'val': 20} ,既然在例項 b 中找不到 a。 所以會去父類中找,呼叫:type(b).__dict__['a'].__get__(b,type(b))
, 也就是:B.__dict__['a'].__get__(b,B)
,列印了第一行的資訊,並返回了None -
當呼叫 B.a 時,會直接呼叫
B.__dict__['a'].__get__(None,B)
,所以第二處列印的資訊中間有個 None
3.現在,我們嘗試給 b.a 賦值
b = B()
b.a = 11
print(b.__dict__)
print(B.__dict__)
B.a = 12
print(b.__dict__)
print(B.__dict__)
"""
set: self,instance,value <__main__.A object at 0x037CFD70> <__main__.B object at 0x037CFDD0> 11
{'val': 20}
{'__module__': '__main__', 'a': <__main__.A object at 0x037CFD70>, '__init__': <function B.__init__ at 0x07E85078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
{'val': 20}
{'__module__': '__main__', 'a': 12, '__init__': <function B.__init__ at 0x07E85078>, '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
"""
可以看出,當呼叫了 b.a=11 時,呼叫了描述符的 __set__()
, 但是物件 b 的例項屬性並沒有改變,依然只有 val=20, 同時類B的類屬性也沒有改變。 但是當呼叫 B.a = 12 時,類屬性 a 變成了12,並沒有呼叫描述符的__set__()
方法。
所以,結合上面的 docstring,我們可以看出,資料描述符應該是給例項使用的,類使用它用處不大,至少沒法呼叫它的 __set__()
-
如果類屬性的描述符物件和例項物件的屬性同名,如果查詢?
-
也就是說,如果把類B改成:
class B(object):
a = A()
def __init__(self):
self.val = 20
self.a = 11 # 這裡同名的屬性
此時呼叫 b.a ,會如何?
當類A是一個資料描述符,也就是說類A包含 __set__
方法時,此時資料描述符優先順序高,所以例項屬性 self.a 其實就是對類屬性 a 的賦值,會呼叫資料描述符的 __set__
方法:
set: self,instance,value <__main__.A object at 0x009DFD70> <__main__.B object at 0x009DFDD0> 11
get: self,instance,owner <__main__.A object at 0x009DFD70> <__main__.B object at 0x009DFDD0> <class '__main__.B'>
11
當類A是一個非資料描述符,那麼例項的字典優先順序高,所以會使用例項字典中的資料,即結果:
11
屬性查詢優先順序:
-
obj.__getattribute__()
-
資料描述符
-
例項的字典
-
類的字典
-
非資料描述符
-
父類的字典
-
__getattr__
class Quantity1(object):
def __get__(self, instance, owner):
return 2
def __set__(self, instance, val):
pass
class Quantity2(object):
def __get__(self, instance, owner):
return 5
class A(object):
val = 6 # 6 父類屬性
x = None
class B(A):
val = Quantity2() # 5 非覆蓋型描述符
val = 4 # 4 類屬性
val = Quantity1() # 2 覆蓋型描述符
def __init__(self):
super(B, self).__init__()
self.val = 3
def __getattr__(self, name): # 7 __getattr__
return 7
def __getattribute__(self, name): # 1 __getattribute__
return 1
b = B()
print(b.val)
說了一堆有的沒的,其實描述符就是一個特殊的實現,當你的一個物件的屬性是描述符時,設定/賦值/讀取 這個屬性,都會觸發這個描述符內部相應實現的方法。從而可以實現一些定製化的內容。