1.1. 摘要
定義描述器, 總結(jié)描述器協(xié)議,并展示描述器是怎么被調(diào)用的臼氨。展示一個(gè)自定義的描述器和包括函數(shù),屬性(property), 靜態(tài)方法(static method), 類方法在內(nèi)的幾個(gè)Python內(nèi)置描述器。通過(guò)給出一個(gè)純Python的實(shí)現(xiàn)和示例應(yīng)用來(lái)展示每個(gè)描述器是怎么工作的。
學(xué)習(xí)描述器不僅讓你接觸到更多的工具镜悉,還可以讓你更深入地了解Python,讓你體會(huì)到Python設(shè)計(jì)的優(yōu)雅之處医瘫。
1.2. 定義和介紹
一般來(lái)說(shuō)侣肄,一個(gè)描述器是一個(gè)有“綁定行為”的對(duì)象屬性(object attribute),它的訪問(wèn)控制被描述器協(xié)議方法重寫醇份。這些方法是 __get__()
, __set__()
, 和__delete__()
稼锅。有這些方法的對(duì)象叫做描述器吼具。
默認(rèn)對(duì)屬性的訪問(wèn)控制是從對(duì)象的字典里面(__dict__
)中獲取(get), 設(shè)置(set)和刪除(delete)它。舉例來(lái)說(shuō)矩距, a.x的查找順序是, a.__dict__['x']
, 然后type(a).__dict__['x']
, 然后找 type(a)
的父類(不包括元類(metaclass)).如果查找到的值是一個(gè)描述器, Python就會(huì)調(diào)用描述器的方法來(lái)重寫默認(rèn)的控制行為拗盒。這個(gè)重寫發(fā)生在這個(gè)查找環(huán)節(jié)的哪里取決于定義了哪個(gè)描述器方法。注意, 只有在新式類中時(shí)描述器才會(huì)起作用锥债。(新式類是繼承自 type或者 object的類)
描述器是強(qiáng)大的陡蝇,應(yīng)用廣泛的。描述器正是屬性, 實(shí)例方法, 靜態(tài)方法, 類方法和super
的背后的實(shí)現(xiàn)機(jī)制哮肚。描述器在Python自身中廣泛使用登夫,以實(shí)現(xiàn)Python 2.2中引入的新式類。描述器簡(jiǎn)化了底層的C代碼允趟,并為Python的日常編程提供了一套靈活的新工具恼策。
1.3. 描述器協(xié)議
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
這是所有描述器方法。一個(gè)對(duì)象具有其中任一個(gè)方法就會(huì)成為描述器潮剪,從而在被當(dāng)作對(duì)象屬性時(shí)重寫默認(rèn)的查找行為涣楷。
如果一個(gè)對(duì)象同時(shí)定義了__get__()
和__set__()
,它叫做資料描述器(data descriptor)。僅定義了__get__()
的描述器叫非資料描述器(常用于方法抗碰,當(dāng)然其他用途也是可以的)
資料描述器和非資料描述器的區(qū)別在于:相對(duì)于實(shí)例的字典的優(yōu)先級(jí)狮斗。如果實(shí)例字典中有與描述器同名的屬性,如果描述器是資料描述器改含,優(yōu)先使用資料描述器情龄,如果是非資料描述器,優(yōu)先使用字典中的屬性捍壤。(譯者注:這就是為何實(shí)例 a的方法和屬性重名時(shí)骤视,比如都叫foo
,Python會(huì)在訪問(wèn) a.foo的時(shí)候優(yōu)先訪問(wèn)實(shí)例字典中的屬性鹃觉,因?yàn)閷?shí)例函數(shù)的實(shí)現(xiàn)是個(gè)非資料描述器)
要想制作一個(gè)只讀的資料描述器专酗,需要同時(shí)定義__set__
和__get__
,并在__set__
中引發(fā)一個(gè)AttributeError異常。定義一個(gè)引發(fā)異常的__set__
方法就足夠讓一個(gè)描述器成為資料描述器盗扇。
1.4. 描述器的調(diào)用
描述器可以直接這么調(diào)用: d.__get__(obj)
然而更常見(jiàn)的情況是描述器在屬性訪問(wèn)時(shí)被自動(dòng)調(diào)用祷肯。舉例來(lái)說(shuō), obj.d
會(huì)在obj
的字典中找d
,如果d
定義了__get__
方法疗隶,那么d.__get__(obj)
會(huì)依據(jù)下面的優(yōu)先規(guī)則被調(diào)用佑笋。
調(diào)用的細(xì)節(jié)取決于obj
是一個(gè)類還是一個(gè)實(shí)例。另外斑鼻,描述器只對(duì)于新式對(duì)象和新式類才起作用蒋纬。繼承于object
的類叫做新式類。
對(duì)于對(duì)象來(lái)講,方法object.__getattribute__()
把b.x
變成type(b).__dict__['x'].__get__(b, type(b))
蜀备。具體實(shí)現(xiàn)是依據(jù)這樣的優(yōu)先順序:資料描述器優(yōu)先于實(shí)例變量关摇,實(shí)例變量?jī)?yōu)先于非資料描述器,__getattr__()
方法(如果對(duì)象中包含的話)具有最低的優(yōu)先級(jí)碾阁。完整的C語(yǔ)言實(shí)現(xiàn)可以在 Objects/object.c 中PyObject_GenericGetAttr() 查看输虱。
對(duì)于類來(lái)講,方法type.__getattribute__()
把B.x
變成B.__dict__['x'].__get__(None, B)
脂凶。用Python來(lái)描述就是:
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
其中重要的幾點(diǎn):
- 描述器的調(diào)用是因?yàn)?code>__getattribute__()
- 重寫
__getattribute__()
方法會(huì)阻止正常的描述器調(diào)用 -
__getattribute__()
只對(duì)新式類的實(shí)例可用 -
object.__getattribute__()
和type.__getattribute__()
對(duì)__get__()
的調(diào)用不一樣 - 資料描述器總是比實(shí)例字典優(yōu)先宪睹。
- 非資料描述器可能被實(shí)例字典重寫。(非資料描述器不如實(shí)例字典優(yōu)先)
super()
返回的對(duì)象同樣有一個(gè)定制的__getattribute__()
方法用來(lái)調(diào)用描述器艰猬。調(diào)用super(B, obj).m()
時(shí)會(huì)先在obj.__class__.__mro__
中查找與B緊鄰的基類A横堡,然后返回A.__dict__['m'].__get__(obj, A)
。如果不是描述器冠桃,原樣返回m
。如果實(shí)例字典中找不到m
道宅,會(huì)回溯繼續(xù)調(diào)用object.__getattribute__()
查找食听。(譯者注:即在__mro__
中的下一個(gè)基類中查找)
注意:在Python 2.2中,如果m
是一個(gè)描述器, super(B, obj).m()
只會(huì)調(diào)用方法__get__()
污茵。在Python 2.3中樱报,非資料描述器(除非是個(gè)舊式類)也會(huì)被調(diào)用。super_getattro()
的實(shí)現(xiàn)細(xì)節(jié)在: Objects/typeobject.c 泞当,[del] 一個(gè)等價(jià)的Python實(shí)現(xiàn)在 Guido’s Tutorial [/del] (譯者注:原文此句已刪除迹蛤,保留供大家參考)。
以上展示了描述器的機(jī)理是在object
, type
, 和super
的__getattribute__()
方法中實(shí)現(xiàn)的襟士。由object
派生出的類自動(dòng)的繼承這個(gè)機(jī)理盗飒,或者它們有個(gè)有類似機(jī)理的元類。同樣陋桂,可以重寫類的__getattribute__()
方法來(lái)關(guān)閉這個(gè)類的描述器行為逆趣。
1.5. 描述器例子
下面的代碼中定義了一個(gè)資料描述器,每次get
和set
都會(huì)打印一條消息嗜历。重寫__getattribute__()
是另一個(gè)可以使所有屬性擁有這個(gè)行為的方法宣渗。但是,描述器在監(jiān)視特定屬性的時(shí)候是很有用的梨州。
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
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
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
這個(gè)協(xié)議非常簡(jiǎn)單痕囱,并且提供了令人激動(dòng)的可能。一些用途實(shí)在是太普遍以致于它們被打包成獨(dú)立的函數(shù)暴匠。像屬性(property), 方法(bound和unbound method), 靜態(tài)方法和類方法都是基于描述器協(xié)議的鞍恢。
1.6. 屬性(properties)
調(diào)用property()
是建立資料描述器的一種簡(jiǎn)潔方式,從而可以在訪問(wèn)屬性時(shí)觸發(fā)相應(yīng)的方法調(diào)用。這個(gè)函數(shù)的原型:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
下面展示了一個(gè)典型應(yīng)用:定義一個(gè)托管屬性(Managed Attribute) 有序。
class C(object):
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "I'm the 'x' property.")
想要看看property()
是怎么用描述器實(shí)現(xiàn)的抹腿? 這里有一個(gè)純Python的等價(jià)實(shí)現(xiàn):
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError, "unreadable attribute"
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError, "can't set attribute"
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError, "can't delete attribute"
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
當(dāng)用戶接口已經(jīng)被授權(quán)訪問(wèn)屬性之后,需求發(fā)生一些變化旭寿,屬性需要進(jìn)一步處理才能返回給用戶警绩。這時(shí) property()能夠提供很大幫助。
例如盅称,一個(gè)電子表格類提供了訪問(wèn)單元格的方式: Cell('b10').value
肩祥。之后,對(duì)這個(gè)程序的改善要求在每次訪問(wèn)單元格時(shí)重新計(jì)算單元格的值缩膝。然而混狠,程序員并不想影響那些客戶端中直接訪問(wèn)屬性的代碼。那么解決方案是將屬性訪問(wèn)包裝在一個(gè)屬性資料描述器中:
class Cell(object):
. . .
def getvalue(self, obj):
"Recalculate cell before returning value"
self.recalc()
return obj._value
value = property(getvalue)
1.7. 函數(shù)和方法
Python的面向?qū)ο筇卣魇墙⒃诨诤瘮?shù)的環(huán)境之上的疾层。非資料描述器把兩者無(wú)縫地連接起來(lái)将饺。
類的字典把方法當(dāng)做函數(shù)存儲(chǔ)。在定義類的時(shí)候痛黎,方法通常用關(guān)鍵字def
和lambda
來(lái)聲明予弧。這和創(chuàng)建函數(shù)是一樣的。唯一的不同之處是類方法的第一個(gè)參數(shù)用來(lái)表示對(duì)象實(shí)例湖饱。Python約定掖蛤,這個(gè)參數(shù)通常是 self, 但也可以叫 this 或者其它任何名字。
為了支持方法調(diào)用井厌,函數(shù)包含一個(gè)__get__()
方法以便在屬性訪問(wèn)時(shí)綁定方法蚓庭。這就是說(shuō)所有的函數(shù)都是非資料描述器,它們返回綁定(bound)還是非綁定(unbound)的方法取決于他們是被實(shí)例調(diào)用還是被類調(diào)用仅仆。用Python代碼來(lái)描述就是:
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
下面運(yùn)行解釋器來(lái)展示實(shí)際情況下函數(shù)描述器是如何工作的:
>>> class D(object):
def f(self, x):
return x
>>> d = D()
>>> D.__dict__['f'] # 存儲(chǔ)成一個(gè)function
<function f at 0x00C45070>
>>> D.f # 從類來(lái)方法器赞,返回unbound method
<unbound method D.f>
>>> d.f # 從實(shí)例來(lái)訪問(wèn),返回bound method
<bound method D.f of <__main__.D object at 0x00B18C90>>
從輸出來(lái)看蝇恶,綁定方法和非綁定方法是兩個(gè)不同的類型拳魁。它們是在文件 Objects/classobject.c(http://svn.python.org/view/python/trunk/Objects/classobject.c?view=markup) 中用C實(shí)現(xiàn)的,PyMethod_Type
是一個(gè)對(duì)象撮弧,但是根據(jù)im_self
是否是 NULL (在C中等價(jià)于 None ) 而表現(xiàn)不同潘懊。
同樣,一個(gè)方法的表現(xiàn)依賴于im_self
贿衍。如果設(shè)置了(意味著bound), 原來(lái)的函數(shù)(保存在im_func
中)被調(diào)用授舟,并且第一個(gè)參數(shù)設(shè)置成實(shí)例。如果unbound, 所有參數(shù)原封不動(dòng)地傳給原來(lái)的函數(shù)贸辈。函數(shù)instancemethod_call()
的實(shí)際C語(yǔ)言實(shí)現(xiàn)只是比這個(gè)稍微復(fù)雜些(有一些類型檢查)释树。
1.8. 靜態(tài)方法和類方法
非資料描述器為將函數(shù)綁定成方法這種常見(jiàn)模式提供了一個(gè)簡(jiǎn)單的實(shí)現(xiàn)機(jī)制。
簡(jiǎn)而言之,函數(shù)有個(gè)方法__get__()
奢啥,當(dāng)函數(shù)被當(dāng)作屬性訪問(wèn)時(shí)秸仙,它就會(huì)把函數(shù)變成一個(gè)實(shí)例方法。非資料描述器把obj.f(*args)
的調(diào)用轉(zhuǎn)換成f(obj, *args)
桩盲。 調(diào)用klass.f(*args)
就變成調(diào)用f(*args)
寂纪。
下面的表格總結(jié)了綁定和它最有用的兩個(gè)變種:
Transformation | Called from an Object | Called from a Class |
---|---|---|
function | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) |
classmethod | f(type(obj), *args) | f(klass, *args) |
靜態(tài)方法原樣返回函數(shù),調(diào)用c.f
或者C.f
分別等價(jià)于object.__getattribute__(c, "f")
或者object.__getattribute__(C, "f")
赌结。也就是說(shuō)捞蛋,無(wú)論是從一個(gè)對(duì)象還是一個(gè)類中,這個(gè)函數(shù)都會(huì)同樣地訪問(wèn)到柬姚。
那些不需要self
變量的方法適合用做靜態(tài)方法拟杉。
例如, 一個(gè)統(tǒng)計(jì)包可能包含一個(gè)用來(lái)做實(shí)驗(yàn)數(shù)據(jù)容器的類。這個(gè)類提供了一般的方法量承,來(lái)計(jì)算平均數(shù)搬设,中位數(shù),以及其他基于數(shù)據(jù)的描述性統(tǒng)計(jì)指標(biāo)宴合。然而焕梅,這個(gè)類可能包含一些概念上與統(tǒng)計(jì)相關(guān)但不依賴具體數(shù)據(jù)的函數(shù)。比如erf(x)
就是一個(gè)統(tǒng)計(jì)工作中經(jīng)常用到的卦洽,但卻不依賴于特定數(shù)據(jù)的函數(shù)。它可以從類或者實(shí)例調(diào)用:s.erf(1.5) --> .9332
或者Sample.erf(1.5) --> .9332
.
既然staticmethod將函數(shù)原封不動(dòng)的返回斜棚,那下面的代碼看上去就很正常了:
>>> class E(object):
def f(x):
print x
f = staticmethod(f)
>>> print E.f(3)
3
>>> print E().f(3)
3
利用非資料描述器阀蒂,staticmethod()
的純Python版本看起來(lái)像這樣:
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
不像靜態(tài)方法,類方法需要在調(diào)用函數(shù)之前會(huì)在參數(shù)列表前添上class的引用作為第一個(gè)參數(shù)弟蚀。不管 調(diào)用者是對(duì)象還是類蚤霞,這個(gè)格式是一樣的:
>>> class E(object):
def f(klass, x):
return klass.__name__, x
f = classmethod(f)
>>> print E.f(3)
('E', 3)
>>> print E().f(3)
('E', 3)
當(dāng)一個(gè)函數(shù)不需要相關(guān)的數(shù)據(jù)做參數(shù)而只需要一個(gè)類的引用的時(shí)候,這個(gè)特征就顯得很有用了义钉。類方法的一個(gè)用途是用來(lái)創(chuàng)建不同的類構(gòu)造器昧绣。在Python 2.3中,dict.fromkeys()
可以依據(jù)一個(gè)key列表來(lái)創(chuàng)建一個(gè)新的字典。等價(jià)的Python實(shí)現(xiàn)就是:
class Dict:
. . .
def fromkeys(klass, iterable, value=None):
"Emulate dict_fromkeys() in Objects/dictobject.c"
d = klass()
for key in iterable:
d[key] = value
return d
fromkeys = classmethod(fromkeys)
現(xiàn)在捶闸,一個(gè)新的字典就可以這么創(chuàng)建:
>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}
用非資料描述器協(xié)議夜畴,classmethod()
的純Python版本實(shí)現(xiàn)看起來(lái)像這樣:
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc