Python基礎:新式類的屬性訪問


一、概述

自從Python 2.2引入新式類(New-style classes)以后,元類(Metaclass)、描述符(Descriptor)和一些特殊方法(如__getattribute__)的出現,使得原本簡單的 屬性訪問(Attribute access)變得復雜起來。

對於 新式類的屬性訪問 這一主題,官方文檔 Customizing attribute accessDescriptor HowTo Guide 都是很好的參考,但感覺講得還不夠全面、通透。本文結合 官方文檔Python 2.7源碼,嘗試給出 屬性訪問的一般規則

在以下討論中,根據觸發方式的不同,屬性訪問分為 實例綁定的屬性訪問類綁定的屬性訪問;而根據操作類型的不同,訪問又包括 獲取設置刪除

二、准備工作

1、討論對象

下面的討論會涉及五個對象:

  • 實例a
  • A
  • 元類MetaA
  • 描述符類Descr
  • 屬性attr

它們之間的關系如下:

  • aA的實例
  • AMetaA的實例
  • attr可能是普通屬性,也可能是描述符(此時,attrDescr的實例)
  • 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對應的訪問規則為:

  1. 首先查找A中是否覆蓋了特殊方法__getattribute__

    • 存在則使用覆蓋版本,直接返回A.__getattribute__(a, 'attr')
    • 沒有覆蓋則使用默認版本,跳到步驟2
  2. 依次查找A.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr
      • 如果attr是數據描述符,則為情況(case_a)
      • 如果attr是非數據描述符,則為情況(case_b)
      • 如果attr是普通屬性,則為情況(case_c)
    • 如果沒有找到attr,則為情況(case_d)
  3. 如果為情況(case_a),則返回Descr.__get__(attr, a, A)

  4. 否則查找實例字典a.__dict__中是否存在屬性attr,存在則返回attr
  5. 否則如果為情況(case_b),則返回Descr.__get__(attr, a, A)
  6. 否則如果為情況(case_c),則返回attr
  7. 否則如果為情況(case_d)或者上述步驟拋出了AttributeError異常,則查找A中是否存在特殊方法__getattr__,存在則返回A.__getattr__(a, 'attr')
  8. 否則不存在屬性attr,拋出AttributeError異常

參考源碼

PyObject_GenericGetAttr

示例驗證

# 步驟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對應的訪問規則為:

  1. 首先查找A中是否覆蓋了特殊方法__setattr__

    • 存在則使用覆蓋版本,直接調用A.__setattr__(a, 'attr', value)
    • 沒有覆蓋則使用默認版本,跳到步驟2
  2. 依次查找A.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr,如果attr是描述符(定義__set__即可,參考 『更多細節』),則調用Descr.__set__(attr, a, value)
    • 否則(attr是未定義__set__的描述符或普通屬性,或者沒有找到attr),跳到步驟3
  3. 在實例字典a.__dict__中設置(有則改之,無則加之)屬性attr

參考源碼

PyObject_GenericSetAttr

示例驗證

# 步驟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對應的訪問規則為:

  1. 首先查找A中是否覆蓋了特殊方法__delattr__

    • 存在則使用覆蓋版本,直接調用A.__delattr__(a, 'attr')
    • 沒有覆蓋則使用默認版本,跳到步驟2
  2. 依次查找A.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr,如果attr是描述符(定義__delete__即可,參考 『更多細節』),則調用Descr.__delete__(attr, a)
    • 否則(attr是未定義__delete__的描述符或普通屬性,或者沒有找到attr),跳到步驟3
  3. 如果實例字典a.__dict__中存在屬性attr,則刪除該屬性

  4. 否則無法刪除不存在的屬性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對應的訪問規則為:

  1. 首先查找MetaA中是否覆蓋了特殊方法__getattribute__

    • 存在則使用覆蓋版本,直接返回MetaA.__getattribute__(A, 'attr')
    • 沒有覆蓋則使用默認版本,跳到步驟2
  2. 依次查找MetaA.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr
      • 如果attr是數據描述符,則為情況(case_a)
      • 如果attr是非數據描述符,則為情況(case_b)
      • 如果attr是普通屬性,則為情況(case_c)
    • 如果沒有找到attr,則為情況(case_d)
  3. 如果為情況(case_a),則返回Descr.__get__(attr, A, MetaA)

  4. 否則依次查找A.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr
      • 如果attr是描述符(定義__get__即可),則返回Descr.__get__(attr, None, A)
      • 如果attr是未定義__get__的描述符或普通屬性,則直接返回attr
    • 如果沒有找到attr,跳到步驟5
  5. 否則如果為情況(case_b),則返回Descr.__get__(attr, A, MetaA)

  6. 否則如果為情況(case_c),則返回attr
  7. 否則如果為情況(case_d)或者上述步驟拋出了AttributeError異常,則查找MetaA中是否存在特殊方法__getattr__,存在則返回MetaA.__getattr__(A, 'attr')
  8. 否則不存在屬性attr,拋出AttributeError異常

注意:差異點在 步驟4

參考源碼

type_getattro

示例驗證

請舉一反三

2、設置屬性

一般規則

A.attr = value對應的訪問規則為:

  1. 首先查找MetaA中是否覆蓋了特殊方法__setattr__

    • 存在則使用覆蓋版本,直接調用MetaA.__setattr__(A, 'attr', value)
    • 沒有覆蓋則使用默認版本,跳到步驟2
  2. 依次查找MetaA.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr,如果attr是描述符(定義__set__即可,參考 『更多細節』),則調用Descr.__set__(attr, A, value)
    • 否則(attr是未定義__set__的描述符或普通屬性,或者沒有找到attr),跳到步驟3
  3. 在類字典A.__dict__中設置(有則改之,無則加之)屬性attr

參考源碼

type_setattro

示例驗證

請舉一反三

3、刪除屬性

一般規則

del A.attr對應的訪問規則為:

  1. 首先查找MetaA中是否覆蓋了特殊方法__delattr__

    • 存在則使用覆蓋版本,直接調用MetaA.__delattr__(A, 'attr')
    • 沒有覆蓋則使用默認版本,跳到步驟2
  2. 依次查找MetaA.__mro__的類字典__dict__中是否存在屬性attr

    • 對於第一個找到的attr,如果attr是描述符(定義__delete__即可,參考 『更多細節』),則調用Descr.__delete__(attr, A)
    • 否則(attr是未定義__delete__的描述符或普通屬性,或者沒有找到attr),跳到步驟3
  3. 如果類字典A.__dict__中存在屬性attr,則刪除該屬性

  4. 否則無法刪除不存在的屬性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_DelAttrPyObject_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'


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM