在Python中,對於一個物件的屬性訪問,我們一般採用的是點(.)屬性運算子進行操作。例如,有一個類例項物件foo,它有一個name屬性,那便可以使用foo.name對此屬性進行訪問。一般而言,點(.)屬性運算子比較直觀,也是我們經常碰到的一種屬性訪問方式。然而,在點(.)屬性運算子的背後卻是別有洞天,值得我們對物件的屬性訪問進行探討。
在進行物件屬性訪問的分析之前,我們需要先了解一下物件怎麼表示其屬性。為了便於說明,本文以新式類為例。有關新式類和舊式類的區別,大家可以檢視Python官方文件。
物件的屬性
Python中,“一切皆物件”。我們可以給物件設定各種屬性。先來看一個簡單的例子:
1 2 3 4 5 6 7 8 |
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__中檢視。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 例項化一個物件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__允許我們在訪問物件屬性時自定義訪問行為,但是使用它特別要小心無限遞迴的問題。
還是以上面的情景為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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__的特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# 例項化物件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的寫法。
還是以上面的例子進行說明,不過在這裡我們要重寫三個屬性控制方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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__:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# 建立例項物件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__:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# 給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__:
1 2 3 4 5 6 |
# 由於上面的例子中我們為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的類物件。
以下以官方的一個例子進行說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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__等方法在控制訪問物件屬性上的特殊性(特殊性是指它針對某個特定屬性可以定義不同的行為)。
以下進行驗證:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# 建立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__的底層實現有相關的介紹,本文暫時只是討論屬性查詢的規則,相關規則可見下圖:
上圖是查詢b.x這樣一個屬性的過程。在這裡要對此圖進行簡單的介紹:
- 查詢屬性的第一步是搜尋基類列表,即type(b).__mro__,直到找到該屬性的第一個定義,並將該屬性的值賦值給descr;
- 判斷descr的型別。它的型別可分為資料描述符、非資料描述符、普通屬性、未找到等型別。若descr為資料描述符,則呼叫desc.__get__(b, type(b)),並將結果返回,結束執行。否則進行下一步;
- 如果descr為非資料描述符、普通屬性、未找到等型別,則查詢例項b的例項屬性,即b.__dict__。如果找到,則將結果返回,結束執行。否則進行下一步;
- 如果在b.__dict__未找到相關屬性,則重新回到descr值的判斷上。
- 若descr為非資料描述符,則呼叫desc.__get__(b, type(b)),並將結果返回,結束執行;
- 若descr為普通屬性,直接返回結果並結束執行;
- 若descr為空(未找到),則最終丟擲 AttributeError 異常,結束查詢。