自從Python 2.2引入新式類(New-style classes)之後,元類(Metaclass)、描述符(Descriptor)和一些特殊方法(如__getattribute__
)的出現,使得本來簡單的 屬性訪問(Attribute access)變得複雜起來。html
對於 新式類的屬性訪問 這一主題,官方文檔 Customizing attribute access 和 Descriptor HowTo Guide 都是很好的參考,但感受講得還不夠全面、通透。本文結合 官方文檔 和 Python 2.7源碼,嘗試給出 屬性訪問的通常規則。python
在如下討論中,根據觸發方式的不一樣,屬性訪問分爲 實例綁定的屬性訪問 和 類綁定的屬性訪問;而根據操做類型的不一樣,訪問又包括 獲取、設置 和 刪除。算法
下面的討論會涉及五個對象:c#
a
A
MetaA
Descr
attr
它們之間的關係以下:數組
a
是A
的實例A
是MetaA
的實例attr
多是普通屬性,也多是描述符(此時,attr
是Descr
的實例)attr
可能位於a
的實例字典中,也可能位於A
的MRO的類字典中,還可能位於MetaA
的MRO的類字典中如下是討論過程當中會用到的名詞:markdown
a.attr
A.attr
a.__dict__
A.__dict__
A.__mro__
MetaA
Descr
)中存在__get__
、__set__
和__delete__
三種特殊方法的任意組合,那麼該類的實例就是一個描述符__get__
和__set__
的描述符__get__
的描述符a.attr
對應的訪問規則爲:ide
首先查找A
中是否覆蓋了特殊方法__getattribute__
:函數
A.__getattribute__(a, 'attr')
依次查找A.__mro__
的類字典__dict__
中是否存在屬性attr
:學習
attr
:
attr
是數據描述符,則爲狀況(case_a)attr
是非數據描述符,則爲狀況(case_b)attr
是普通屬性,則爲狀況(case_c)attr
,則爲狀況(case_d)若是爲狀況(case_a),則返回Descr.__get__(attr, a, A)
ui
a.__dict__
中是否存在屬性attr
,存在則返回attr
Descr.__get__(attr, a, A)
attr
A
中是否存在特殊方法__getattr__
,存在則返回A.__getattr__(a, 'attr')
attr
,拋出AttributeError異常# 步驟8:不存在屬性attr,拋出AttributeError異常 >>> class A(object): pass ... >>> a = A() >>> a.attr Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'A' object has no attribute 'attr' # 步驟7:A中存在特殊方法__getattr__,返回A.__getattr__(a, 'attr') >>> class A(object): ... def __getattr__(self, name): ... return name + ' in __getattr__' ... >>> a = A() >>> a.attr 'attr in __getattr__' # 步驟6:類字典A.__dict__中存在普通屬性attr,返回A.__dict__['attr'] >>> class A(object): ... attr = 'ordinary attribute in A' ... def __getattr__(self, name): ... return name + ' in __getattr__' ... >>> a = A() >>> a.attr 'ordinary attribute in A' # 步驟5:類字典A.__dict__中存在非數據描述符attr,返回Descr.__get__(attr, a, A) >>> class Descr(object): ... def __get__(self, instance, owner): ... return 'non-data descriptor in A' ... >>> class A(object): ... attr = Descr() ... def __getattr__(self, name): ... return name + ' in __getattr__' ... >>> a = A() >>> a.attr 'non-data descriptor in A' # 步驟4:實例字典a.__dict__中存在屬性attr,返回a.__dict__['attr'] >>> class Descr(object): ... def __get__(self, instance, owner): ... return 'non-data descriptor in A' ... >>> class A(object): ... attr = Descr() ... def __init__(self): ... self.attr = 'attribute in a' ... def __getattr__(self, name): ... return name + ' in __getattr__' ... >>> a = A() >>> a.attr 'attribute in a' # 步驟3:類字典A.__dict__中存在數據描述符attr,返回Descr.__get__(attr, a, A) >>> class Descr(object): ... def __get__(self, instance, owner): ... return 'data descriptor in A' ... def __set__(self, instance, value): ... pass ... >>> class A(object): ... attr = Descr() ... def __init__(self): ... self.attr = 'attribute in a' ... def __getattr__(self, name): ... return name + ' in __getattr__' ... >>> a = A() >>> a.attr 'data descriptor in A' # 步驟1:A中覆蓋了特殊方法__getattribute__,返回A.__getattribute__(a, 'attr') >>> class Descr(object): ... def __get__(self, instance, owner): ... return 'data descriptor in A' ... def __set__(self, instance, value): ... pass ... >>> class A(object): ... attr = Descr() ... def __init__(self): ... self.attr = 'attribute in a' ... def __getattribute__(self, name): ... return name + ' in __getattribute__' ... def __getattr__(self, name): ... return name + ' in __getattr__' ... >>> a = A() >>> a.attr 'attr in __getattribute__'
a.attr = value
對應的訪問規則爲:
首先查找A
中是否覆蓋了特殊方法__setattr__
:
A.__setattr__(a, 'attr', value)
依次查找A.__mro__
的類字典__dict__
中是否存在屬性attr
:
attr
,若是attr
是描述符(定義__set__
便可,參考 『更多細節』),則調用Descr.__set__(attr, a, value)
attr
是未定義__set__
的描述符或普通屬性,或者沒有找到attr
),跳到步驟3在實例字典a.__dict__
中設置(有則改之,無則加之)屬性attr
# 步驟3:在實例字典a.__dict__中設置屬性attr,即執行a.__dict__['attr'] = value >>> class A(object): pass ... >>> a = A() >>> a.attr = 'newbie' >>> a.__dict__['attr'] 'newbie' # 步驟2:類字典A.__dict__中存在定義了__set__的描述符,調用Descr.__set__(attr, a, value) >>> class Descr(object): ... def __set__(self, instance, value): ... print('set {0!r} within descriptor'.format(value)) ... >>> class A(object): ... attr = Descr() ... >>> a = A() >>> a.attr = 'newbie' set 'newbie' within descriptor # 步驟1:A中覆蓋了特殊方法__setattr__,調用A.__setattr__(a, 'attr', value) >>> class Descr(object): ... def __set__(self, instance, value): ... print('set {0!r} within descriptor'.format(value)) ... >>> class A(object): ... attr = Descr() ... def __setattr__(self, name, value): ... print('set {0!r} in __setattr__'.format(value)) ... >>> a = A() >>> a.attr = 'newbie' set 'newbie' in __setattr__
del a.attr
對應的訪問規則爲:
首先查找A
中是否覆蓋了特殊方法__delattr__
:
A.__delattr__(a, 'attr')
依次查找A.__mro__
的類字典__dict__
中是否存在屬性attr
:
attr
,若是attr
是描述符(定義__delete__
便可,參考 『更多細節』),則調用Descr.__delete__(attr, a)
attr
是未定義__delete__
的描述符或普通屬性,或者沒有找到attr
),跳到步驟3若是實例字典a.__dict__
中存在屬性attr
,則刪除該屬性
attr
,拋出AttributeError異常PyObject_GenericSetAttr(參考 『更多細節』)
# 步驟4:沒法刪除不存在的屬性attr,拋出AttributeError異常 >>> class A(object): pass ... >>> a = A() >>> del a.attr Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'A' object has no attribute 'attr' # 步驟3:實例字典a.__dict__中存在屬性attr,刪除該屬性 >>> class A(object): ... def __init__(self): ... self.attr = 'dying' ... >>> a = A() >>> del a.attr # 步驟2:類字典A.__dict__中存在定義了__delete__的描述符,調用Descr.__delete__(attr, a) >>> class Descr(object): ... def __delete__(self, instance): ... print('delete within descriptor') ... >>> class A(object): ... attr = Descr() ... >>> a = A() >>> del a.attr delete within descriptor # 步驟1:A中覆蓋了特殊方法__delattr__,調用A.__delattr__(a, 'attr') >>> class Descr(object): ... def __delete__(self, instance): ... print('delete within descriptor') ... >>> class A(object): ... attr = Descr() ... def __delattr__(self, name): ... print('delete in __delattr__') ... >>> a = A() >>> del a.attr delete in __delattr__
在上述對 實例綁定的屬性訪問 的討論中,若是把 實例a 換成 類A,把 類A 換成 元類MetaA,幾乎就是 類綁定的屬性訪問 的全過程。
是的,兩種訪問過程的算法模型幾乎徹底一致,只有很是微小的差別。從這一點上,也能夠看出Python語言的設計是很是優秀的:Special cases aren't special enough to break the rules。「擁抱一致性,減小特例」,這也是值得咱們學習的態度。
在如下討論中,爲了保證結論的完整性,會給出 通常規則 的全貌,並特別指出差別點;但爲了DRY(Don’t Repeat Yourself),將再也不給出 示例驗證 部分,由於只要明白 「元類MetaA 與 類A」 和 「類A 與 實例a」 是關係對等的,就能夠觸類旁通了(若是不明白,能夠參考 Python基礎:元類)。
A.attr
對應的訪問規則爲:
首先查找MetaA
中是否覆蓋了特殊方法__getattribute__
:
MetaA.__getattribute__(A, 'attr')
依次查找MetaA.__mro__
的類字典__dict__
中是否存在屬性attr
:
attr
:
attr
是數據描述符,則爲狀況(case_a)attr
是非數據描述符,則爲狀況(case_b)attr
是普通屬性,則爲狀況(case_c)attr
,則爲狀況(case_d)若是爲狀況(case_a),則返回Descr.__get__(attr, A, MetaA)
不然依次查找A.__mro__
的類字典__dict__
中是否存在屬性attr
:
attr
:
attr
是描述符(定義__get__
便可),則返回Descr.__get__(attr, None, A)
attr
是未定義__get__
的描述符或普通屬性,則直接返回attr
attr
,跳到步驟5不然若是爲狀況(case_b),則返回Descr.__get__(attr, A, MetaA)
attr
MetaA
中是否存在特殊方法__getattr__
,存在則返回MetaA.__getattr__(A, 'attr')
attr
,拋出AttributeError異常注意:差別點在 步驟4
請觸類旁通
A.attr = value
對應的訪問規則爲:
首先查找MetaA
中是否覆蓋了特殊方法__setattr__
:
MetaA.__setattr__(A, 'attr', value)
依次查找MetaA.__mro__
的類字典__dict__
中是否存在屬性attr
:
attr
,若是attr
是描述符(定義__set__
便可,參考 『更多細節』),則調用Descr.__set__(attr, A, value)
attr
是未定義__set__
的描述符或普通屬性,或者沒有找到attr
),跳到步驟3在類字典A.__dict__
中設置(有則改之,無則加之)屬性attr
請觸類旁通
del A.attr
對應的訪問規則爲:
首先查找MetaA
中是否覆蓋了特殊方法__delattr__
:
MetaA.__delattr__(A, 'attr')
依次查找MetaA.__mro__
的類字典__dict__
中是否存在屬性attr
:
attr
,若是attr
是描述符(定義__delete__
便可,參考 『更多細節』),則調用Descr.__delete__(attr, A)
attr
是未定義__delete__
的描述符或普通屬性,或者沒有找到attr
),跳到步驟3若是類字典A.__dict__
中存在屬性attr
,則刪除該屬性
attr
,拋出AttributeError異常type_setattro(參考 『更多細節』)
請觸類旁通
CPython實現中,刪除屬性 被視爲是 設置屬性 的一種特殊狀況(參考 PyObject_DelAttr):
#define PyObject_DelAttr(O,A) PyObject_SetAttr((O),(A),NULL)
所以,在上述討論的 參考源碼 中,您會發現 設置屬性 和 刪除屬性 調用的函數實際上是同樣的。
以 實例綁定的屬性訪問 爲例(類綁定的屬性訪問 相似),若是 設置屬性 和 刪除屬性 最終都調用PyObject_GenericSetAttr
,那麼在判斷描述符的時候,又是如何區分並調用__set__
和__delete__
的呢?
實際上,PyObject_GenericSetAttr
最終調用了_PyObject_GenericSetAttrWithDict
,觀察函數_PyObject_GenericSetAttrWithDict
中 對描述符的判斷方法,咱們能夠發現:只要函數指針tp_descr_set
不爲空,就會調用它指向的函數完成操做。
而在 數組slotdefs 中,咱們又發現__set__
和__delete__
都對應一樣的函數指針tp_descr_set
,並被賦值指向同一個函數slot_tp_descr_set
;更進一步地,在函數slot_tp_descr_set
中,會判斷入參指針value
,若是爲空則調用__delete__
,不然調用__set__
。此時,再回頭看看PyObject_DelAttr
和PyObject_SetAttr
的區別,咱們會發現 刪除 和 設置 的區分標準是一致的。
至此,問題的答案應該很清楚了:
__set__
,函數指針tp_descr_set
就不爲空,就會進一步調用函數slot_tp_descr_set
,並在該函數中再實際調用函數__set__
__delete__
,函數指針tp_descr_set
也不爲空,也會進一步調用函數slot_tp_descr_set
,並在該函數中再實際調用函數__delete__
咱們再來看看描述符的定義:
若是一個類(如
Descr
)中存在__get__
、__set__
和__delete__
三種特殊方法的任意組合,那麼該類的實例就是一個描述符
從排列組合的層面計算,總共有 7 種合法的描述符;但從實用的角度考慮,常見的是如下三種描述符(固然也不排除您可能的應用創新:-)):
__get__
(非數據描述符)__get__
和__set__
(數據描述符)__get__
、__set__
和__delete__
(也是數據描述符)上面關於屬性訪問的所有細節,您是否真的懂了?觀察下面的現象,嘗試解釋其中的緣由:
# 現象1 >>> class Descr(object): ... def __delete__(self, instance): ... pass ... >>> class A(object): ... attr = Descr() ... def __init__(self): ... self.attr = 'why' ... >>> a = A() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __init__ AttributeError: __set__ # 現象2 >>> class Descr(object): ... def __set__(self, instance, value): ... pass ... >>> class A(object): ... attr = Descr() ... >>> a = A() >>> a.attr = 'why' >>> del a.attr Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: __delete__ # 現象3 >>> class Descr(object): ... def __get__(self, instance, owner): ... return 'why' ... >>> class A(object): ... attr = Descr() ... def __init__(self): ... self.attr = Descr() ... >>> a = A() >>> a.attr <__main__.Descr object at 0x8c483ec> >>> A.attr 'why'