Python裝飾器系列02 - 裝飾器和描述器之間的交互

這是關(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樣事情。

  1. __wrapped__儲(chǔ)存wrapped function的引用。這是一個(gè)bug,它應(yīng)該放在函數(shù)的最后部分實(shí)現(xiàn)。
  1. Copy __name____doc__等屬性
  1. 把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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 雖然人們能利用函數(shù)閉包(function clouser)寫出簡(jiǎn)單的裝飾器,但其可用范圍常受限制。多數(shù)實(shí)現(xiàn)裝飾器的...
    gomibako閱讀 1,031評(píng)論 0 4
  • 裝飾是為函數(shù)和類指定管理代碼的一種方式.裝飾器本身的形式是處理其他的可調(diào)用對(duì)象的可調(diào)用的對(duì)象。 函數(shù)裝飾器在函數(shù)定...
    低吟淺唱1990閱讀 235評(píng)論 0 0
  • 函數(shù)和對(duì)象 1、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語(yǔ)言來(lái)說(shuō)都是核心的概念。通過函數(shù)可以封裝任意多條語(yǔ)句,而且...
    道無(wú)虛閱讀 4,630評(píng)論 0 5
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,869評(píng)論 18 139
  • 我喜歡自己鄉(xiāng)村的傍晚,傍晚時(shí)屋頂上空悠然升起的炊煙。煙囪下灶塘彌漫著食物的清香,那歡聲笑語(yǔ)的屋子座落在傍晚的村莊中...
    長(zhǎng)林張少閱讀 1,714評(píng)論 0 7