Python 中幾種屬性訪問的區別

語言: CN / TW / HK

起步
python的提供一系列和屬性訪問有關的特殊方法:__get__, __getattr__, __getattribute__, __getitem__。本文闡述它們的區別和用法。

屬性的訪問機制
一般情況下,屬性訪問的預設行為是從物件的字典中獲取,並當獲取不到時會沿著一定的查詢鏈進行查詢。例如 a.x 的查詢鏈就是,從 a.__dict__['x'] ,然後是 type(a).__dict__['x'] ,再通過 type(a) 的基類開始查詢。

若查詢鏈都獲取不到屬性,則丟擲 AttributeError 異常。

一、__getattr__ 方法
這個方法是當物件的屬性不存在是呼叫。如果通過正常的機制能找到物件屬性的話,不會呼叫 getattr 方法。

class A:
    a = 1
    def __getattr__(self, item):
        print('__getattr__ call')
        return item

t = A()
print(t.a)
print(t.b)
# output
1
__getattr__ call
b

二、__getattribute__ 方法
這個方法會被無條件呼叫。不管屬性存不存在。如果類中還定義了 getattr ,則不會呼叫 getattr__()方法,除非在 __getattribute 方法中顯示呼叫__getattr__() 或者丟擲了 AttributeError 。

class A:
    a = 1
    def __getattribute__(self, item):
        print('__getattribute__ call')
        raise AttributeError

    def __getattr__(self, item):
        print('__getattr__ call')
        return item

t = A()
print(t.a)
print(t.b)

所以一般情況下,為了保留 getattr 的作用,__getattribute__() 方法中一般返回父類的同名方法:

def __getattribute__(self, item):
    return object.__getattribute__(self, item)

使用基類的方法來獲取屬效能避免在方法中出現無限遞迴的情況。

三、__get__ 方法
這個方法比較簡單說明,它與前面的關係不大。

如果一個類中定義了 __get__(), __set__() 或 __delete__() 中的任何方法。則這個類的物件稱為描述符。

class Descri(object):
    def __get__(self, obj, type=None):
        print("call get")

    def __set__(self, obj, value):
        print("call set")

class A(object):
    x = Descri()

a = A()
a.__dict__['x'] = 1  # 不會呼叫 __get__
a.x                  # 呼叫 __get__

如果查詢的屬性是在描述符物件中,則這個描述符會覆蓋上文說的屬性訪問機制,體現在查詢鏈的不同,而這個行文也會因為呼叫的不同而稍有不一樣:

  • 如果呼叫是物件例項(題目中的呼叫方式),a.x 則轉換為呼叫:。type(a).__dict__['x'].__get__(a, type(a))
  • 如果呼叫的是類屬性, A.x 則轉換為:A.__dict__['x'].__get__(None, A)
  • 其他情況見文末參考資料的文件

四、__getitem__ 方法

這個呼叫也屬於無條件呼叫,這點與 getattribute 一致。區別在於 getitem 讓類例項允許 [] 運算,可以這樣理解:

  • __getattribute__適用於所有.運算子;
  • __getitem__適用於所有 [] 運算子。

    class A(object): a = 1

    def __getitem__(self, item):
        print('__getitem__ call')
        return item
    

    t = A() print(t['a']) print(t['b'])

如果僅僅想要物件能夠通過 [] 獲取物件屬性可以簡單的:

def __getitem(self, item):
    return object.__getattribute__(self, item)

總結
當這幾個方法同時出現可能就會擾亂你了。我在網上看到一份示例還不錯,稍微改了下:

class C(object):
    a = 'abc'

    def __getattribute__(self, *args, **kwargs):
        print("__getattribute__() is called")
        return object.__getattribute__(self, *args, **kwargs)

    #        return "haha"
    def __getattr__(self, name):
        print("__getattr__() is called ")
        return name + " from getattr"

    def __get__(self, instance, owner):
        print("__get__() is called", instance, owner)
        return self

    def __getitem__(self, item):
        print('__getitem__ call')
        return object.__getattribute__(self, item)

    def foo(self, x):
        print(x)

class C2(object):
    d = C()

if __name__ == '__main__':
    c = C()
    c2 = C2()
    print(c.a)
    print(c.zzzzzzzz)
    c2.d
    print(c2.d.a)
    print(c['a'])

可以結合輸出慢慢理解,這裡還沒涉及繼承關係呢。總之,每個以 __get 為字首的方法都是獲取物件內部資料的鉤子,名稱不一樣,用途也存在較大的差異,只有在實踐中理解它們,才能真正掌握它們的用法。

以上就是本次分享的所有內容,想要了解更多 python 知識歡迎前往公眾號:Python 程式設計學習圈 ,傳送 “J” 即可免費獲取,每日干貨分享