Python 中的屬性訪問與描述符

phcs8142 7年前發布 | 15K 次閱讀 Python Python開發

在Python中,對于一個對象的屬性訪問,我們一般采用的是點(.)屬性運算符進行操作。例如,有一個類實例對象 foo ,它有一個 name 屬性,那便可以使用 foo.name 對此屬性進行訪問。一般而言,點(.)屬性運算符比較直觀,也是我們經常碰到的一種屬性訪問方式。然而,在點(.)屬性運算符的背后卻是別有洞天,值得我們對對象的屬性訪問進行探討。

在進行對象屬性訪問的分析之前,我們需要先了解一下對象怎么表示其屬性。為了便于說明,本文以新式類為例。有關新式類和舊式類的區別,大家可以查看Python官方文檔。

對象的屬性

Python中,“一切皆對象”。我們可以給對象設置各種屬性。先來看一個簡單的例子:

class Animal(object):
    run = True

class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def sound(self):
        return "wang wang~"

上面的例子中,我們定義了兩個類。類 Animal 定義了一個屬性 run ;類 Dog 繼承自 Animal ,定義了一個屬性 fly 和兩個函數。接下來,我們實例化一個對象。對象的屬性可以從特殊屬性 __dict__ 中查看。

# 實例化一個對象dog
>>> dog = Dog(1)
# 查看dog對象的屬性
>>> dog.__dict__
{'age': 1}
# 查看類Dog的屬性
>>> Dog.__dict__
dict_proxy({'__doc__': None,
            '__init__': <function __main__.__init__>,
            '__module__': '__main__',
            'fly': False,
            'sound': <function __main__.sound>})
# 查看類Animal的屬性
>>> Animal.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Animal' objects>,
            '__doc__': None,
            '__module__': '__main__',
            '__weakref__': <attribute '__weakref__' of 'Animal' objects>,
            'run': True})

由上面的例子可以看出:屬性在哪個對象上定義,便會出現在哪個對象的 __dict__ 中。例如:

  • 類 Animal 定義了一個屬性 run ,那這個 run 屬性便只會出現在類 Animal 的 __dict__ 中,而不會出現在其子類中。
  • 類 Dog 定義了一個屬性 fly 和兩個函數,那這些屬性和方法便會出現在類 Dog 的 __dict__ 中,同時它們也不會出現在實例的 __dict__ 中。
  • 實例對象 dog 的 __dict__ 中只出現了一個屬性 age ,這是在初始化實例對象的時候添加的,它沒有父類的屬性和方法。
  • 由此可知:Python中對象的屬性具有 “層次性” ,屬性在哪個對象上定義,便會出現在哪個對象的 __dict__ 中。

在這里我們首先了解的是屬性值會存儲在對象的 __dict__ 中,查找也會在對象的 __dict__ 中進行查找的。至于Python對象進行屬性訪問時,會按照怎樣的規則來查找屬性值呢?這個問題在后文中進行討論。

對象屬性訪問與特殊方法 __getattribute__

正如前面所述,Python的屬性訪問方式很直觀,使用點屬性運算符。在新式類中,對對象屬性的訪問,都會調用特殊方法 __getattribute__ 。 __getattribute__ 允許我們在訪問對象屬性時自定義訪問行為,但是使用它特別要小心無限遞歸的問題。

還是以上面的情景為例:

class Animal(object):
    run = True

class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    # 重寫__getattribute__。需要注意的是重寫的方法中不能
    # 使用對象的點運算符訪問屬性,否則使用點運算符訪問屬性時,
    # 會再次調用__getattribute__。這樣就會陷入無限遞歸。
    # 可以使用super()方法避免這個問題。
    def __getattribute__(self, key):
        print  "calling __getattribute__\n"
        return super(Dog, self).__getattribute__(key)
    def sound(self):
        return "wang wang~"

上面的例子中我們重寫了 __getattribute__ 方法。注意我們使用了 super() 方法來避免無限循環問題。下面我們實例化一個對象來說明訪問對象屬性時 __getattribute__ 的特性。

# 實例化對象dog
>>> dog = Dog(1)
# 訪問dog對象的age屬性

>>> dog.age
calling __getattribute__
1

# 訪問dog對象的fly屬性
>>> dog.fly
calling __getattribute__
False

# 訪問dog對象的run屬性
>>> dog.run
calling __getattribute__
True

# 訪問dog對象的sound方法
>>> dog.sound
calling __getattribute__
<bound method Dog.sound of <__main__.Dog object at 0x0000000005A90668>>

由上面的驗證可知, __getattribute__ 是實例對象查找屬性或方法的入口 。實例對象訪問屬性或方法時都需要調用到 __getattribute__ ,之后才會根據一定的規則在各個 __dict__ 中查找相應的屬性值或方法對象,若沒有找到則會調用 __getattr__ (后面會介紹到)。 __getattribute__ 是Python中的一個內置方法,關于其底層實現可以查看相關官方文檔,后面將要介紹的屬性訪問規則就是依賴于 __getattribute__ 的。

對象屬性控制

在繼續介紹后面相關內容之前,讓我們先來了解一下Python中和對象屬性控制相關的相關方法。

  • __getattr__(self, name)

    __getattr__ 可以用來在當用戶試圖訪問一個根本不存在(或者暫時不存在)的屬性時,來定義類的行為。前面講到過,當 __getattribute__ 方法找不到屬性時,最終會調用 __getattr__ 方法。它可以用于捕捉錯誤的以及靈活地處理AttributeError。只有當試圖訪問不存在的屬性時它才會被調用。

  • __setattr__(self, name, value)

    __setattr__ 方法允許你自定義某個屬性的賦值行為,不管這個屬性存在與否,都可以對任意屬性的任何變化都定義自己的規則。關于 __setattr__ 有兩點需要說明:第一,使用它時必須小心,不能寫成類似 self.name = "Tom" 這樣的形式,因為這樣的賦值語句會調用 __setattr__ 方法,這樣會讓其陷入無限遞歸;第二,你必須區分 對象屬性類屬性 這兩個概念。后面的例子中會對此進行解釋。

  • __delattr__(self, name)

    __delattr__ 用于處理刪除屬性時的行為。和 __setattr__ 方法要注意無限遞歸的問題,重寫該方法時不要有類似 del self.name 的寫法。

還是以上面的例子進行說明,不過在這里我們要重寫三個屬性控制方法。

class Animal(object):
    run = True

class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def __getattr__(self, name):
        print "calling __getattr__\n"
        if name == 'adult':
            return True if self.age >= 2 else False
        else:
            raise AttributeError
    def __setattr__(self, name, value):
        print "calling __setattr__"
        super(Dog, self).__setattr__(name, value)
    def __delattr__(self, name):
        print "calling __delattr__"
        super(Dog, self).__delattr__(name)

以下進行驗證。首先是 __getattr__ :

# 創建實例對象dog
>>> dog = Dog(1)
calling __setattr__
# 檢查一下dog和Dog的__dict__
>>> dog.__dict__
{'age': 1}
>>> Dog.__dict__
dict_proxy({'__delattr__': <function __main__.__delattr__>,
            '__doc__': None,
            '__getattr__': <function __main__.__getattr__>,
            '__init__': <function __main__.__init__>,
            '__module__': '__main__',
            '__setattr__': <function __main__.__setattr__>,
            'fly': False})

# 獲取dog的age屬性
>>> dog.age
1
# 獲取dog的adult屬性。
# 由于__getattribute__沒有找到相應的屬性,所以調用__getattr__。
>>> dog.adult
calling __getattr__
False

# 調用一個不存在的屬性name,__getattr__捕獲AttributeError錯誤
>>> dog.name
calling __getattr__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in __getattr__
AttributeError

可以看到,屬性訪問時,當訪問一個不存在的屬性時觸發 __getattr__ ,它會對訪問行為進行控制。接下來是 __setattr__ :

# 給dog.age賦值,會調用__setattr__方法
>>> dog.age = 2
calling __setattr__
>>> dog.age
2

# 先調用dog.fly時會返回False,這時因為Dog類屬性中有fly屬性;
# 之后再給dog.fly賦值,觸發__setattr__方法。
>>> dog.fly
False
>>> dog.fly = True
calling __setattr__

# 再次查看dog.fly的值以及dog和Dog的__dict__;
# 可以看出對dog對象進行賦值,會在dog對象的__dict__中添加了一條對象屬性;
# 然而,Dog類屬性沒有發生變化
# 注意:dog對象和Dog類中都有fly屬性,訪問時會選擇哪個呢?
>>> dog.fly
True
>>> dog.__dict__
{'age': 2, 'fly': True}
>>> Dog.__dict__
dict_proxy({'__delattr__': <function __main__.__delattr__>,
            '__doc__': None,
            '__getattr__': <function __main__.__getattr__>,
            '__init__': <function __main__.__init__>,
            '__module__': '__main__',
            '__setattr__': <function __main__.__setattr__>,
            'fly': False})

實例對象的 __setattr__ 方法可以定義屬性的賦值行為,不管屬性是否存在。當屬性存在時,它會改變其值;當屬性不存在時,它會添加一個對象屬性信息到對象的 __dict__ 中,然而這并不改變類的屬性。從上面的例子可以看出來。

最后,看一下 __delattr__ :

# 由于上面的例子中我們為dog設置了fly屬性,現在刪除它觸發__delattr__方法
>>> del dog.fly
calling __delattr__
# 再次查看dog對象的__dict__,發現和fly屬性相關的信息被刪除
>>> dog.__dict__
{'age': 2}

描述符

描述符是Python 2.2 版本中引進來的新概念。描述符一般用于實現對象系統的底層功能, 包括綁定和非綁定方法、類方法、靜態方法特特性等。關于描述符的概念,官方并沒有明確的定義,可以在網上查閱相關資料。這里我從自己的認識談一些想法,如有不當之處還請包涵。

在前面我們了解了對象屬性訪問和行為控制的一些特殊方法,例如 __getattribute__ 、 __getattr__ 、 __setattr__ 、 __delattr__ 。以我的理解來看,這些方法應當具有屬性的"普適性",可以用于屬性查找、設置、刪除的一般方法,也就是說所有的屬性都可以使用這些方法實現屬性的查找、設置、刪除等操作。但是,這并不能很好地實現對某個具體屬性的訪問控制行為。例如,上例中假如要實現 dog.age 屬性的類型設置(只能是整數),如果單單去修改 __setattr__ 方法滿足它,那這個方法便有可能不能支持其他的屬性設置。

在類中設置屬性的控制行為不能很好地解決問題,Python給出的方案是: __getattribute__ 、 __getattr__ 、 __setattr__ 、 __delattr__ 等方法用來實現屬性查找、設置、刪除的一般邏輯,而對屬性的控制行為就由屬性對象來控制。這里單獨抽離出來一個屬性對象,在屬性對象中定義這個屬性的查找、設置、刪除行為。這個屬性對象就是描述符。

描述符對象一般是作為其他類對象的屬性而存在。在其內部定義了三個方法用來實現屬性對象的查找、設置、刪除行為。這三個方法分別是:

  • get (self, instance, owner):定義當試圖取出描述符的值時的行為。
  • set (self, instance, value):定義當描述符的值改變時的行為。
  • delete (self, instance):定義當描述符的值被刪除時的行為。

其中:instance為把描述符對象作為屬性的對象實例;

owner為instance的類對象。

以下以官方的一個例子進行說明:

class RevealAccess(object):

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print 'Retrieving', self.name
        return self.val

    def __set__(self, obj, val):
        print 'Updating', self.name
        self.val = val

class MyClass(object):
    x = RevealAccess(10, 'var "x"')
    y = 5

以上定義了兩個類。其中 RevealAccess 類的實例是作為 MyClass 類屬性 x 的值存在的。而且 RevealAccess 類定義了 __get__ 、 __set__ 方法,它是一個描述符對象。注意,描述符對象的 __get__ 、 __set__ 方法中使用了諸如 self.val 和 self.val = val 等語句,這些語句會調用 __getattribute__ 、 __setattr__ 等方法,這也說明了 __getattribute__ 、 __setattr__ 等方法在控制訪問對象屬性上的一般性(一般性是指對于所有屬性它們的控制行為一致),以及 __get__ 、 __set__ 等方法在控制訪問對象屬性上的特殊性(特殊性是指它針對某個特定屬性可以定義不同的行為)。

以下進行驗證:

# 創建Myclass類的實例m
>>> m = MyClass()

# 查看m和MyClass的__dict__
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
            '__doc__': None,
            '__module__': '__main__',
            '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
            'x': <__main__.RevealAccess at 0x5130080>,
            'y': 5})

# 訪問m.x。會先觸發__getattribute__方法
# 由于x屬性的值是一個描述符,會觸發它的__get__方法
>>> m.x
Retrieving var "x"
10

# 設置m.x的值。對描述符進行賦值,會觸發它的__set__方法
# 在__set__方法中還會觸發__setattr__方法(self.val = val)
>>> m.x = 20
Updating var "x"

# 再次訪問m.x
>>> m.x
Retrieving var "x"
20

# 查看m和MyClass的__dict__,發現這與對描述符賦值之前一樣。
# 這一點與一般屬性的賦值不同,可參考上述的__setattr__方法。
# 之所以前后沒有發生變化,是因為變化體現在描述符對象上,
# 而不是實例對象m和類MyClass上。
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
            '__doc__': None,
            '__module__': '__main__',
            '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
            'x': <__main__.RevealAccess at 0x5130080>,
            'y': 5})

上面的例子對描述符進行了一定的解釋,不過對描述符還需要更進一步的探討和分析,這個工作先留待以后繼續進行。

最后,還需要注意一點:描述符有數據描述符和非數據描述符之分。

  • 只要至少實現 __get__ 、 __set__ 、 __delete__ 方法中的一個就可以認為是描述符;
  • 只實現 __get__ 方法的對象是非數據描述符,意味著在初始化之后它們只能被讀取;
  • 同時實現 __get__ 和 __set__ 的對象是數據描述符,意味著這種屬性是可讀寫的。

屬性訪問的優先規則

在以上的討論中,我們一直回避著一個問題,那就是屬性訪問時的優先規則。我們了解到,屬性一般都在 __dict__ 中存儲,但是在訪問屬性時,在對象屬性、類屬型、基類屬性中以怎樣的規則來查詢屬性呢?以下對Python中屬性訪問的規則進行分析。

由上述的分析可知,屬性訪問的入口點是 __getattribute__ 方法。它的實現中定義了Python中屬性訪問的優先規則。Python官方文檔中對 __getattribute__ 的底層實現有相關的介紹,本文暫時只是討論屬性查找的規則,相關規則可見下圖:

Python屬性查找

上圖是查找 b.x 這樣一個屬性的過程。在這里要對此圖進行簡單的介紹:

  1. 查找屬性的第一步是搜索基類列表,即 type(b).__mro__ ,直到找到該屬性的第一個定義,并將該屬性的值賦值給 descr ;
  2. 判斷 descr 的類型。它的類型可分為數據描述符、非數據描述符、普通屬性、未找到等類型。若 descr 為數據描述符,則調用 desc.__get__(b, type(b)) ,并將結果返回,結束執行。否則進行下一步;
  3. 如果 descr 為非數據描述符、普通屬性、未找到等類型,則查找實例b的實例屬性,即 b.__dict__ 。如果找到,則將結果返回,結束執行。否則進行下一步;
  4. 如果在 b.__dict__ 未找到相關屬性,則重新回到 descr 值的判斷上。
    • 若 descr 為非數據描述符,則調用 desc.__get__(b, type(b)) ,并將結果返回,結束執行;
    • 若 descr 為普通屬性,直接返回結果并結束執行;
    • 若 descr 為空(未找到),則最終拋出 AttributeError 異常,結束查找。

 

 

來自:https://juejin.im/post/58ff7151da2f60005dd86f2d

 

 本文由用戶 phcs8142 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!