一、概述
自從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'
