在
Python開發(fā)中,對于一個對象的屬性訪問,我們一般采用的是點(.)
屬性運算符進(jìn)行操作。例如,有一個類實例對象 foo
,它有一個 name
屬性,那便可以使用 foo.name
對此屬性進(jìn)行訪問。一般而言,點(.)
屬性運算符比較直觀,也是我們經(jīng)常碰到的一種屬性訪問方式。然而,在點
(.)
屬性運算符的背后卻是別有洞天,值得我們對對象的屬性訪問進(jìn)行探討。
在進(jìn)行對象屬性訪問的分析之前,我們需要先了解一下對象怎么表示其屬性。為了便于說明,本文以新式類為例。有關(guān)新式類和舊式類的區(qū)別,大家可以查看Python
官方文檔。
對象的屬性
Python
中,
“
一切皆對象
”
。我們可以給對象設(shè)置各種屬性。先來看一個簡單的例子:
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
和兩個函數(shù)。接下來,我們實例化一個對象。對象的屬性可以從特殊屬性 __dict__
中查看。
#
實例化一個對象
dog>>> dog = Dog(1)#
查看
dog
對象的屬性
>>> dog.__dict__
{'age': 1}#
查看類
Dog
的屬性
>>> Dog.__dict__
dict_proxy({'__doc__': None,
'__init__':,
'__module__': '__main__',
'fly': False,
'sound':})#
查看類
Animal
的屬性
>>> Animal.__dict__
dict_proxy({'__dict__':,
'__doc__': None,
'__module__': '__main__',
'__weakref__':,
'run': True})
由上面的例子可以看出:屬性在哪個對象上定義,便會出現(xiàn)在哪個對象的 __dict__
中。例如:
·
類 Animal
定義了一個屬性 run
,那這個 run
屬性便只會出現(xiàn)在類 Animal
的 __dict__
中,而不會出現(xiàn)在其子類中。
·
類 Dog
定義了一個屬性 fly
和兩個函數(shù),那這些屬性和方法便會出現(xiàn)在類 Dog
的 __dict__
中,同時它們也不會出現(xiàn)在實例的__dict__
中。
·
實例對象 dog
的 __dict__
中只出現(xiàn)了一個屬性 age
,這是在初始化實例對象的時候添加的,它沒有父類的屬性和方法。
·
由此可知:
Python
中對象的屬性具有 “
層次性
”
,屬性在哪個對象上定義,便會出現(xiàn)在哪個對象的__dict__
中。
在這里我們首先了解的是屬性值會存儲在對象的 __dict__
中,查找也會在對象的 __dict__
中進(jìn)行查找的。至于Python
對象進(jìn)行屬性訪問時,會按照怎樣的規(guī)則來查找屬性值呢?這個問題在后文中進(jìn)行討論。
對象屬性訪問與特殊方法 __getattribute__
正如前面所述,Python
的屬性訪問方式很直觀,使用點屬性運算符。在新式類中,對對象屬性的訪問,都會調(diào)用特殊方法__getattribute__
。 __getattribute__
允許我們在訪問對象屬性時自定義訪問行為,但是使用它特別要小心無限遞歸的問題。
還是以上面的情景為例:
class Animal(object):
run = True
class Dog(Animal):
fly = False
def __init__(self, age):
self.age = age
#
重寫
__getattribute__
。需要注意的是重寫的方法中不能
#
使用對象的點運算符訪問屬性,否則使用點運算符訪問屬性時,
#
會再次調(diào)用
__getattribute__
。這樣就會陷入無限遞歸。
#
可以使用
super()
方法避免這個問題。
def __getattribute__(self, key):
print "calling __getattribute__\n"
return super(Dog, self).__getattribute__(key)
def sound(self):
return "wang wang~"
上面的例子中我們重寫了 __getattribute__
方法。注意我們使用了 super()
方法來避免無限循環(huán)問題。下面我們實例化一個對象來說明訪問對象屬性時 __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__
是實例對象查找屬性或方法的入口
。實例對象訪問屬性或方法時都需要調(diào)用到__getattribute__
,之后才會根據(jù)一定的規(guī)則在各個 __dict__
中查找相應(yīng)的屬性值或方法對象,若沒有找到則會調(diào)用__getattr__
(后面會介紹到)。 __getattribute__
是Python
中的一個內(nèi)置方法,關(guān)于其底層實現(xiàn)可以查看相關(guān)官方文檔,后面將要介紹的屬性訪問規(guī)則就是依賴于 __getattribute__
的。
對象屬性控制
在繼續(xù)介紹后面相關(guān)內(nèi)容之前,讓我們先來了解一下Python
中和對象屬性控制相關(guān)的相關(guān)方法。
__getattr__(self, name)
__getattr__
可以用來在當(dāng)用戶試圖訪問一個根本不存在(或者暫時不存在)的屬性時,來定義類的行為。前面講到過,當(dāng)__getattribute__
方法找不到屬性時,最終會調(diào)用 __getattr__
方法。它可以用于捕捉錯誤的以及靈活地處理AttributeError
。只有當(dāng)試圖訪問不存在的屬性時它才會被調(diào)用。
__setattr__(self, name, value)
__setattr__
方法允許你自定義某個屬性的賦值行為,不管這個屬性存在與否,都可以對任意屬性的任何變化都定義自己的規(guī)則。關(guān)于 __setattr__
有兩點需要說明:第一,使用它時必須小心,不能寫成類似 self.name = "Tom"
這樣的形式,因為這樣的賦值語句會調(diào)用 __setattr__
方法,這樣會讓其陷入無限遞歸;第二,你必須區(qū)分
對象屬性
和
類屬性
這兩個概念。后面的例子中會對此進(jìn)行解釋。
__delattr__(self, name)
__delattr__
用于處理刪除屬性時的行為。和 __setattr__
方法要注意無限遞歸的問題,重寫該方法時不要有類似 del self.name
的寫法。
還是以上面的例子進(jìn)行說明,不過在這里我們要重寫三個屬性控制方法。
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)
以下進(jìn)行驗證。首先是 __getattr__ :
#
創(chuàng)建實例對象
dog>>> dog = Dog(1)
calling __setattr__#
檢查一下
dog
和
Dog
的
__dict__>>> dog.__dict__
{'age': 1}>>> Dog.__dict__
dict_proxy({'__delattr__':,
'__doc__': None,
'__getattr__':,
'__init__':,
'__module__': '__main__',
'__setattr__':,
'fly': False})
#
獲取
dog
的
age
屬性
>>> dog.age1#
獲取
dog
的
adult
屬性。
#
由于
__getattribute__
沒有找到相應(yīng)的屬性,所以調(diào)用
__getattr__
。
>>> dog.adult
calling __getattr__
False
#
調(diào)用一個不存在的屬性
name
,
__getattr__
捕獲
AttributeError
錯誤
>>> dog.name
calling __getattr__
Traceback (most recent call last):
File "", line 1, in <module>
File "", line 10, in __getattr__
AttributeError
可以看到,屬性訪問時,當(dāng)訪問一個不存在的屬性時觸發(fā) __getattr__
,它會對訪問行為進(jìn)行控制。接下來是 __setattr__
:
#
給
dog.age
賦值,會調(diào)用
__setattr__
方法
>>> dog.age = 2
calling __setattr__>>> dog.age2
#
先調(diào)用
dog.fly
時會返回
False
,這時因為
Dog
類屬性中有
fly
屬性;
#
之后再給
dog.fly
賦值,觸發(fā)
__setattr__
方法。
>>> dog.fly
False>>> dog.fly = True
calling __setattr__
#
再次查看
dog.fly
的值以及
dog
和
Dog
的
__dict__;#
可以看出對
dog
對象進(jìn)行賦值,會在
dog
對象的
__dict__
中添加了一條對象屬性;
#
然而,
Dog
類屬性沒有發(fā)生變化
#
注意:
dog
對象和
Dog
類中都有
fly
屬性,訪問時會選擇哪個呢?
>>> dog.fly
True>>> dog.__dict__
{'age': 2, 'fly': True}>>> Dog.__dict__
dict_proxy({'__delattr__':,
'__doc__': None,
'__getattr__':,
'__init__':,
'__module__': '__main__',
'__setattr__':,
'fly': False})
實例對象的 __setattr__
方法可以定義屬性的賦值行為,不管屬性是否存在。當(dāng)屬性存在時,它會改變其值;當(dāng)屬性不存在時,它會添加一個對象屬性信息到對象的 __dict__
中,然而這并不改變類的屬性。從上面的例子可以看出來。
最后,看一下 __delattr__
:
#
由于上面的例子中我們?yōu)?/span>
dog
設(shè)置了
fly
屬性,現(xiàn)在刪除它觸發(fā)
__delattr__
方法
>>> del dog.fly
calling __delattr__#
再次查看
dog
對象的
__dict__
,發(fā)現(xiàn)和
fly
屬性相關(guān)的信息被刪除
>>> dog.__dict__
{'age': 2}
描述符
描述符是Python 2.2
版本中引進(jìn)來的新概念。描述符一般用于實現(xiàn)對象系統(tǒng)的底層功能, 包括綁定和非綁定方法、類方法、靜態(tài)方法特特性等。關(guān)于描述符的概念,官方并沒有明確的定義,可以在網(wǎng)上查閱相關(guān)資料。這里我從自己的認(rèn)識談一些想法,如有不當(dāng)之處還請包涵。
在前面我們了解了對象屬性訪問和行為控制的一些特殊方法,例如 __getattribute__
、 __getattr__
、__setattr__
、__delattr__
。以我的理解來看,這些方法應(yīng)當(dāng)具有屬性的"
普適性
"
,可以用于屬性查找、設(shè)置、刪除的一般方法,也就是說所有的屬性都可以使用這些方法實現(xiàn)屬性的查找、設(shè)置、刪除等操作。但是,這并不能很好地實現(xiàn)對某個具體屬性的訪問控制行為。例如,上例中假如要實現(xiàn) dog.age
屬性的類型設(shè)置(只能是整數(shù)),如果單單去修改 __setattr__
方法滿足它,那這個方法便有可能不能支持其他的屬性設(shè)置。
在類中設(shè)置屬性的控制行為不能很好地解決問題,Python
給出的方案是: __getattribute__
、 __getattr__
、 __setattr__
、 __delattr__
等方法用來實現(xiàn)屬性查找、設(shè)置、刪除的一般邏輯,而對屬性的控制行為就由屬性對象來控制。這里單獨抽離出來一個屬性對象,在屬性對象中定義這個屬性的查找、設(shè)置、刪除行為。這個屬性對象就是描述符。
描述符對象一般是作為其他類對象的屬性而存在。在其內(nèi)部定義了三個方法用來實現(xiàn)屬性對象的查找、設(shè)置、刪除行為。這三個方法分別是:
· get (self, instance, owner)
:定義當(dāng)試圖取出描述符的值時的行為。
· set (self, instance, value)
:定義當(dāng)描述符的值改變時的行為。
· delete (self, instance)
:定義當(dāng)描述符的值被刪除時的行為。
其中:instance
為把描述符對象作為屬性的對象實例;
owner
為
instance
的類對象。
以下以官方的一個例子進(jìn)行說明:
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
等語句,這些語句會調(diào)用 __getattribute__
、 __setattr__
等方法,這也說明了__getattribute__
、__setattr__
等方法在控制訪問對象屬性上的一般性(一般性是指對于所有屬性它們的控制行為一致),以及 __get__
、 __set__
等方法在控制訪問對象屬性上的特殊性(特殊性是指它針對某個特定屬性可以定義不同的行為)。
以下進(jìn)行驗證:
#
創(chuàng)建
Myclass
類的實例
m>>> m = MyClass()
#
查看
m
和
MyClass
的
__dict__>>> m.__dict__
{}>>> MyClass.__dict__
dict_proxy({'__dict__':,
'__doc__': None,
'__module__': '__main__',
'__weakref__':,
'x':<__main__.revealaccess at="" 0x5130080="">,
'y': 5})
#
訪問
m.x
。會先觸發(fā)
__getattribute__
方法
#
由于
x
屬性的值是一個描述符,會觸發(fā)它的
__get__
方法
>>> m.x
Retrieving var "x"10
#
設(shè)置
m.x
的值。對描述符進(jìn)行賦值,會觸發(fā)它的
__set__
方法
#
在
__set__
方法中還會觸發(fā)
__setattr__
方法(
self.val = val
)
>>> m.x = 20
Updating var "x"
#
再次訪問
m.x>>> m.x
Retrieving var "x"20
#
查看
m
和
MyClass
的
__dict__
,發(fā)現(xiàn)這與對描述符賦值之前一樣。
#
這一點與一般屬性的賦值不同,可參考上述的
__setattr__
方法。
#
之所以前后沒有發(fā)生變化,是因為變化體現(xiàn)在描述符對象上,
#
而不是實例對象
m
和類
MyClass
上。
>>> m.__dict__
{}>>> MyClass.__dict__
dict_proxy({'__dict__':,
'__doc__': None,
'__module__': '__main__',
'__weakref__':,
'x':<__main__.revealaccess at="" 0x5130080="">,
'y': 5})
上面的例子對描述符進(jìn)行了一定的解釋,不過對描述符還需要更進(jìn)一步的探討和分析,這個工作先留待以后繼續(xù)進(jìn)行。
最后,還需要注意一點:描述符有數(shù)據(jù)描述符和非數(shù)據(jù)描述符之分。
·
只要至少實現(xiàn) __get__
、 __set__
、 __delete__
方法中的一個就可以認(rèn)為是描述符;
·
只實現(xiàn) __get__
方法的對象是非數(shù)據(jù)描述符,意味著在初始化之后它們只能被讀取;
·
同時實現(xiàn) __get__
和 __set__
的對象是數(shù)據(jù)描述符,意味著這種屬性是可讀寫的。
屬性訪問的優(yōu)先規(guī)則
在以上的討論中,我們一直回避著一個問題,那就是屬性訪問時的優(yōu)先規(guī)則。我們了解到,屬性一般都在__dict__
中存儲,但是在訪問屬性時,在對象屬性、類屬型、基類屬性中以怎樣的規(guī)則來查詢屬性呢?以下對Python
中屬性訪問的規(guī)則進(jìn)行分析。
由上述的分析可知,屬性訪問的入口點是 __getattribute__
方法。它的實現(xiàn)中定義了Python
中屬性訪問的優(yōu)先規(guī)則。
Python
官方文檔中對 __getattribute__
的底層實現(xiàn)有相關(guān)的介紹,本文暫時只是討論屬性查找的規(guī)則,相關(guān)規(guī)則可見下圖:
Python
屬性查找
上圖是查找 b.x
這樣一個屬性的過程。在這里要對此圖進(jìn)行簡單的介紹:
1.
查找屬性的第一步是搜索基類列表,即 type(b).__mro__
,直到找到該屬性的第一個定義,并將該屬性的值賦值給 descr
;
2.
判斷 descr
的類型。它的類型可分為數(shù)據(jù)描述符、非數(shù)據(jù)描述符、普通屬性、未找到等類型。若 descr
為數(shù)據(jù)描述符,則調(diào)用 desc.__get__(b, type(b))
,并將結(jié)果返回,結(jié)束執(zhí)行。否則進(jìn)行下一步;
3.
如果 descr
為非數(shù)據(jù)描述符、普通屬性、未找到等類型,則查找實例b
的實例屬性,即 b.__dict__
。如果找到,則將結(jié)果返回,結(jié)束執(zhí)行。否則進(jìn)行下一步;
4.
如果在 b.__dict__
未找到相關(guān)屬性,則重新回到 descr
值的判斷上。
·
若 descr
為非數(shù)據(jù)描述符,則調(diào)用 desc.__get__(b, type(b))
,并將結(jié)果返回,結(jié)束執(zhí)行;
·
若 descr
為普通屬性,直接返回結(jié)果并結(jié)束執(zhí)行;
·
若 descr
為空(未找到),則最終拋出 AttributeError
異常,結(jié)束查找。
來源:
稀土掘金