python 装饰器实现_Python装饰器系列01 - 如何正确地实现装饰器
雖然人們能利用函數閉包(function clouser)寫出簡單的裝飾器,但其可用范圍常受限制。多數實現裝飾器的基本方式會破壞與內省(Introspection)的關聯性。
可大多數人會說:who cares!
但我仍堅持追求正確地寫出漂亮代碼。
我愛內省(introspection),討厭猴子補丁(Monkey Patching)
請記住以下兩點:
要為被裝飾器包裹的函數(wrapped function)保留內省功能。
要理解清楚Python對象模型的執行方式如何工作。
接下來,我會通過14篇blog來向你解釋:
你的典型Python裝飾器及包裹的函數哪里有問題
如何修復這些問題
以下是第一篇內容,我會從幾個方面簡單說明你的典型Python裝飾器如何產生問題。
Python 裝飾器基礎知識
人皆所知Python裝飾器語法如下:
@function_wrapper
def function():
pass
@符號為自Python2.4引入的裝飾器的語法糖(syntactic sugar), 它等同以下寫法
def function():
pass
function = function_wrapper(function)
此@裝飾器語法用于包裹定義或修改的函數
裝飾器與猴子補丁不同,前者作用于定義時,后者作用于運行時
函數wrapper剖析
以下用class來實現一個裝飾器
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
@function_wrapper
def function():
pass
以上例子,class實例初始化后會在其內部記錄一個原函數(self.wrapped = wrapped),在調用這個被class裝飾器包裹起來的函數時,實際上是通過調用class對象的__call()__方法來調用原函數。
你可以通過裝飾器,在調用原函數之前或之后,實現一些額外的功能。如需修改傳遞給原函數的輸入參數,或原函數返回的結果,你只要在__call__()方法內進行修改。
用class來實現裝飾器或許不太流行(2014年)。普遍用函數閉包來實現裝飾器。函數閉包實現方式為:利用嵌套函數逐層返回傳入的原函數(wrapped)。代碼如下:
def function_wrapper(wrapped):
def _wrapper(*args, **kwargs):
return wrapped(*args, **kwargs)
return _wrapper
@function_wrapper
def function():
pass
此例中,無明顯地給內嵌函數_wrapper傳入原函數wrapped,內嵌函數仍可通過外層函數function_wrapper的參數訪問到原函數(閉包原理),與用class實現裝飾器相比,此做法方便多了。
Introspecting a function
函數內省
我們期望函數可指定一些與描述自身相關的特性(properties),如__name__ 及 __doc__ 這樣的屬性。當我們把以函數閉包方式實現的裝飾器應用到普通函數時,函數的這些屬性會發生意料之外的變化。這些屬性細節為內嵌函數提供。
def function_wrapper(wrapped):
def _wrapper(*args, **kwargs):
return wrapped(*args, **kwargs)
return _wrapper
@function_wrapper
def function():
pass
>>> print(function.__name__)
_wrapper
若以class方式實現的wrapper,類實例通常不帶有__name__屬性,以此方式去嘗試訪問原函數的name屬性時會得到一個AttributeError異常
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
@function_wrapper
def function():
pass
>>> print(function.__name__)
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'function_wrapper' object has no attribute '__name__'
當以函數閉包方式實現裝飾器時,為保留原函數相關信息,我們可以把原函數的相關屬性Copy一份給內嵌函數。如下例,可正確獲得原函數的__name__及__doc__內容。
def function_wrapper(wrapped):
def _wrapper(*args, **kwargs):
return wrapped(*args, **kwargs)
_wrapper.__name__ = wrapped.__name__
_wrapper.__doc__ = wrapped.__doc__
return _wrapper
@function_wrapper
def function():
pass
>>> print(function.__name__)
function
這樣Copy屬性實在費力,將來如有要追加的屬性還得更新代碼。例如我們想Copy__module__,還有Python 3新增加的__qualname__及__annotations__屬性。我們可以利用Python標準庫提供的functools.wraps()裝飾器來實現這些需求。
import functools
def function_wrapper(wrapped):
@functools.wraps(wrapped)
def _wrapper(*args, **kwargs):
return wrapped(*args, **kwargs)
return _wrapper
@function_wrapper
def function():
pass
>>> print(function.__name__)
function
如以class方式實現裝飾器,則可用functools.update_wrapper(),如下例所示:
import functools
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
functools.update_wrapper(self, wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
雖然functools.wraps()能解決諸如訪問原函數的__name__及__doc__的問題,但實際上并沒有完美解決函數內省,接下來你會看到。
當我們查詢被裝飾器包裹的原函數的參數定義時,返回的結果卻是wrapper的參數定義。以函數閉包實現的裝飾器為例,返回的為內嵌函數的參數定義。因此,裝飾器不具簽名保護(not signature preserving)
import inspect
def function_wrapper(wrapped):
def _wrapper(*arg, **kwarg):
return wrapped(*arg, **kwarg)
return _wrapper
@function_wrapper
def function(arg1, arg2): pass
>>> print(inspect.signature(function))
(*arg, **kwarg)
以class實現的裝飾器也是同樣的結果。
import inspect
class function_wrapper:
def __init__(self, wrapped):
self.wrapped = wrapped
def __call__(self, *arg, **kwarg):
return self.wrapped(*arg, **kwarg)
@function_wrapper
def function(arg1, arg2): pass
>>> print(inspect.signature(function))
(*arg, **kwarg)
另一個和內省相關的例子是,當用inspect.getsource()嘗試返回函數(此函數被以class方式實現的裝飾器包裹起來)的源碼時,會得到一個TypeError異常。
TypeError: <__main__.function_wrapper object at> is not a module, class, method,
function, traceback, frame, or code object
The terminal process terminated with exit code: 1
包裹class方法
和普通函數一樣,裝飾器也可應用在class的方法上。Python內置的兩個特殊裝飾器——@staticmethod和@classmethod可將普通的實例方法(instance method)轉化為class相關的特殊方法。雖然這些特殊方法也隱含著一些問題。
class Class(object):
@function_wrapper
def method(self):
pass
@classmethod
def cmethod(cls):
pass
@staticmethod
def smethod():
pass
首先,就算在你的裝飾器里用上了 functools.wraps() 或 functools.update_wrapper(),當你把這個裝飾器放在 @classmethod 或 @staticmethod前面時,依然會得到一個異常。這是因為依然有一些屬性并未被functools.wraps()或functools.update_wrapper()Copy進來。以下為Python2的運行情況。
class Class(object):
@function_wrapper
@classmethod
def cmethod(cls):
pass
Traceback (most recent call last):
File "", line 1, in
File "", line 3, in Class
File "", line 2, in wrapper
File ".../functools.py", line 33, in update_wrapper
setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'classmethod' object has no attribute '__module__'
此為Python2的bug所致,此bug已在Python3中得到修正。
就算在Python3中運行,依然有異常拋出。那是因為兩個包裹類型(wrapper types,即@function_wrapper及@classmethod)都期望被包裹函數(wrapped function)是可以被直接調用的(callable)。此被包裹的函數可稱之為描述器(descriptor)。這意味為了返回一個可調用的描述器,它(描述器)須先正確地與實例綁定起來。參考以下代碼
class Class(object):
@function_wrapper
@classmethod
def cmethod(cls):
pass
>>> Class.cmethod()
Traceback (most recent call last):
File "classmethod.py", line 15, in
Class.cmethod()
File "classmethod.py", line 6, in _wrapper
return wrapped(*args, **kwargs)
TypeError: 'classmethod' object is not callable
簡單并非意味著正確
雖然我們可以簡單地實現裝飾器,并不見得這些裝飾器必然正確及長久有效。
至此,比較突出的問題如下:
保留函數的 __name__ and __doc__。
保留函數的參數定義。
保留獲取函數源碼的能力。
能夠在帶有描述器協議的其他裝飾器上應用自己所寫的裝飾器。
functools.wraps() 為我們解決了第一個問題,但不能一勞永逸。例如不能解決內省相關的問題。
就算能解決內省相關的問題,簡單實現的裝飾器依然會破壞python對象的執行模型,譬如被裝飾器包裹著的帶描述器協議的對象。
第三方包(packages)如decorator模塊嘗試解決這些問題,但只能解決前面兩點問題。通用猴子補丁動態地應用函數包裝器(function wrapper)時依然會發生問題。
我們找出了一些問題,后續博文中,我們會看到如何解決這些問題。而且你也會寫出優雅的裝飾器。
請繼續關注我下期博文,希望我能保持繼續寫博的沖勁。
總結
以上是生活随笔為你收集整理的python 装饰器实现_Python装饰器系列01 - 如何正确地实现装饰器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python的数值类型_Python的数
- 下一篇: python execute_comma