淺談Python裝飾器
淺談Python裝飾器
By 馬冬亮(凝霜 Loki)
一個人的戰爭(http://blog.csdn.net/MDL13412)
前置知識
一級物件
Python將一切視為 objec t的子類,即一切都是物件,因此函式可以像變數一樣被指向和傳遞,我們來看下面的例子:
其執行結果如下:def foo(): pass print issubclass(foo.__class__, object)
上述程式碼說明了Python 中的函式是 object 的子類,下面讓我們看函式被當作引數傳遞時的效果:True
其執行結果如下:def foo(func): func() def bar(): print "bar" foo(bar)
bar
Python中的namespace
Python中通過提供 namespace 來實現重名函式/方法、變數等資訊的識別,其一共有三種 namespace,分別為:
- local namespace: 作用範圍為當前函式或者類方法
- global namespace: 作用範圍為當前模組
- build-in namespace: 作用範圍為所有模組
當函式/方法、變數等資訊發生重名時,Python會按照 local namespace -> global namespace -> build-in namespace的順序搜尋使用者所需元素,並且以第一個找到此元素的 namespace 為準。
下面以系統的 build-in 函式 str 為例進行說明:
首先定義三個 global namespace 的函式 str、foo 和bar,然後在 foo 函式中定義一個內嵌的 local namespace 的函式str,然後在函式 foo 和 bar 中分別呼叫 str("dummy"),其執行結果如下所示:def str(s): print "global str()" def foo(): def str(s): print "closure str()" str("dummy") def bar(): str("dummy") foo() bar()
通過編碼實驗,我們可以看到:closure str() global str()
下面我們使用Python內建的 `ocals() 和 globals() 函式檢視不同 namespace 中的元素定義:
- foo 中呼叫 str 函式時,首先搜尋 local namespace,並且成功找到了所需的函式,停止搜尋,使用此namespace 中的定義
- bar 中呼叫 str 函式時,首先搜尋 local namespace,但是沒有找到str 方法的定義,因此繼續搜尋 global namespace,併成功找到了 str 的定義,停止搜尋,並使用此定義
執行結果如下:var = "var in global" def fun(): var = "var in fun" print "fun: " + str(locals()) print "globals: " + str(globals()) fun()
通過執行結果,我們看到了 fun 定義了 local namespace 的變數var,在 global namespace 有一個全域性的 var 變數,那麼當在global namespace 中直接訪問 var 變數的時候,將會得到 var = "var in global" 的定義,而在fun 函式的 local namespace 中訪問 var 變數,則會得到fun 私有的 var = "var in fun" 定義。globals: {'__builtins__': <module '__builtin__' (built-in)>, '__file__': 'a.py', '__package__': None, 'fun': <function fun at 0x7f2ca74f66e0>, 'var': 'var in global', '__name__': '__main__', '__doc__': None} fun: {'var': 'var in fun'}
*args and **kwargs
- *args: 把所有的引數按出現順序打包成一個 list
- **kwargs:把所有 key-value 形式的引數打包成一個 dict
下面給出一個 *args 的例子:
其執行結果如下:params_list = (1, 2) params_tupple = (1, 2) def add(x, y): print x + y add(*params_list) add(*params_tupple)
**kwargs 的例子:3 3
其執行結果如下:params = { 'x': 1, 'y': 2 } def add(x, y): print x + y add(**params)
3
閉包
下面給出一個使用閉包實現的logger factory的例子:閉包在維基百科上的定義如下: 在電腦科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。
其執行結果如下: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 logger logger_with_prefix = logger_facroty("Prefix: ") logger_without_prefix = logger_facroty(with_prefix=False) logger_with_prefix("msg") logger_without_prefix("msg")
在上面這個閉包的例子中,prefix 變數時所謂的自由變數,其在 return logger 執行完畢後,便脫離了建立它的環境logger_factory,但因為其被 logger_factory 中定義的 logger 函式所引用,其生命週期將至少和 logger 函式相同。這樣,在 logger 中就可以引用到logger_factory 作用域內的變數 prefix。Prefix: msg msg
將閉包與 namespace 結合起來:其執行結果如下: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_inner fun_outer()()
在這個例子中,當 fun_outer 被定義時,其內部的定義的 fun_inner 函式對 print "fun_inner: " + var中所引用的 var 變數進行搜尋,發現第一個被搜尋到的 var 定義在 fun_outer 的 local namespace 中,因此使用此定義,通過 print "fun_outer: " + str(id(var)) 和 print "fun_inner: " + str(id(var)),當var 超出 fun_outer 的作用域後,依然存活,而 fun_outer 中的unused_var 變數由於沒有被 fun_inner 所引用,因此會被 GC。fun_outer: var in fun_outer fun_outer: {'var': 'var in fun_outer', 'unused_var': 'this var is not used in fun_inner'} fun_outer: 140228141915584 fun_inner: var in fun_outer fun_inner: {'var': 'var in fun_outer'} fun_inner: 140228141915584
探索裝飾器
定義
點選開啟裝飾器在維基百科上的定義連結如下: A decorator is any callable Python object that is used to modify a function, method or class definition.
基本語法
語法糖
其等價於:@bar def foo(): print "foo"
def foo(): print "foo" foo = bar(foo)
無引數裝飾器
foo 函式被用作裝飾器,其本身接收一個函式物件作為引數,然後做一些工作後,返回接收的引數,供外界呼叫。def foo(func): print 'decorator foo' return func @foo def bar(): print 'bar' bar()
注意: 時刻牢記 @foo 只是一個語法糖,其本質是 foo = bar(foo)
帶引數裝飾器
上述程式碼想要實現一個效能分析器,並接收一個引數,來控制效能分析器是否生效,其執行效果如下所示:import time def 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)
上述程式碼中裝飾器的呼叫等價於:add result: 2 used time: 0 mul result: 10
add = function_performance_statistics(True)(add(1, 1)) mul = function_performance_statistics(False)(mul(10))
類的裝飾器
類的裝飾器不常用,因此只簡單介紹。
上述程式碼的 inject 裝飾器為類動態的新增一個 bar 方法,因為類在呼叫非靜態方法的時候會傳進一個self 指標,因此 bar 的第一個引數我們簡單的忽略即可,其執行結果如下:def bar(dummy): print 'bar' def inject(cls): cls.bar = bar return cls @inject class Foo(object): pass foo = Foo() foo.bar()
bar
類裝飾器
類裝飾器相比函式裝飾器,具有靈活度大,高內聚、封裝性等優點。其實現起來主要是靠類內部的 __call__ 方法,當使用 @ 形式將裝飾器附加到函式上時,就會呼叫此方法,下面時一個例項:
其執行結果如下:class Foo(object): def __init__(self, func): super(Foo, self).__init__() self._func = func def __call__(self): print 'class decorator' self._func() @Foo def bar(): print 'bar' bar()
class decorator bar
內建裝飾器
Python中內建的裝飾器有三個: staticmethod、classmethod 和property
staticmethod 是類靜態方法,其跟成員方法的區別是沒有 self 指標,並且可以在類不進行例項化的情況下呼叫,下面是一個例項,對比靜態方法和成員方法
其執行結果如下:class Foo(object): @staticmethod def statc_method(msg): print msg def member_method(self, msg): print msg foo = Foo() foo.member_method('some msg') foo.statc_method('some msg') Foo.statc_method('some msg')
classmethod 與成員方法的區別在於所接收的第一個引數不是 self 類例項的指標,而是當前類的具體型別,下面是一個例項:some msg some msg some msg
其執行結果如下: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()
property 是屬性的意思,即可以通過通過類例項直接訪問的資訊,下面是具體的例子:<class '__main__.Foo'> <__main__.Foo object at 0x10a611c50>
注意: 如果將上面的 @var.setter 裝飾器所裝飾的成員函式去掉,則Foo.var 屬性為只讀屬性,使用 foo.var = 'var 2' 進行賦值時會丟擲異常,其執行結果如下: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 = var foo = Foo('var 1') print foo.var foo.var = 'var 2' print foo.var
注意: 如果使用老式的Python類定義,所宣告的屬性不是 read only的,下面程式碼說明了這種情況:var 1 var 2
其執行結果如下:class Foo: def __init__(self, var): self._var = var @property def var(self): return self._var foo = Foo('var 1') print foo.var foo.var = 'var 2' print foo.var
var 1 var 2
呼叫順序
裝飾器的呼叫順序與使用 @ 語法糖宣告的順序相反,如下所示:
其等價於:def decorator_a(func): print "decorator_a" return func def decorator_b(func): print "decorator_b" return func @decorator_a @decorator_b def foo(): print "foo" foo()
通過等價的呼叫形式我們可以看到,按照python的函式求值序列,decorator_b(fun) 會首先被求值,然後將其結果作為輸入,傳遞給decorator_a,因此其呼叫順序與宣告順序相反。其執行結果如下所示:def decorator_a(func): print "decorator_a" return func def decorator_b(func): print "decorator_b" return func def foo(): print "foo" foo = decorator_a(decorator_b(foo)) foo()
decorator_b decorator_a foo
呼叫時機
裝飾器很好用,那麼它什麼時候被呼叫?效能開銷怎麼樣?會不會有副作用?接下來我們就以幾個例項來驗證我們的猜想。
首先我們驗證一下裝飾器的效能開銷,程式碼如下所示:其執行結果如下:def decorator_a(func): print "decorator_a" print 'func id: ' + str(id(func)) return func def decorator_b(func): print "decorator_b" print 'func id: ' + str(id(func)) return func print 'Begin declare foo with decorators' @decorator_a @decorator_b def 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))
在執行結果中的:Begin declare foo with decorators decorator_b func id: 140124961990488 decorator_a func id: 140124961990488 End declare foo with decorators First call foo foo Second call foo foo Function infos decorator_a id: 140124961954464 decorator_b id: 140124961988808 fooid : 140124961990488
證實了裝飾器的呼叫時機為: 被裝飾物件定義時Begin declare foo with decorators decorator_b func id: 140124961990488 decorator_a func id: 140124961990488 End declare foo with decorators
而執行結果中的:證實了在相同 .py 檔案中,裝飾器對所裝飾的函式只進行一次裝飾,不會每次呼叫相應函式時都重新裝飾,這個很容易理解,因為其本質等價於下面的函式簽名重新繫結:First call foo foo Second call foo foo
對於跨模組的呼叫,我們編寫如下結構的測試程式碼:foo = decorator_a(decorator_b(foo))
上述所有模組中的 __init__.py 檔案均為: # -*- coding: utf-8 -*-. ├── common │ ├── decorator.py │ ├── __init__.py │ ├── mod_a │ │ ├── fun_a.py │ │ └── __init__.py │ └── mod_b │ ├── fun_b.py │ └── __init__.py └── test.py
# -*- coding: utf-8 -*- # common/mod_a/fun_a.py from common.decorator import foo def fun_a(): print 'in common.mod_a.fun_a.fun_a call foo' foo()
# -*- coding: utf-8 -*- # common/mod_b/fun_b.py from common.decorator import foo def fun_b(): print 'in common.mod_b.fun_b.fun_b call foo' foo()
# -*- coding: utf-8 -*- # common/decorator.py def decorator_a(func): print 'init decorator_a' return func @decorator_a def foo(): print 'function foo'
上述程式碼通過建立 common.mod_a 和 common.mod_b 兩個子模組,並呼叫common.decorator 中的 foo 函式,來測試跨模組時裝飾器的工作情況,執行 test.py 的結果如下所示:# -*- coding: utf-8 -*- # test.py from common.mod_a.fun_a import fun_a from common.mod_b.fun_b import fun_b fun_a() fun_b()
經過上面的驗證,可以看出,對於跨模組的呼叫,裝飾器也只會初始化一次,不過這要歸功於 *.pyc,這與本文主題無關,故不詳述。init decorator_a in common.mod_a.fun_a.fun_a call foo function foo in common.mod_b.fun_b.fun_b call foo function foo
關於裝飾器副作用的話題比較大,這不僅僅是裝飾器本身的問題,更多的時候是我們設計上的問題,下面給出一個初學裝飾器時大家都會遇到的一個問題——丟失函式元資訊:其執行結果如下所示:def decorator_a(func): def inner(*args, **kwargs): res = func(*args, **kwargs) return res return inner @decorator_a def 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()
我們可以看到,在使用 decorator_a 對 foo 函式進行裝飾後,foo 的元資訊會丟失,解決方案參見: functools.wrapsfoo.__module__: __main__ foo.__name__: inner foo.__doc__: None foo result
多個裝飾器執行期行為
前面已經講解過裝飾器的呼叫順序和呼叫時機,但是被多個裝飾器裝飾的函式,其執行期行為還是有一些細節需要說明的,而且很可能其行為會讓你感到驚訝,下面時一個例項:
大家先來看一下執行結果,看看是不是跟自己想象中的一致:def tracer(msg): print "[TRACE] %s" % msg def logger(func): tracer("logger") def inner(username, password): tracer("inner") print "call %s" % func.__name__ return func(username, password) return inner 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 print '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 True print 'Call login_a' login_a("mdl", "pwd")
首先,裝飾器初始化時的呼叫順序與我們前面講解的一致,如下:Declaring login_a [TRACE] login_debug_helper [TRACE] proxy_fun [TRACE] logger Call login_a [TRACE] inner call delegate_fun [TRACE] delegate_fun username: mdl password: pwd [TRACE] login_a do some login authentication
然而,接下來,來自 logger 裝飾器中的 inner 函式首先被執行,然後才是login_debug_helper 返回的 proxy_fun 中的 delegate_fun 函式。各位讀者發現了嗎,執行期執行login_a 函式的時候,裝飾器中返回的函式的執行順序是相反的,難道是我們前面講解的例子有錯誤嗎?其實,如果大家的認為執行期呼叫順序應該與裝飾器初始化階段的順序一致的話,那說明大家沒有看透這段程式碼的呼叫流程,下面我來為大家分析一下。Declaring login_a [TRACE] login_debug_helper [TRACE] proxy_fun [TRACE] logger
當裝飾器 login_debug_helper 被呼叫時,其等價於: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
對於只有 login_debug_helper 的情況,現在就應該是執行玩login_a輸出結果的時刻了,但是如果現在在加上logger 裝飾器的話,那麼這個 login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd')就被延遲執行,而將 login_debug_helper(show_debug_info=True)(login_a) 作為引數傳遞給 logger,我們令 login_tmp = login_debug_helper(show_debug_info=True)(login_a),則呼叫過程等價於:login_debug_helper(show_debug_info=True)(login_a)('mdl', 'pwd')
相信大家看過上面的等價變換後,已經明白問題出在哪裡了,如果你還沒有明白,我強烈建議你把這個例子自己敲一遍,並嘗試用自己的方式進行化簡,逐步得出結論。login_tmp = login_debug_helper(show_debug_info=True)(login_a) login_a = logger(login_tmp) login_a('mdl', 'pwd')
一些例項參考
本文主要講解原理性的東西,具體的例項可以參考下面的連結:
Python裝飾器例項:呼叫引數合法性驗證
參考資料
Understanding Python Decorators in 12 Easy Steps
Decorators and Functional Python
Meta-matters: Using decorators for better Python programming
相關文章
- 【Python】淺談裝飾器Python
- 淺談TypeScript型別、介面、裝飾器TypeScript型別
- 粗淺聊聊Python裝飾器Python
- 談一談Python中的裝飾器Python
- python裝飾器2:類裝飾器Python
- Python裝飾器探究——裝飾器引數Python
- 淺談ES7的修飾器
- Python 裝飾器Python
- Python裝飾器Python
- 裝飾器 pythonPython
- Python 裝飾器裝飾類中的方法Python
- 通俗易懂的談談裝飾器模式模式
- Python裝飾器模式Python模式
- python的裝飾器Python
- 1.5.3 Python裝飾器Python
- Python 裝飾器(一)Python
- Python 裝飾器原理Python
- 草根學Python(十六) 裝飾器(逐步演化成裝飾器)Python
- python 之裝飾器(decorator)Python
- Python深入05 裝飾器Python
- Python裝飾器詳解Python
- Python中的裝飾器Python
- 初識Python裝飾器Python
- Python 裝飾器的理解Python
- python裝飾器介紹Python
- Python3 裝飾器解析Python
- Python 語法之裝飾器Python
- Python裝飾器高階用法Python
- python裝飾器入門探究Python
- python 裝飾器 part2Python
- python裝飾器有哪些作用Python
- python裝飾器是什麼Python
- Python 裝飾器簡單示例Python
- python中裝飾器的原理Python
- Python閉包與裝飾器Python
- Python之函式裝飾器Python函式
- Python深入分享之裝飾器Python
- Python裝飾器的前世今生Python