實(shí)現(xiàn)描述器?
- 作者
Raymond Hettinger
- 聯(lián)系方式
<python at rcn dot com>
摘要?
定義描述器,總結(jié)描述器協(xié)議,展示描述器被如何使用。測試一個(gè)自定義的描述器和若干 Python 內(nèi)置的描述器,包括函數(shù)、屬性、靜態(tài)方法和類方法。通過給出一個(gè)純 Python 的等價(jià)實(shí)現(xiàn)和例程,展示每個(gè)描述器如何工作。
學(xué)習(xí)描述器不僅能提供接觸到更多工具集的途徑,還能更深地理解 Python 工作的原理并更加體會(huì)到其設(shè)計(jì)的優(yōu)雅性。
定義和簡介?
一般地,一個(gè)描述器是一個(gè)包含 “綁定行為” 的對(duì)象,對(duì)其屬性的訪問被描述器協(xié)議中定義的方法覆蓋。這些方法有:__get__(),__set__() 和 __delete__()。如果某個(gè)對(duì)象中定義了這些方法中的任意一個(gè),那么這個(gè)對(duì)象就可以被稱為一個(gè)描述器。
屬性訪問的默認(rèn)行為是從一個(gè)對(duì)象的字典中獲取、設(shè)置或刪除屬性。例如,a.x 的查找順序會(huì)從 a.__dict__['x'] 開始,然后是 type(a).__dict__['x'],接下來依次查找 type(a) 的基類,不包括元類。 如果找到的值是定義了某個(gè)描述器方法的對(duì)象,則 Python 可能會(huì)重載默認(rèn)行為并轉(zhuǎn)而發(fā)起調(diào)用描述器方法。這具體發(fā)生在優(yōu)先級(jí)鏈的哪個(gè)環(huán)節(jié)則要根據(jù)所定義的描述器方法及其被調(diào)用的方式來決定。
描述器是一個(gè)強(qiáng)大而通用的協(xié)議。 它們是特征屬性、方法靜態(tài)方法、類方法和 super() 背后的實(shí)現(xiàn)機(jī)制。 它們?cè)?Python 內(nèi)部被廣泛使用來實(shí)現(xiàn)自 2.2 版中引入的新式類。 描述器簡化了底層的 C 代碼并為 Python 的日常程序提供了一組靈活的新工具。
描述器協(xié)議?
descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None
以上就是全部。定義這些方法中的任何一個(gè)的對(duì)象被視為描述器,并覆蓋其默認(rèn)行為。
如果一個(gè)對(duì)象同時(shí)定義了 __get__() 和 __set__(),則它會(huì)被視為數(shù)據(jù)描述器。 僅定義了 __get__() 的稱為非數(shù)據(jù)描述器(它們通常被用于方法,但也可以有其他用途)。
數(shù)據(jù)和非數(shù)據(jù)描述器的不同之處在于,如何計(jì)算實(shí)例字典中條目的替代值。如果實(shí)例的字典具有與數(shù)據(jù)描述器同名的條目,則數(shù)據(jù)描述器優(yōu)先。如果實(shí)例的字典具有與非數(shù)據(jù)描述器同名的條目,則該字典條目優(yōu)先。
為了使只讀數(shù)據(jù)描述符,同時(shí)定義 __get__() 和 __set__() ,并在 __set__() 中引發(fā) AttributeError 。用引發(fā)異常的占位符定義 __set__() 方法能夠使其成為數(shù)據(jù)描述符。
發(fā)起調(diào)用描述器?
描述符可以通過其方法名稱直接調(diào)用。例如, d.__get__(obj) 。
或者,更常見的是在屬性訪問時(shí)自動(dòng)調(diào)用描述符。例如,在中 obj.d 會(huì)在 d 的字典中查找 obj 。如果 d 定義了方法 __get__() ,則 d.__get__(obj) 根據(jù)下面列出的優(yōu)先級(jí)規(guī)則進(jìn)行調(diào)用。
調(diào)用的細(xì)節(jié)取決于 obj 是對(duì)象還是類。
對(duì)于對(duì)象來說, object.__getattribute__() 中的機(jī)制是將 b.x 轉(zhuǎn)換為 type(b).__dict__['x'].__get__(b, type(b)) 。 這個(gè)實(shí)現(xiàn)通過一個(gè)優(yōu)先級(jí)鏈完成,該優(yōu)先級(jí)鏈賦予數(shù)據(jù)描述器優(yōu)先于實(shí)例變量的優(yōu)先級(jí),實(shí)例變量優(yōu)先于非數(shù)據(jù)描述符的優(yōu)先級(jí),并如果 __getattr__() 方法存在,為其分配最低的優(yōu)先級(jí)。 完整的C實(shí)現(xiàn)可在 Objects/object.c 中的 PyObject_GenericGetAttr() 找到。
對(duì)于類來說,機(jī)制是 type.__getattribute__() 中將 B.x 轉(zhuǎn)換為 B.__dict__['x'].__get__(None, B) 。在純Python中,它就像:
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)是:
描述器由
__getattribute__()方法調(diào)用重寫
__getattribute__()?會(huì)阻止描述器的自動(dòng)調(diào)用object.__getattribute__()和type.__getattribute__()會(huì)用不同的方式調(diào)用__get__().數(shù)據(jù)描述符始終會(huì)覆蓋實(shí)例字典。
非數(shù)據(jù)描述器會(huì)被實(shí)例字典覆蓋。
super() 返回的對(duì)象還有一個(gè)自定義的 __getattribute__() 方法用來發(fā)起調(diào)用描述器。 調(diào)用 super(B, obj).m() 會(huì)搜索 obj.__class__.__mro__ 緊隨 B 的基類 A,然后返回 A.__dict__['m'].__get__(obj, B)。 如果其不是描述器,則原樣返回 m。 如果不在字典中,m 會(huì)轉(zhuǎn)而使用 object.__getattribute__() 進(jìn)行搜索。
這個(gè)實(shí)現(xiàn)的具體細(xì)節(jié)在 Objects/typeobject.c. 的 super_getattro() 中,并且你還可以在 中找到等價(jià)的純Python實(shí)現(xiàn)。
以上展示的關(guān)于描述器機(jī)制的細(xì)節(jié)嵌入在 object , type , 和 super() 中的 __getattribute__() 。當(dāng)類派生自類 object 或有提供類似功能的元類時(shí),它們將繼承此機(jī)制。同樣,類可以通過重寫 __getattribute__() 阻止描述器調(diào)用。
描述符示例?
以下代碼創(chuàng)建一個(gè)類,其對(duì)象是數(shù)據(jù)描述器,該描述器為每個(gè) get 或 set 打印一條消息。覆蓋 __getattribute__() 是可以對(duì)每個(gè)屬性執(zhí)行此操作的替代方法。但是,此描述器對(duì)于跟蹤僅幾個(gè)選定的屬性很有用:
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é)議很簡單,并提供了令人興奮的可能性。有幾種用例非常普遍,以至于它們被打包到單獨(dú)的函數(shù)調(diào)用中。屬性、綁定方法、靜態(tài)方法和類方法均基于描述器協(xié)議。
屬性?
調(diào)用 property() 是構(gòu)建數(shù)據(jù)描述器的簡潔方式,該數(shù)據(jù)描述器在訪問屬性時(shí)觸發(fā)函數(shù)調(diào)用。它的簽名是:
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
該文檔顯示了定義托管屬性 x 的典型用法:
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() 如何根據(jù)描述符協(xié)議實(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
if doc is None and fget is not None:
doc = fget.__doc__
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__)
這個(gè)內(nèi)置的 property() 每當(dāng)用戶訪問屬性時(shí)生效,隨后的變化需要一個(gè)方法的參與。
例如,一個(gè)電子表格類可以通過 Cell('b10').value 授予對(duì)單元格值的訪問權(quán)限。對(duì)程序的后續(xù)改進(jìn)要求每次訪問都要重新計(jì)算單元格;但是,程序員不希望影響直接訪問該屬性的現(xiàn)有客戶端代碼。解決方案是將對(duì)value屬性的訪問包裝在屬性數(shù)據(jù)描述器中:
class Cell(object):
. . .
def getvalue(self):
"Recalculate the cell before returning value"
self.recalc()
return self._value
value = property(getvalue)
函數(shù)和方法?
Python 的面向?qū)ο蠊δ苁窃诨诤瘮?shù)的環(huán)境構(gòu)建的。使用非數(shù)據(jù)描述符,兩者完成了無縫融合。
類字典將方法存儲(chǔ)為函數(shù)。在類定義中,方法是用 def 或 lambda 這兩個(gè)創(chuàng)建函數(shù)的常用工具編寫的。方法與常規(guī)函數(shù)的不同之處僅在于第一個(gè)參數(shù)是為對(duì)象實(shí)例保留的。按照 Python 約定,實(shí)例引用稱為 self ,但也可以稱為 this 或任何其他變量名稱。
為了支持方法調(diào)用,函數(shù)包含 __get__() 方法用于在訪問屬性時(shí)將其綁定成方法。這意味著所有函數(shù)都是非數(shù)據(jù)描述符,當(dāng)從對(duì)象調(diào)用它們時(shí),它們返回綁定方法。在純 Python 中,它的工作方式如下:
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Simulate func_descr_get() in Objects/funcobject.c"
if obj is None:
return self
return types.MethodType(self, obj)
運(yùn)行解釋器顯示了函數(shù)描述器在實(shí)踐中的工作方式:
>>> class D(object):
... def f(self, x):
... return x
...
>>> d = D()
# Access through the class dictionary does not invoke __get__.
# It just returns the underlying function object.
>>> D.__dict__['f']
<function D.f at 0x00C45070>
# Dotted access from a class calls __get__() which just returns
# the underlying function unchanged.
>>> D.f
<function D.f at 0x00C45070>
# The function has a __qualname__ attribute to support introspection
>>> D.f.__qualname__
'D.f'
# Dotted access from an instance calls __get__() which returns the
# function wrapped in a bound method object
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>
# Internally, the bound method stores the underlying function,
# the bound instance, and the class of the bound instance.
>>> d.f.__func__
<function D.f at 0x1012e5ae8>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>
>>> d.f.__class__
<class 'method'>
靜態(tài)方法和類方法?
非數(shù)據(jù)描述器為把函數(shù)綁定到方法的通常模式提供了一種簡單的機(jī)制。
概括地說,函數(shù)具有 __get__() 方法,以便在作為屬性訪問時(shí)可以將其轉(zhuǎn)換為方法。非數(shù)據(jù)描述符將 obj.f(*args) 的調(diào)用轉(zhuǎn)換為 f(obj, *args) 。調(diào)用 klass.f(*args)` 因而變成 f(*args) 。
下表總結(jié)了綁定及其兩個(gè)最有用的變體:
轉(zhuǎn)換形式
通過對(duì)象調(diào)用
通過類調(diào)用
function -- 函數(shù)
f(obj, *args)
f(*args)
靜態(tài)方法
f(*args)
f(*args)
類方法
f(type(obj), *args)
f(klass, *args)
靜態(tài)方法返回底層函數(shù),不做任何更改。調(diào)用 c.f 或 C.f 等效于通過 object.__getattribute__(c, "f") 或 object.__getattribute__(C, "f") 查找。結(jié)果,該函數(shù)變得可以從對(duì)象或類中進(jìn)行相同的訪問。
適合于作為靜態(tài)方法的是那些不引用 self 變量的方法。
例如,一個(gè)統(tǒng)計(jì)用的包可能包含一個(gè)實(shí)驗(yàn)數(shù)據(jù)的容器類。該容器類提供了用于計(jì)算數(shù)據(jù)的平均值,均值,中位數(shù)和其他描述性統(tǒng)計(jì)信息的常規(guī)方法。但是,可能有在概念上相關(guān)但不依賴于數(shù)據(jù)的函數(shù)。例如, erf(x) 是在統(tǒng)計(jì)中的便捷轉(zhuǎn)換,但并不直接依賴于特定的數(shù)據(jù)集??梢詮膶?duì)象或類中調(diào)用它: s.erf(1.5) --> .9332 或 Sample.erf(1.5) --> .9332。
由于靜態(tài)方法直接返回了底層的函數(shù),因此示例調(diào)用是平淡的:
>>> class E(object):
... def f(x):
... print(x)
... f = staticmethod(f)
...
>>> E.f(3)
3
>>> E().f(3)
3
使用非數(shù)據(jù)描述器,純Python的版本 staticmethod() 如下所示:
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ù)之前將類引用放在參數(shù)列表的最前。無論調(diào)用方是對(duì)象還是類,此格式相同:
>>> 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)函數(shù)僅需要使用類引用并且不關(guān)心任何底層數(shù)據(jù)時(shí)的情況。 類方法的一種用途是創(chuàng)建替代的類構(gòu)造器。 在 Python 2.3 中,類方法 dict.fromkeys() 會(huì)從鍵列表中創(chuàng)建一個(gè)新的字典。 純 Python 的等價(jià)形式是:
class Dict(object):
. . .
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òu)造一個(gè)新的唯一鍵字典:
>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}
使用非數(shù)據(jù)描述符協(xié)議,純 Python 版本的 classmethod() 如下:
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
