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

1、概述

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

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

在如下討論中,根據觸發方式的不一樣,屬性訪問分爲 實例綁定的屬性訪問類綁定的屬性訪問;而根據操做類型的不一樣,訪問又包括 獲取設置刪除算法

2、準備工做

一、討論對象

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

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

它們之間的關係以下:數組

  • aA的實例
  • AMetaA的實例
  • attr多是普通屬性,也多是描述符(此時,attrDescr的實例)
  • attr可能位於a的實例字典中,也可能位於A的MRO的類字典中,還可能位於MetaA的MRO的類字典中

二、名詞解釋

如下是討論過程當中會用到的名詞:markdown

  • 實例綁定:經過實例訪問屬性的方式,如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__的描述符

3、實例綁定的屬性訪問

一、獲取屬性

通常規則

a.attr對應的訪問規則爲:ide

  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)ui

  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__'

二、設置屬性

通常規則

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__

三、刪除屬性

通常規則

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__

4、類綁定的屬性訪問

在上述對 實例綁定的屬性訪問 的討論中,若是把 實例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對應的訪問規則爲:

  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

示例驗證

請觸類旁通

二、設置屬性

通常規則

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

示例驗證

請觸類旁通

三、刪除屬性

通常規則

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(參考 『更多細節』)

示例驗證

請觸類旁通

5、更多細節

一、屬性的設置與刪除

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_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__(也是數據描述符)

6、簡單自測

上面關於屬性訪問的所有細節,您是否真的懂了?觀察下面的現象,嘗試解釋其中的緣由:

# 現象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'
相關文章
相關標籤/搜索