淺談Python裝飾器 By 馬冬亮(凝霜 Loki) 一個(gè)人的戰(zhàn)爭(http://blog.csdn.net/MDL13412) 前置知識一級對象Python將一切視為 objec t的子類,即一切都是對象,因此函數(shù)可以像變量一樣被指向和傳遞,我們來看下面的例子:
def foo(): pass print issubclass(foo.__class__, object) 其運(yùn)行結(jié)果如下:True 上述代碼說明了Python 中的函數(shù)是object 的子類,下面讓我們看函數(shù)被當(dāng)作參數(shù)傳遞時(shí)的效果:def foo(func): func()def bar(): print 'bar'foo(bar) 其運(yùn)行結(jié)果如下:bar
Python中的namespacePython中通過提供 namespace 來實(shí)現(xiàn)重名函數(shù)/方法、變量等信息的識別,其一共有三種 namespace,分別為: - local namespace: 作用范圍為當(dāng)前函數(shù)或者類方法
- global namespace: 作用范圍為當(dāng)前模塊
- build-in namespace: 作用范圍為所有模塊
當(dāng)函數(shù)/方法、變量等信息發(fā)生重名時(shí),Python會按照 local namespace -> global namespace -> build-in namespace的順序搜索用戶所需元素,并且以第一個(gè)找到此元素的namespace 為準(zhǔn)。 下面以系統(tǒng)的 build-in 函數(shù) str 為例進(jìn)行說明:
def str(s): print 'global str()'def foo(): def str(s): print 'closure str()' str('dummy')def bar(): str('dummy')foo()bar() 首先定義三個(gè)global namespace 的函數(shù) str、foo 和bar,然后在foo 函數(shù)中定義一個(gè)內(nèi)嵌的 local namespace 的函數(shù)str,然后在函數(shù)foo 和 bar 中分別調(diào)用 str('dummy'),其運(yùn)行結(jié)果如下所示:closure str()global str() 通過編碼實(shí)驗(yàn),我們可以看到:
- foo 中調(diào)用 str 函數(shù)時(shí),首先搜索 local namespace,并且成功找到了所需的函數(shù),停止搜索,使用此namespace 中的定義
- bar 中調(diào)用 str 函數(shù)時(shí),首先搜索 local namespace,但是沒有找到str 方法的定義,因此繼續(xù)搜索global namespace,并成功找到了 str 的定義,停止搜索,并使用此定義
下面我們使用Python內(nèi)置的 `ocals() 和 globals() 函數(shù)查看不同 namespace 中的元素定義:
var = 'var in global'def fun(): var = 'var in fun' print 'fun: ' + str(locals())print 'globals: ' + str(globals())fun() 運(yùn)行結(jié)果如下:
globals: {'__builtins__': , '__file__': 'a.py', '__package__': None, 'fun': , 'var': 'var in global', '__name__': '__main__', '__doc__': None}fun: {'var': 'var in fun'} 通過運(yùn)行結(jié)果,我們看到了 fun 定義了 local namespace 的變量var,在global namespace 有一個(gè)全局的 var 變量,那么當(dāng)在global namespace 中直接訪問var 變量的時(shí)候,將會得到 var = 'var in global' 的定義,而在fun 函數(shù)的local namespace 中訪問 var 變量,則會得到fun 私有的var = 'var in fun' 定義。
*args and **kwargs- *args: 把所有的參數(shù)按出現(xiàn)順序打包成一個(gè) list
- **kwargs:把所有 key-value 形式的參數(shù)打包成一個(gè) dict
下面給出一個(gè) *args 的例子:
params_list = (1, 2)params_tupple = (1, 2)def add(x, y): print x + yadd(*params_list)add(*params_tupple) 其運(yùn)行結(jié)果如下:33 **kwargs 的例子:
params = { 'x': 1, 'y': 2}def add(x, y): print x + yadd(**params) 其運(yùn)行結(jié)果如下:3
閉包閉包在維基百科上的定義如下: 在計(jì)算機(jī)科學(xué)中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數(shù)。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體。 下面給出一個(gè)使用閉包實(shí)現(xiàn)的logger factory的例子:
def logger_facroty(prefix='', with_prefix=True): if with_prefix: def logger(msg): print prefix + msg return logger else: def logger(msg): print msg return loggerlogger_with_prefix = logger_facroty('Prefix: ')logger_without_prefix = logger_facroty(with_prefix=False)logger_with_prefix('msg')logger_without_prefix('msg') 其運(yùn)行結(jié)果如下:
Prefix: msgmsg 在上面這個(gè)閉包的例子中,prefix 變量時(shí)所謂的自由變量,其在 return logger 執(zhí)行完畢后,便脫離了創(chuàng)建它的環(huán)境logger_factory,但因?yàn)槠浔?strong>logger_factory 中定義的 logger 函數(shù)所引用,其生命周期將至少和 logger 函數(shù)相同。這樣,在 logger 中就可以引用到logger_factory 作用域內(nèi)的變量prefix。 將閉包與 namespace 結(jié)合起來:var = 'var in global'def fun_outer(): var = 'var in fun_outer' unused_var = 'this var is not used in fun_inner' print 'fun_outer: ' + var print 'fun_outer: ' + str(locals()) print 'fun_outer: ' + str(id(var)) def fun_inner(): print 'fun_inner: ' + var print 'fun_inner: ' + str(locals()) print 'fun_inner: ' + str(id(var)) return fun_innerfun_outer()() 其運(yùn)行結(jié)果如下:
fun_outer: var in fun_outerfun_outer: {'var': 'var in fun_outer', 'unused_var': 'this var is not used in fun_inner'}fun_outer: 140228141915584fun_inner: var in fun_outerfun_inner: {'var': 'var in fun_outer'}fun_inner: 140228141915584 在這個(gè)例子中,當(dāng) fun_outer 被定義時(shí),其內(nèi)部的定義的 fun_inner 函數(shù)對 print 'fun_inner: ' + var中所引用的var 變量進(jìn)行搜索,發(fā)現(xiàn)第一個(gè)被搜索到的 var 定義在 fun_outer 的local namespace 中,因此使用此定義,通過 print 'fun_outer: ' + str(id(var)) 和 print 'fun_inner: ' + str(id(var)),當(dāng)var 超出fun_outer 的作用域后,依然存活,而 fun_outer 中的unused_var 變量由于沒有被fun_inner 所引用,因此會被 GC。
探索裝飾器定義點(diǎn)擊打開裝飾器在維基百科上的定義鏈接如下: A decorator is any callable Python object that is used to modify a function, method or class definition.
基本語法語法糖@bardef foo(): print 'foo' 其等價(jià)于:
def foo(): print 'foo'foo = bar(foo)
無參數(shù)裝飾器def foo(func): print 'decorator foo' return func@foodef bar(): print 'bar'bar() foo 函數(shù)被用作裝飾器,其本身接收一個(gè)函數(shù)對象作為參數(shù),然后做一些工作后,返回接收的參數(shù),供外界調(diào)用。
注意: 時(shí)刻牢記 @foo 只是一個(gè)語法糖,其本質(zhì)是 foo = bar(foo)
帶參數(shù)裝飾器import timedef function_performance_statistics(trace_this=True): if trace_this: def performace_statistics_delegate(func): def counter(*args, **kwargs): start = time.clock() func(*args, **kwargs) end =time.clock() print 'used time: %d' % (end - start, ) return counter else: def performace_statistics_delegate(func): return func return performace_statistics_delegate@function_performance_statistics(True)def add(x, y): time.sleep(3) print 'add result: %d' % (x + y,)@function_performance_statistics(False)def mul(x, y=1): print 'mul result: %d' % (x * y,)add(1, 1)mul(10) 上述代碼想要實(shí)現(xiàn)一個(gè)性能分析器,并接收一個(gè)參數(shù),來控制性能分析器是否生效,其運(yùn)行效果如下所示:add result: 2used time: 0mul result: 10 上述代碼中裝飾器的調(diào)用等價(jià)于:
add = function_performance_statistics(True)(add(1, 1))mul = function_performance_statistics(False)(mul(10))
類的裝飾器類的裝飾器不常用,因此只簡單介紹。
def bar(dummy): print 'bar'def inject(cls): cls.bar = bar return cls@injectclass Foo(object): passfoo = Foo()foo.bar() 上述代碼的 inject 裝飾器為類動態(tài)的添加一個(gè) bar 方法,因?yàn)轭愒谡{(diào)用非靜態(tài)方法的時(shí)候會傳進(jìn)一個(gè)self 指針,因此bar 的第一個(gè)參數(shù)我們簡單的忽略即可,其運(yùn)行結(jié)果如下:
bar
類裝飾器類裝飾器相比函數(shù)裝飾器,具有靈活度大,高內(nèi)聚、封裝性等優(yōu)點(diǎn)。其實(shí)現(xiàn)起來主要是靠類內(nèi)部的 __call__ 方法,當(dāng)使用 @ 形式將裝飾器附加到函數(shù)上時(shí),就會調(diào)用此方法,下面時(shí)一個(gè)實(shí)例: class Foo(object): def __init__(self, func): super(Foo, self).__init__() self._func = func def __call__(self): print 'class decorator' self._func()@Foodef bar(): print 'bar'bar() 其運(yùn)行結(jié)果如下:class decoratorbar
內(nèi)置裝飾器Python中內(nèi)置的裝飾器有三個(gè): staticmethod、classmethod 和property
staticmethod 是類靜態(tài)方法,其跟成員方法的區(qū)別是沒有 self 指針,并且可以在類不進(jìn)行實(shí)例化的情況下調(diào)用,下面是一個(gè)實(shí)例,對比靜態(tài)方法和成員方法 class Foo(object): @staticmethod def statc_method(msg): print msg def member_method(self, msg): print msgfoo = Foo()foo.member_method('some msg')foo.statc_method('some msg')Foo.statc_method('some msg') 其運(yùn)行結(jié)果如下:
some msgsome msgsome msg classmethod 與成員方法的區(qū)別在于所接收的第一個(gè)參數(shù)不是 self 類實(shí)例的指針,而是當(dāng)前類的具體類型,下面是一個(gè)實(shí)例:
class Foo(object): @classmethod def class_method(cls): print repr(cls) def member_method(self): print repr(self)foo = Foo()foo.class_method()foo.member_method() 其運(yùn)行結(jié)果如下:
<__main__.Foo object at 0x10a611c50> property 是屬性的意思,即可以通過通過類實(shí)例直接訪問的信息,下面是具體的例子:
class Foo(object): def __init__(self, var): super(Foo, self).__init__() self._var = var @property def var(self): return self._var @var.setter def var(self, var): self._var = varfoo = Foo('var 1')print foo.varfoo.var = 'var 2'print foo.var 注意: 如果將上面的 @var.setter 裝飾器所裝飾的成員函數(shù)去掉,則Foo.var 屬性為只讀屬性,使用 foo.var = 'var 2'進(jìn)行賦值時(shí)會拋出異常,其運(yùn)行結(jié)果如下:
var 1var 2 注意: 如果使用老式的Python類定義,所聲明的屬性不是 read only的,下面代碼說明了這種情況:
class Foo: def __init__(self, var): self._var = var @property def var(self): return self._varfoo = Foo('var 1')print foo.varfoo.var = 'var 2'print foo.var 其運(yùn)行結(jié)果如下:
var 1var 2
調(diào)用順序裝飾器的調(diào)用順序與使用 @ 語法糖聲明的順序相反,如下所示: def decorator_a(func): print 'decorator_a' return funcdef decorator_b(func): print 'decorator_b' return func@decorator_a@decorator_bdef foo(): print 'foo' foo() 其等價(jià)于:
def decorator_a(func): print 'decorator_a' return funcdef decorator_b(func): print 'decorator_b' return funcdef foo(): print 'foo'foo = decorator_a(decorator_b(foo))foo() 通過等價(jià)的調(diào)用形式我們可以看到,按照python的函數(shù)求值序列,decorator_b(fun) 會首先被求值,然后將其結(jié)果作為輸入,傳遞給decorator_a,因此其調(diào)用順序與聲明順序相反。其運(yùn)行結(jié)果如下所示:
decorator_bdecorator_afoo
調(diào)用時(shí)機(jī)裝飾器很好用,那么它什么時(shí)候被調(diào)用?性能開銷怎么樣?會不會有副作用?接下來我們就以幾個(gè)實(shí)例來驗(yàn)證我們的猜想。 首先我們驗(yàn)證一下裝飾器的性能開銷,代碼如下所示: def decorator_a(func): print 'decorator_a' print 'func id: ' + str(id(func)) return funcdef decorator_b(func): print 'decorator_b' print 'func id: ' + str(id(func)) return funcprint 'Begin declare foo with decorators'@decorator_a@decorator_bdef foo(): print 'foo'print 'End declare foo with decorators'print 'First call foo'foo()print 'Second call foo'foo()print 'Function infos'print 'decorator_a id: ' + str(id(decorator_a))print 'decorator_b id: ' + str(id(decorator_b))print 'fooid : ' + str(id(foo)) 其運(yùn)行結(jié)果如下:
Begin declare foo with decoratorsdecorator_bfunc id: 140124961990488decorator_afunc id: 140124961990488End declare foo with decoratorsFirst call foofooSecond call foofooFunction infosdecorator_a id: 140124961954464decorator_b id: 140124961988808fooid : 140124961990488 在運(yùn)行結(jié)果中的:
Begin declare foo with decoratorsdecorator_bfunc id: 140124961990488decorator_afunc id: 140124961990488End declare foo with decorators 證實(shí)了裝飾器的調(diào)用時(shí)機(jī)為: 被裝飾對象定義時(shí) 而運(yùn)行結(jié)果中的:
First call foofooSecond call foofoo 證實(shí)了在相同 .py 文件中,裝飾器對所裝飾的函數(shù)只進(jìn)行一次裝飾,不會每次調(diào)用相應(yīng)函數(shù)時(shí)都重新裝飾,這個(gè)很容易理解,因?yàn)槠浔举|(zhì)等價(jià)于下面的函數(shù)簽名重新綁定:
foo = decorator_a(decorator_b(foo)) 對于跨模塊的調(diào)用,我們編寫如下結(jié)構(gòu)的測試代碼:
.├── common│ ├── decorator.py│ ├── __init__.py│ ├── mod_a│ │ ├── fun_a.py│ │ └── __init__.py│ └── mod_b│ ├── fun_b.py│ └── __init__.py└── test.py 上述所有模塊中的 __init__.py 文件均為: # -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-# common/mod_a/fun_a.pyfrom common.decorator import foodef fun_a(): print 'in common.mod_a.fun_a.fun_a call foo' foo() # -*- coding: utf-8 -*-# common/mod_b/fun_b.pyfrom common.decorator import foodef fun_b(): print 'in common.mod_b.fun_b.fun_b call foo' foo() # -*- coding: utf-8 -*-# common/decorator.pydef decorator_a(func): print 'init decorator_a' return func@decorator_adef foo(): print 'function foo' # -*- coding: utf-8 -*-# test.pyfrom common.mod_a.fun_a import fun_afrom common.mod_b.fun_b import fun_bfun_a()fun_b() 上述代碼通過創(chuàng)建 common.mod_a 和 common.mod_b 兩個(gè)子模塊,并調(diào)用common.decorator 中的foo 函數(shù),來測試跨模塊時(shí)裝飾器的工作情況,運(yùn)行 test.py 的結(jié)果如下所示:
init decorator_ain common.mod_a.fun_a.fun_a call foofunction fooin common.mod_b.fun_b.fun_b call foofunction foo 經(jīng)過上面的驗(yàn)證,可以看出,對于跨模塊的調(diào)用,裝飾器也只會初始化一次,不過這要?dú)w功于 *.pyc,這與本文主題無關(guān),故不詳述。 關(guān)于裝飾器副作用的話題比較大,這不僅僅是裝飾器本身的問題,更多的時(shí)候是我們設(shè)計(jì)上的問題,下面給出一個(gè)初學(xué)裝飾器時(shí)大家都會遇到的一個(gè)問題——丟失函數(shù)元信息:
def decorator_a(func): def inner(*args, **kwargs): res = func(*args, **kwargs) return res return inner@decorator_adef foo(): '''foo doc''' return 'foo result'print 'foo.__module__: ' + str(foo.__module__)print 'foo.__name__: ' + str(foo.__name__)print 'foo.__doc__: ' + str(foo.__doc__)print foo() 其運(yùn)行結(jié)果如下所示:
foo.__module__: __main__foo.__name__: innerfoo.__doc__: Nonefoo result 我們可以看到,在使用 decorator_a 對 foo 函數(shù)進(jìn)行裝飾后,foo 的元信息會丟失,解決方案參見: functools.wraps
多個(gè)裝飾器運(yùn)行期行為前面已經(jīng)講解過裝飾器的調(diào)用順序和調(diào)用時(shí)機(jī),但是被多個(gè)裝飾器裝飾的函數(shù),其運(yùn)行期行為還是有一些細(xì)節(jié)需要說明的,而且很可能其行為會讓你感到驚訝,下面時(shí)一個(gè)實(shí)例:
def tracer(msg): print '[TRACE] %s' % msgdef logger(func): tracer('logger') def inner(username, password): tracer('inner') print 'call %s' % func.__name__ return func(username, password) return innerdef login_debug_helper(show_debug_info=False): tracer('login_debug_helper') def proxy_fun(func): tracer('proxy_fun') def delegate_fun(username, password): tracer('delegate_fun') if show_debug_info: print 'username: %s\npassword: %s' % (username, password) return func(username, password) return delegate_fun return proxy_funprint 'Declaring login_a'@logger@login_debug_helper(show_debug_info=True)def login_a(username, password): tracer('login_a') print 'do some login authentication' return Trueprint 'Call login_a'login_a('mdl', 'pwd') 大家先來看一下運(yùn)行結(jié)果,看看是不是跟自己想象中的一致:
Declaring login_a[TRACE] login_debug_helper[TRACE] proxy_fun[TRACE] loggerCall login_a[TRACE] innercall delegate_fun[TRACE] delegate_funusername: mdlpassword: pwd[TRACE] login_ado some login authentication 首先,裝飾器初始化時(shí)的調(diào)用順序與我們前面講解的一致,如下:
Declaring login_a[TRACE] login_debug_helper[TRACE] proxy_fun[TRACE] logger 然而,接下來,來自 logger 裝飾器中的 inner 函數(shù)首先被執(zhí)行,然后才是login_debug_helper 返回的proxy_fun 中的 delegate_fun 函數(shù)。各位讀者發(fā)現(xiàn)了嗎,運(yùn)行期執(zhí)行login_a 函數(shù)的時(shí)候,裝飾器中返回的函數(shù)的執(zhí)行順序是相反的,難道是我們前面講解的例子有錯(cuò)誤嗎?其實(shí),如果大家的認(rèn)為運(yùn)行期調(diào)用順序應(yīng)該與裝飾器初始化階段的順序一致的話,那說明大家沒有看透這段代碼的調(diào)用流程,下面我來為大家分析一下。
def login_debug_helper(show_debug_info=False): tracer('login_debug_helper') def proxy_fun(func): tracer('proxy_fun') def delegate_fun(username, password): tracer('delegate_fun') if show_debug_info: print 'username: %s\npassword: %s' % (username, password) return func(username, password) return delegate_fun return proxy_fun 當(dāng)裝飾器 login_debug_helper 被調(diào)用時(shí),其等價(jià)于:
login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd') 對于只有 login_debug_helper 的情況,現(xiàn)在就應(yīng)該是執(zhí)行玩login_a輸出結(jié)果的時(shí)刻了,但是如果現(xiàn)在在加上logger 裝飾器的話,那么這個(gè)login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd')就被延遲執(zhí)行,而將login_debug_helper(show_debug_info=True)(login_a) 作為參數(shù)傳遞給 logger,我們令 login_tmp = login_debug_helper(show_debug_info=True)(login_a),則調(diào)用過程等價(jià)于:
login_tmp = login_debug_helper(show_debug_info=True)(login_a)login_a = logger(login_tmp)login_a('mdl', 'pwd') 相信大家看過上面的等價(jià)變換后,已經(jīng)明白問題出在哪里了,如果你還沒有明白,我強(qiáng)烈建議你把這個(gè)例子自己敲一遍,并嘗試用自己的方式進(jìn)行化簡,逐步得出結(jié)論。
一些實(shí)例參考本文主要講解原理性的東西,具體的實(shí)例可以參考下面的鏈接: Python裝飾器實(shí)例:調(diào)用參數(shù)合法性驗(yàn)證 Python裝飾器與面向切面編程 Python裝飾器小結(jié) Python tips: 超時(shí)裝飾器, @timeout decorator python中判斷一個(gè)運(yùn)行時(shí)間過長的函數(shù) python利用裝飾器和threading實(shí)現(xiàn)異步調(diào)用 python輸出指定函數(shù)運(yùn)行時(shí)間的裝飾器 python通過裝飾器和線程限制函數(shù)的執(zhí)行時(shí)間 python裝飾器的一個(gè)妙用 通過 Python 裝飾器實(shí)現(xiàn)DRY(不重復(fù)代碼)原則
參考資料Understanding Python Decorators in 12 Easy Steps Decorators and Functional Python Python Wiki: PythonDecorators Meta-matters: Using decorators for better Python programming Python裝飾器入門(譯) Python裝飾器與面向切面編程 Python 的閉包和裝飾器 Python裝飾器學(xué)習(xí)(九步入門) python 裝飾器和 functools 模塊
|