python-__getattr__ 和 __getattribute__

bitterz發表於2019-01-25

python3完全使用了新式類,廢棄了舊式類,getattribute作為新式類的一個特性有非常奇妙的作用。檢視一些部落格和文章後,發現想要徹底理解getattr和getattribute的區別,實際上需要理解python中屬性的查詢順序、描述器(descriptor)、__get__、__set__、__dict__等知識。

首先需要指出,如果一個類只定義了__get__方法則稱之為non-data descriptor(非資料描述器),如果這個類將__get__和 __set__都定義了,則稱之為data descriptor(資料描述器),具體可見我的另一篇部落格,方便理解。下面介紹例項屬性的查詢順序。假設 t = T(),t.at的查詢順序如下:

1、如果是python自動產生的屬性,返回,否則進行第2步

2、如果‘at’出現在了T或者其父類和祖先類__dict__中(即‘at’是一個類屬性,並非只屬於t的例項屬性),並且at是一個data descriptor,則優先呼叫其__get__方法。不是data descriptor或者沒有該屬性則進行第2步。

3、查詢例項t的__dict__中是否有at屬性,有則返回,沒有則到第3步。

4、查詢t的父類和祖先類的__dict__中是否有at屬性,如果沒有則執行第4步,如果有則執行如下步驟:

​ 4.1 at是一個non-data descriptor,呼叫其__get__方法,不是則執行3.2

​ 4.2 返回__dict__[`at`]

5、如果例項t的父類中有__getattr__方法,則呼叫該方法,沒有則丟擲AttributeError。

注意每次類或例項呼叫屬性時getattribute會被無條件首先呼叫。下方程式碼略長,耐心檢視。

class Descriptor:  # 定義描述器的類
    def __get__(self, instance, owner):  # get方法用於返回例項的a屬性
        print(`3 get called,`, `instance is`, instance, `,owner is`, owner)
        return instance.a

    def __set__(self, instance, value):  # set方法用於修改例項的a屬性
        print(`4 set called,`, `instance is`, instance, `,value is`, value)
        instance.a = value**2

    def __getattribute__(self, item): 
        print(`5 Des getattribute called, item is %s` % item)

class NotDescriptor: # 定義non-data descriptor
    def __get__(self, instance, owner):
        print(`6 get called,`, `instance is`, instance, `,owner is`, owner)
        return instance.a - 100

class T:
    desc = Descriptor()  # 類屬性,一個資料描述器 data descriptor
    Not_Desc = NotDescriptor()  # 類屬性,一個非資料描述器 non-data descriptor    
    def __init__(self):
        self.a = 123
        self.not_desc = `instance not_desc`
        
    def func(self):
        return `T func`

    def __getattribute__(self, item):
        print(`1 getattribute called, item is `, item)
        return object.__getattribute__(self, item)

    def __getattr__(self, item):
        print(`2 getattr called, item is`, item)
        raise AttributeError(`NO such attr %s` % item)

以下的測試均使用上方的程式碼,每次的輸出操作都重置過,以避免混淆。

1 getattribute called, item is  func
T func  # 例項的函式
1 getattribute called, item is  a
123  # 例項的屬性
==================================================
4 set called, instance is <__main__.T object at 0x000001E55B630240> ,value is 2
# set方法前不呼叫getattribute。instance是擁有該描述器類的一個例項。value是要設定的值。
==================================================
1 getattribute called, item is  desc
3 get called, instance is <__main__.T object at 0x000001E55B630240> ,owner is <class `__main__.T`>
# get方法前還是優先呼叫getattribute,instance是擁有該描述器物件的一個例項。owner是擁有者本身
1 getattribute called, item is  a
t.a = 4 # 2**2 = 4

可見呼叫例項的屬性和方法時都會無條件首先呼叫getattrtibute方法,而set方法則不會呼叫。另外程式碼利用描述器的get和set方法,實際上已經實現了類似於@property的使用(當然,property裝飾器其實就是基於描述器實現的,對property裝飾器不瞭解的話可以檢視我的另一篇部落格)。

下面繼續使用上方程式碼驗證__getattr__、非描述器的執行順序。

t = T()
print(t.not_desc)
--------結果如下------
1 getattribute called, item is not_desc
instance not_desc

回想前面提到的屬性查詢順序,‘not_desc’在類的__dict__被找到了,但不是descriptor,所以執行第2步,在例項的__dict__中發現該屬性,返回。

t = T()
print(t.Not_Desc)
--------結果如下------
1 getattribute called, item is  Not_Desc
6 get called, instance is <__main__.T object at 0x0000017F70A006A0> ,owner is <class `__main__.T`>
# 這裡是NonDataDescriptor中的get方法
1 getattribute called, item is  a
23  # a的初始值為123 由get方法返回的是123-100=23

根據我們之前提到的順序,‘Not_Desc’屬性在類的__dict__中找到但不是data descriptor,又沒有在例項的dict中找到,所以進行到了4.1步驟,發現它是一個non-data descriptor,然後就執行了其中的get方法。

t = T()
print(t.bbbbbb)  # T類中沒有定義該屬性
------結果如下----------
1 getattribute called, item is bbbbbb
2 getattr called, item is bbbbbb
Traceback (most recent call last):
  File "C:/省略/.py", line 40, in <module>
    print(t.bbbbbb)
  File "C:C:/省略/.py.py", line 36, in __getattr__
    raise AttributeError(`NO such attr %s` % item)
AttributeError: NO such attr bbbbbb

可見,當我們訪問一個沒有被定義的屬性時,仍然會首先呼叫getattribute,根據屬性查詢原則,在例項和類中都沒有找到這個屬性,於是執行getattr。到這裡我們也就瞭解到了python中屬性的查詢順序、getattribute和getattr的執行順序。

回過頭來,getattribute會在呼叫類和例項的屬性時無條件呼叫,所以可以用於許可權鑑別、日誌記錄等操作;而getattr會在屬性沒有被找到的時候執行,因此可以用來做一些兜底的操作,可見這篇部落格

另外注意重寫getattribute時的迴圈陷阱,返回語句要寫成return object.__getattribute__(),不要使用return self.xxx,否則self相當於又指向了自己,而getattribute會無條件呼叫,從而進入無限迴圈。


最後的我們檢視類和例項的__dict__屬性,看看又什麼不同

t = T()
print(t.__dict__)
print(T.__dict__)
------結果如下--------
1 getattribute called, item is __dict__
{`a`: 123, `not_desc`: `instance not_desc`}
{`__module__`: `__main__`, `desc`: <__main__.Descriptor object at 0x0000021FD20C0128>, `Not_Desc`: <__main__.NotDescriptor object at 0x0000021FD20C0160>, `__init__`: <function T.__init__ at 0x0000021FD20B59D8>, `func`: <function T.func at 0x0000021FD20B5A60>, `__getattribute__`: <function T.__getattribute__ at 0x0000021FD20B5AE8>, `__getattr__`: <function T.__getattr__ at 0x0000021FD20B5B70>, `__dict__`: <attribute `__dict__` of `T` objects>, `__weakref__`: <attribute `__weakref__` of `T` objects>, `__doc__`: None}

可見例項t只有init中定義的屬性,而類T中才有描述器和非描述器等屬性。

參考:

https://blog.csdn.net/yitiaodashu/article/details/78974596

https://www.cnblogs.com/Vito2008/p/5280216.html

https://www.cnblogs.com/pyxiaomangshe/p/7927540.html

相關文章