一、概述
自從Python 2.2引入新式類(New-style classes)以后,元類(Metaclass)、描述符(Descriptor)和一些特殊方法(如__getattribute__
)的出現,使得原本簡單的 屬性訪問(Attribute access)變得復雜起來。
對於 新式類的屬性訪問 這一主題,官方文檔 Customizing attribute access 和 Descriptor HowTo Guide 都是很好的參考,但感覺講得還不夠全面、通透。本文結合 官方文檔 和 Python 2.7源碼,嘗試給出 屬性訪問的一般規則。
在以下討論中,根據觸發方式的不同,屬性訪問分為 實例綁定的屬性訪問 和 類綁定的屬性訪問;而根據操作類型的不同,訪問又包括 獲取、設置 和 刪除。
二、准備工作
1、討論對象
下面的討論會涉及五個對象:
- 實例
a
- 類
A
- 元類
MetaA
- 描述符類
Descr
- 屬性
attr
它們之間的關系如下:
a
是A
的實例A
是MetaA
的實例attr
可能是普通屬性,也可能是描述符(此時,attr
是Descr
的實例)attr
可能位於a
的實例字典中,也可能位於A
的MRO的類字典中,還可能位於MetaA
的MRO的類字典中
2、名詞解釋
以下是討論過程中會用到的名詞:
- 實例綁定:通過實例訪問屬性的方式,如
a.attr
- 類綁定:通過類訪問屬性的方式,如
A.attr
- 實例字典:實例中的屬性字典,如
a.__dict__
- 類字典:類中的屬性字典,如
A.__dict__
- 類的MRO(Method Resolution Order):類及其基類組成的序列,如
A.__mro__
- 元類:用於創建類的類,如
MetaA
- 普通屬性:不是描述符的屬性
- 描述符:如果一個類(如
Descr
)中存在__get__
、__set__
和__delete__
三種特殊方法的任意組合,那么該類的實例就是一個描述符 - 數據描述符(data descriptor):定義了
__get__
和__set__
的描述符 - 非數據描述符(non-data descriptor):只定義了
__get__
的描述符
三、實例綁定的屬性訪問
1、獲取屬性
一般規則
a.attr
對應的訪問規則為:
-
首先查找
A
中是否覆蓋了特殊方法__getattribute__
:- 存在則使用覆蓋版本,直接返回
A.__getattribute__(a, 'attr')
- 沒有覆蓋則使用默認版本,跳到步驟2
- 存在則使用覆蓋版本,直接返回
-
依次查找
A.__mro__
的類字典__dict__
中是否存在屬性attr
:- 對於第一個找到的
attr
:- 如果
attr
是數據描述符,則為情況(case_a) - 如果
attr
是非數據描述符,則為情況(case_b) - 如果
attr
是普通屬性,則為情況(case_c)
- 如果
- 如果沒有找到
attr
,則為情況(case_d)
- 對於第一個找到的
-
如果為情況(case_a),則返回
Descr.__get__(attr, a, A)
- 否則查找實例字典
a.__dict__
中是否存在屬性attr
,存在則返回attr
- 否則如果為情況(case_b),則返回
Descr.__get__(attr, a, A)
- 否則如果為情況(case_c),則返回
attr
- 否則如果為情況(case_d)或者上述步驟拋出了AttributeError異常,則查找
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__'
2、設置屬性
一般規則
a.attr = value
對應的訪問規則為:
-
首先查找
A
中是否覆蓋了特殊方法__setattr__
:- 存在則使用覆蓋版本,直接調用
A.__setattr__(a, 'attr', value)
- 沒有覆蓋則使用默認版本,跳到步驟2
- 存在則使用覆蓋版本,直接調用
-
依次查找
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__
3、刪除屬性
一般規則
del a.attr
對應的訪問規則為:
-
首先查找
A
中是否覆蓋了特殊方法__delattr__
:- 存在則使用覆蓋版本,直接調用
A.__delattr__(a, 'attr')
- 沒有覆蓋則使用默認版本,跳到步驟2
- 存在則使用覆蓋版本,直接調用
-
依次查找
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基礎:元類)。
1、獲取屬性
一般規則
A.attr
對應的訪問規則為:
-
首先查找
MetaA
中是否覆蓋了特殊方法__getattribute__
:- 存在則使用覆蓋版本,直接返回
MetaA.__getattribute__(A, 'attr')
- 沒有覆蓋則使用默認版本,跳到步驟2
- 存在則使用覆蓋版本,直接返回
-
依次查找
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)
- 否則如果為情況(case_c),則返回
attr
- 否則如果為情況(case_d)或者上述步驟拋出了AttributeError異常,則查找
MetaA
中是否存在特殊方法__getattr__
,存在則返回MetaA.__getattr__(A, 'attr')
- 否則不存在屬性
attr
,拋出AttributeError異常
注意:差異點在 步驟4
參考源碼
示例驗證
請舉一反三
2、設置屬性
一般規則
A.attr = value
對應的訪問規則為:
-
首先查找
MetaA
中是否覆蓋了特殊方法__setattr__
:- 存在則使用覆蓋版本,直接調用
MetaA.__setattr__(A, 'attr', value)
- 沒有覆蓋則使用默認版本,跳到步驟2
- 存在則使用覆蓋版本,直接調用
-
依次查找
MetaA.__mro__
的類字典__dict__
中是否存在屬性attr
:- 對於第一個找到的
attr
,如果attr
是描述符(定義__set__
即可,參考 『更多細節』),則調用Descr.__set__(attr, A, value)
- 否則(
attr
是未定義__set__
的描述符或普通屬性,或者沒有找到attr
),跳到步驟3
- 對於第一個找到的
-
在類字典
A.__dict__
中設置(有則改之,無則加之)屬性attr
參考源碼
示例驗證
請舉一反三
3、刪除屬性
一般規則
del A.attr
對應的訪問規則為:
-
首先查找
MetaA
中是否覆蓋了特殊方法__delattr__
:- 存在則使用覆蓋版本,直接調用
MetaA.__delattr__(A, 'attr')
- 沒有覆蓋則使用默認版本,跳到步驟2
- 存在則使用覆蓋版本,直接調用
-
依次查找
MetaA.__mro__
的類字典__dict__
中是否存在屬性attr
:- 對於第一個找到的
attr
,如果attr
是描述符(定義__delete__
即可,參考 『更多細節』),則調用Descr.__delete__(attr, A)
- 否則(
attr
是未定義__delete__
的描述符或普通屬性,或者沒有找到attr
),跳到步驟3
- 對於第一個找到的
-
如果類字典
A.__dict__
中存在屬性attr
,則刪除該屬性 - 否則無法刪除不存在的屬性
attr
,拋出AttributeError異常
參考源碼
type_setattro(參考 『更多細節』)
示例驗證
請舉一反三
五、更多細節
1、屬性的設置與刪除
CPython實現中,刪除屬性 被視為是 設置屬性 的一種特殊情況(參考 PyObject_DelAttr):
#define PyObject_DelAttr(O,A) PyObject_SetAttr((O),(A),NULL)
因此,在上述討論的 參考源碼 中,您會發現 設置屬性 和 刪除屬性 調用的函數其實是一樣的。
2、描述符
區分處理
以 實例綁定的屬性訪問 為例(類綁定的屬性訪問 類似),如果 設置屬性 和 刪除屬性 最終都調用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'