這是關(guān)于Python裝飾器系列文章的第二篇,第一篇在這里
如何正確地實(shí)現(xiàn) Python 裝飾器
上一篇博文中,我列出了傳統(tǒng)Python裝飾器所缺失的以下4項(xiàng)功能:
- 保留函數(shù)的
__name__
and__doc__
。 - 保留函數(shù)的參數(shù)定義。
- 保留獲取函數(shù)源碼的能力。
- 能夠在帶有描述器協(xié)議的其他裝飾器上應(yīng)用自己所寫的裝飾器。
上一篇章中,通過使用functools.wraps()
,
能夠保留函數(shù)的__name__
及__doc__
屬性。但是,它無(wú)法保留函數(shù)的參數(shù)定義,或保留獲取函數(shù)源碼的能力。
在本篇, 我們聚焦最后一項(xiàng),關(guān)于裝飾器與描述器的交互。把function wrapper應(yīng)用于描述器(descriptor)。
何為描述器?
有關(guān)描述器的詳細(xì)解釋請(qǐng)參考Python描述器引導(dǎo)(翻譯)
一般來(lái)說(shuō),一個(gè)描述器是一個(gè)有“綁定行為”的對(duì)象屬性(object attribute),它的訪問控制被描述器協(xié)議方法重寫。這些方法是 __get__()
, __set__()
, 和 __delete__()
。有這些方法的對(duì)象叫做描述器。
-
obj.attribute
-->attribute.__get__(obj, type(obj))
-
obj.attribute = value
-->attribute.__set__(obj, value)
-
del obj.attribute
-->attribute.__delete__(obj)
如果一個(gè)類的屬性擁有這些特殊方法,即可重寫此屬性的關(guān)聯(lián)操作行為(取值/賦值/刪除此屬性)。
或許你認(rèn)為從來(lái)不會(huì)用到描述器,但事實(shí)上函數(shù)對(duì)象就是描述器。當(dāng)函數(shù)被添加到class定義時(shí),它作為普通函數(shù)。當(dāng)你通過.
號(hào)訪問此函數(shù)時(shí),你在調(diào)用__get__()
方法把此函數(shù)與實(shí)例綁定,讓其成為對(duì)象的綁定方法。
def f(obj): pass
>>> hasattr(f, '__get__')
True
>>> f
<function f at 0x10e963cf8>
>>> obj = object()
>>> f.__get__(obj, type(obj))
<bound method object.f of <object object at 0x10e8ac0b0>>
當(dāng)調(diào)用類方法(@classmethod
)時(shí),調(diào)用的并不是原函數(shù)對(duì)象的 __call__()
方法,而是臨時(shí)綁定對(duì)象的 __call__()
方法。這個(gè)臨時(shí)綁定對(duì)象在訪問這個(gè)函數(shù)(即類方法)時(shí)所創(chuàng)建。
一般地,你不會(huì)看到前述的這些內(nèi)部細(xì)節(jié)實(shí)現(xiàn)。
>>> class Object(object):
... def f(self): pass
>>> obj = Object()
>>> obj.f
<bound method Object.f of <__main__.Object object at 0x10abf29d0>>
回顧以下第一篇出現(xiàn)過的例子,當(dāng)我們把裝飾器放到類方法上時(shí),會(huì)得到一個(gè)異常:
class Class(object):
@function_wrapper
@classmethod
def cmethod(cls):
pass
>>> Class.cmethod()
Traceback (most recent call last):
File "classmethod.py", line 15, in <module>
Class.cmethod()
File "classmethod.py", line 6, in _wrapper
return wrapped(*args, **kwargs)
TypeError: 'classmethod' object is not callable
特別的是,人們所用的簡(jiǎn)單裝飾器并沒有實(shí)現(xiàn)描述器協(xié)議,將其應(yīng)用在wrapped object(這里指classmethod)上時(shí),會(huì)產(chǎn)生一個(gè)綁定的函數(shù)對(duì)象,理應(yīng)這個(gè)函數(shù)對(duì)象會(huì)被調(diào)用。但實(shí)際上卻是直接調(diào)用被包裹的對(duì)象,如果wrapped object沒有__call__()
方法,便會(huì)導(dǎo)致異常拋出。
那為什么用于普通實(shí)例方法的裝飾器可以正常工作呢?
因?yàn)槠胀ê瘮?shù)仍具有__call__()
方法。在繞過被包裹函數(shù)的描述器協(xié)議后會(huì)繼續(xù)調(diào)用__call__()
方法。且在調(diào)用原非綁定函數(shù)對(duì)象(original unbound function object)時(shí),包裝器(warpper)依然會(huì)顯式地將self
作為第一個(gè)參數(shù)傳給實(shí)例。
作為描述器的包裝器
為解決上述討論的問題,只需給包裝器實(shí)現(xiàn)描述器協(xié)議。
class bound_function_wrapper:
def __init__(self, wrapped):
self.wrapped = wrapped
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
class function_wrapper:
def __init__(self, wrapped):
self.wrapped = wrapped
def __get__(self, instance, owner):
wrapped = self.wrapped.__get__(instance, owner)
return bound_function_wrapper(wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
當(dāng)包裝器應(yīng)用在普通函數(shù)上時(shí),其__call__()
方法會(huì)被調(diào)用。當(dāng)包裝器應(yīng)用在類方法(@classmethod
)上時(shí), __get__()
方法會(huì)被調(diào)用, __get__()
方法返回一個(gè)新綁定的包裝器(bound wrapper),然后調(diào)用bound wrapper的__call__()
方法。通過描述器協(xié)議的傳遞,這個(gè)新的包裝器就可以應(yīng)用在描述器上了。
所以,用函數(shù)閉包去包裹帶有描述器協(xié)議的裝飾器會(huì)導(dǎo)致失敗。若想讓裝飾器正常運(yùn)作,我們應(yīng)該以類方式去實(shí)現(xiàn)包裝器,且這個(gè)類須實(shí)現(xiàn)描述器協(xié)議。
現(xiàn)在我們來(lái)厘清文章開頭列出清單中的其他問題。
之前我們通過functools.wrap()
/functools.update_wrapper()
來(lái)解決名稱(naming)問題,但是它們都做了些什么?我們還能繼續(xù)使用它們嗎?
好吧,wraps()
只是使用了 update_wrapper()
,我們來(lái)看看update_wrapper()
的實(shí)現(xiàn)。
WRAPPER_ASSIGNMENTS = ('__module__',
'__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper, wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
wrapper.__wrapped__ = wrapped
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(
getattr(wrapped, attr, {}))
上面是Python 3.3的代碼,雖然當(dāng)中有個(gè)在Python 3.4中已被修復(fù)的bug。:-)
函數(shù)的主體里完成了3樣事情。
-
__wrapped__
儲(chǔ)存wrapped function的引用。這是一個(gè)bug,它應(yīng)該放在函數(shù)的最后部分實(shí)現(xiàn)。
- Copy
__name__
及__doc__
等屬性
- 把wrapped function的
__dict__
Copy到包裝器,當(dāng)中包含了大部分需要Copy的內(nèi)容。
當(dāng)使用函數(shù)閉包或class形式的裝飾器,這些copy動(dòng)作會(huì)在套用裝飾器時(shí)完成。
就算裝飾器帶有描述器協(xié)議,這些技術(shù)細(xì)節(jié)仍需在綁定包裝器(bound wrapper)里完成。
class bound_function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
functools.update_wrapper(self, wrapped)
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
functools.update_wrapper(self, wrapped)
為了將函數(shù)綁定至class而調(diào)用包裝器(wrapper)的時(shí)候,每次都會(huì)創(chuàng)建綁定包裝器(bound wrapper)。這樣會(huì)帶來(lái)性能上的損失,我們需要額外的工作來(lái)解決這問題。
透明對(duì)象代理
解決性能問題的方案需要使用對(duì)象代理(object proxy),它是一個(gè)特殊的wrapper class,其外觀及行為與被它包裹的對(duì)象相似。
class object_proxy(object):
def __init__(self, wrapped):
self.wrapped = wrapped
try:
self.__name__= wrapped.__name__
except AttributeError:
pass
@property
def __class__(self):
return self.wrapped.__class__
def __getattr__(self, name):
return getattr(self.wrapped, name)
一個(gè)完整的透明對(duì)象代理(A fully transparent object proxy)過于復(fù)雜, 這里帶過細(xì)節(jié)不說(shuō),我會(huì)另撰文章解釋。
上述例子簡(jiǎn)單地展現(xiàn)了它的工作方式。實(shí)踐中它會(huì)做更多的工作,尤其是在使用猴子補(bǔ)丁(monkey patching)時(shí)。
總之,它從wrapped object上copy了有限的屬性至自身,
使用了一些特殊方法、特性及__getattr__()
,當(dāng)必要時(shí)才去獲取被包裹對(duì)象的屬性,這就避免了copy那些從不訪問的屬性。
現(xiàn)在我們只需利用對(duì)象代理來(lái)派生我們的包裝器class,及移除update_wrapper()
class bound_function_wrapper(object_proxy):
def __init__(self, wrapped):
super(bound_function_wrapper, self).__init__(wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
class function_wrapper(object_proxy):
def __init__(self, wrapped):
super(function_wrapper, self).__init__(wrapped)
def __get__(self, instance, owner):
wrapped = self.wrapped.__get__(instance, owner)
return bound_function_wrapper(wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
這樣,通過包裝器(wrapper)查詢__name__
和``doc`這些屬性時(shí),返回的是wrapped function的屬性值,而不是包裝器的屬性值。
通過使用透明對(duì)象代理,inspect.getargspec()
和inspect.getsource()
現(xiàn)可如常運(yùn)作了。無(wú)需額外工作即可同時(shí)解決了兩個(gè)問題。
使這些更有用
上述方式雖解決了一開始提出的問題,但是它含有大量冗余的代碼,包括兩處重復(fù)調(diào)用wrapped function的地方。當(dāng)你要為裝飾器實(shí)現(xiàn)功能時(shí),你仍要往這兩處插入代碼。
每次實(shí)現(xiàn)裝飾器時(shí)重復(fù)這些勞動(dòng)實(shí)在痛苦。
取而代之,我們需要把這些打包進(jìn)裝飾器工廠(decorator factory),以免每次手工重復(fù)編寫。本系列下篇文章將介紹如何實(shí)現(xiàn)。
出處:https://github.com/GrahamDumpleton/wrapt/tree/develop/blog