本文轉載自酷殼http://coolshell.cn/articles/11265.html
Python的修飾器的英文名叫Decorator,當你看到這個英文名的時候,你可能會把其跟Design Pattern裏的Decorator搞混了,其實這是完全不同的兩個東西。雖然好像,他們要乾的事都很相似——都是想要對一個已有的模塊做一些“修飾工作”,所謂修飾工作就是想給現有的模塊加上一些小裝飾(一些小功能,這些小功能可能好多模塊都會用到),但又不讓這個小裝飾(小功能)侵入到原有的模塊中的代碼裏去。但是OO的Decorator簡直就是一場惡夢,不信你就去看看wikipedia上的詞條(Decorator Pattern)裏的UML圖和那些代碼,這就是我在《 從面向對象的設計模式看軟件設計》“餐後甜點”一節中說的,OO鼓勵了——“厚重地膠合和複雜層次”,也是《 如此理解面向對象編程》中所說的“OO的狂熱者們非常害怕處理數據”,Decorator Pattern搞出來的代碼簡直就是OO的反面教程。
Python 的 Decorator在使用上和Java/C#的Annotation很相似,就是在方法名前面加一個@XXX註解來爲這個方法裝飾一些東西。但是,Java/C#的Annotation也很讓人望而卻步,太TMD的複雜了,你要玩它,你需要了解一堆Annotation的類庫文檔,讓人感覺就是在學另外一門語言。
而Python使用了一種相對於Decorator Pattern和Annotation來說非常優雅的方法,這種方法不需要你去掌握什麼複雜的OO模型或是Annotation的各種類庫規定,完全就是語言層面的玩法:一種函數式編程的技巧。如果你看過本站的《函數式編程》,你一定會爲函數式編程的那種“描述你想幹什麼,而不是描述你要怎麼去實現”的編程方式感到暢快。(如果你不瞭解函數式編程,那在讀本文之前,還請你移步去看看《函數式編程》) 好了,我們先來點感性認識,看一個Python修飾器的Hello World的代碼。
Hello World
下面是代碼:
1 2 3 4 5 6 7 8 9 10 11 12 | def hello(fn):
def wrapper():
print "hello, %s" % fn.__name__
fn()
print "goodby, %s" % fn.__name__
return wrapper @hello def foo():
print "i am foo" foo() |
當你運行代碼,你會看到如下輸出:
1 2 3 4 | [chenaho@chenhao-air]$ python hello.py hello, foo i am foo goodby, foo |
你可以看到如下的東西:
1)函數foo前面有個@hello的“註解”,hello就是我們前面定義的函數hello
2)在hello函數中,其需要一個fn的參數(這就用來做回調的函數)
3)hello函數中返回了一個inner函數wrapper,這個wrapper函數回調了傳進來的fn,並在回調前後加了兩條語句。
Decorator 的本質
對於Python的這個@註解語法糖- Syntactic Sugar 來說,當你在用某個@decorator來修飾某個函數func時,如下所示:
1 2 3 | @decorator def func():
pass |
其解釋器會解釋成下面這樣的語句:
1 | func = decorator(func) |
尼瑪,這不就是把一個函數當參數傳到另一個函數中,然後再回調嗎?是的,但是,我們需要注意,那裏還有一個賦值語句,把decorator這個函數的返回值賦值回了原來的func。 根據《函數式編程》中的first class functions中的定義的,你可以把函數當成變量來使用,所以,decorator必需得返回了一個函數出來給func,這就是所謂的higher order function 高階函數,不然,後面當func()調用的時候就會出錯。 就我們上面那個hello.py裏的例子來說,
1 2 3 | @hello def foo():
print "i am foo" |
被解釋成了:
1 | foo = hello(foo) |
是的,這是一條語句,而且還被執行了。你如果不信的話,你可以寫這樣的程序來試試看:
1 2 3 4 5 6 | def fuck(fn):
print "fuck %s!" % fn.__name__[:: - 1 ].upper() @fuck def wfg():
pass |
沒了,就上面這段代碼,沒有調用wfg()的語句,你會發現, fuck函數被調用了,而且還很NB地輸出了我們每個人的心聲!
再回到我們hello.py的那個例子,我們可以看到,hello(foo)返回了wrapper()函數,所以,foo其實變成了wrapper的一個變量,而後面的foo()執行其實變成了wrapper()。
知道這點本質,當你看到有多個decorator或是帶參數的decorator,你也就不會害怕了。
比如:多個decorator
1 2 3 4 | @decorator_one @decorator_two def func():
pass |
相當於:
1 | func = decorator_one(decorator_two(func)) |
比如:帶參數的decorator:
1 2 3 | @decorator (arg1, arg2) def func():
pass |
相當於:
1 | func = decorator(arg1,arg2)(func) |
這意味着decorator(arg1, arg2)這個函數需要返回一個“真正的decorator”。
帶參數及多個Decrorator
我們來看一個有點意義的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def makeHtmlTag(tag, * args, * * kwds):
def real_decorator(fn):
css_class = " class='{0}'" . format (kwds[ "css_class" ]) \
if "css_class" in kwds else ""
def wrapped( * args, * * kwds):
return "<" + tag + css_class + ">" + fn( * args, * * kwds) + "</" + tag + ">"
return wrapped
@makeHtmlTag (tag = "b" , css_class = "bold_css" ) @makeHtmlTag (tag = "i" , css_class = "italic_css" ) def hello():
return "hello world" print hello() # 輸出: # <b class='bold_css'><i class='italic_css'>hello world</i></b> |
在上面這個例子中,我們可以看到:makeHtmlTag有兩個參數。所以,爲了讓 hello = makeHtmlTag(arg1, arg2)(hello) 成功,makeHtmlTag 必需返回一個decorator(這就是爲什麼我們在makeHtmlTag中加入了real_decorator()的原因),這樣一來,我們就可以進入到 decorator 的邏輯中去了—— decorator得返回一個wrapper,wrapper裏回調hello。看似那個makeHtmlTag() 寫得層層疊疊,但是,已經瞭解了本質的我們覺得寫得很自然。
你看,Python的Decorator就是這麼簡單,沒有什麼複雜的東西,你也不需要了解過多的東西,使用起來就是那麼自然、體貼、乾爽、透氣,獨有的速效凹道和完美的吸收軌跡,讓你再也不用爲每個月的那幾天感到焦慮和不安,再加上貼心的護翼設計,量多也不用當心。對不起,我調皮了。
什麼,你覺得上面那個帶參數的Decorator的函數嵌套太多了,你受不了。好吧,沒事,我們看看下面的方法。
class式的 Decorator
首先,先得說一下,decorator的class方式,還是看個示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class myDecorator( object ):
def __init__( self , fn):
print "inside myDecorator.__init__()"
self .fn = fn
def __call__( self ):
self .fn()
@myDecorator def aFunction():
print "inside aFunction()" print "Finished decorating aFunction()" aFunction() # 輸出: # inside myDecorator.__init__() # Finished decorating aFunction() # inside aFunction() # inside myDecorator.__call__() |
上面這個示例展示了,用類的方式聲明一個decorator。我們可以看到這個類中有兩個成員:
1)一個是__init__(),這個方法是在我們給某個函數decorator時被調用,所以,需要有一個fn的參數,也就是被decorator的函數。
2)一個是__call__(),這個方法是在我們調用被decorator函數時被調用的。
上面輸出可以看到整個程序的執行順序。
這看上去要比“函數式”的方式更易讀一些。
下面,我們來看看用類的方式來重寫上面的html.py的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class makeHtmlTagClass( object ):
def __init__( self , tag, css_class = ""):
self ._tag = tag
self ._css_class = " class='{0}'" . format (css_class) \
if css_class ! = " " else " "
def __call__( self , fn):
def wrapped( * args, * * kwargs):
return "<" + self ._tag + self ._css_class + ">" \
+ fn( * args, * * kwargs) + "</" + self ._tag + ">"
@makeHtmlTagClass (tag = "b" , css_class = "bold_css" ) @makeHtmlTagClass (tag = "i" , css_class = "italic_css" ) def hello(name):
return "Hello, {}" . format (name) print hello( "Hao Chen" ) |
上面這段代碼中,我們需要注意這幾點:
1)如果decorator有參數的話,__init__() 成員就不能傳入fn了,而fn是在__call__的時候傳入的。
2)這段代碼還展示了 wrapped(*args, **kwargs) 這種方式來傳遞被decorator函數的參數。(其中:args是一個參數列表,kwargs是參數dict,具體的細節,請參考Python的文檔或是StackOverflow的這個問題,這裏就不展開了)
用Decorator設置函數的調用參數
你有三種方法可以幹這個事:
第一種,通過 **kwargs,這種方法decorator會在kwargs中注入參數。
1 2 3 4 5 6 7 8 9 10 11 | def decorate_A(function):
def wrap_function( * args, * * kwargs):
kwargs[ 'str' ] = 'Hello!'
return function( * args, * * kwargs)
@decorate_A def print_message_A( * args, * * kwargs):
print (kwargs[ 'str' ]) print_message_A() |
第二種,約定好參數,直接修改參數
1 2 3 4 5 6 7 8 9 10 11 | def decorate_B(function):
def wrap_function( * args, * * kwargs):
str = 'Hello!'
return function( str , * args, * * kwargs)
@decorate_B def print_message_B( str , * args, * * kwargs):
print _message_B() |
第三種,通過 *args 注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def decorate_C(function):
def wrap_function( * args, * * kwargs):
str = 'Hello!'
#args.insert(1, str)
args = args + ( str ,)
return function( * args, * * kwargs)
return wrap_function class Printer:
@decorate_C
def print_message( self , str , * args, * * kwargs):
print ( str ) p = Printer() p.print_message() |
Decorator的副作用
到這裏,我相信你應該瞭解了整個Python的decorator的原理了。
相信你也會發現,被decorator的函數其實已經是另外一個函數了,對於最前面那個hello.py的例子來說,如果你查詢一下foo.__name__的話,你會發現其輸出的是“wrapper”,而不是我們期望的“foo”,這會給我們的程序埋一些坑。所以,Python的functool包中提供了一個叫wrap的decorator來消除這樣的副作用。下面是我們新版本的hello.py。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from functools import wraps def hello(fn):
@wraps (fn)
def wrapper():
print "hello, %s" % fn.__name__
fn()
print "goodby, %s" % fn.__name__
@hello def foo():
'''foo help doc'''
print "i am foo"
foo() print foo.__name__ #輸出 foo print foo.__doc__ #輸出 foo help doc |
當然,即使是你用了functools的wraps,也不能完全消除這樣的副作用。
來看下面這個示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | from inspect import getmembers, getargspec from functools import wraps def wraps_decorator(f):
@wraps (f)
def wraps_wrapper( * args, * * kwargs):
return f( * args, * * kwargs)
return wraps_wrapper class SomeClass( object ):
@wraps_decorator
def method( self , x, y):
pass obj = SomeClass() for name, func in getmembers(obj, predicate = inspect.ismethod):
print "Member Name: %s" % name
print "Func Name: %s" % func.func_name
# 輸出: # Member Name: method # Func Name: method # Args: [] |
你會發現,即使是你你用了functools的wraps,你在用getargspec時,參數也不見了。
要修正這一問,我們還得用Python的反射來解決,下面是相關的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 | def get_true_argspec(method):
argspec = inspect.getargspec(method)
args = argspec[ 0 ]
if args and args[ 0 ] = = 'self' :
return argspec
if hasattr (method, '__func__' ):
method = method.__func__
if not hasattr (method, 'func_closure' ) or method.func_closure is None :
raise Exception( "No closure for method." )
method = method.func_closure[ 0 ].cell_contents
return get_true_argspec(method) |
當然,我相信大多數人的程序都不會去getargspec。所以,用functools的wraps應該夠用了。
一些decorator的示例
好了,現在我們來看一下各種decorator的例子:
給函數調用做緩存
這個例實在是太經典了,整個網上都用這個例子做decorator的經典範例,因爲太經典了,所以,我這篇文章也不能免俗。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from functools import wraps def memo(fn):
cache = {}
miss = object ()
@wraps (fn)
def wrapper( * args):
result = cache.get(args, miss)
if result is miss:
result = fn( * args)
cache[args] = result
return result
return wrapper @memo def fib(n):
if n < 2 :
return n
return fib(n - 1 ) + fib(n - 2 ) |
上面這個例子中,是一個斐波拉契數例的遞歸算法。我們知道,這個遞歸是相當沒有效率的,因爲會重複調用。比如:我們要計算fib(5),於是其分解成fib(4) + fib(3),而fib(4)分解成fib(3)+fib(2),fib(3)又分解成fib(2)+fib(1)…… 你可看到,基本上來說,fib(3), fib(2), fib(1)在整個遞歸過程中被調用了兩次。
而我們用decorator,在調用函數前查詢一下緩存,如果沒有才調用了,有了就從緩存中返回值。一下子,這個遞歸從二叉樹式的遞歸成了線性的遞歸。
Profiler的例子
這個例子沒什麼高深的,就是實用一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import cProfile, pstats, StringIO def profiler(func):
def wrapper( * args, * * kwargs):
datafn = func.__name__ + ".profile" # Name the data file
prof = cProfile.Profile()
retval = prof.runcall(func, * args, * * kwargs)
#prof.dump_stats(datafn)
s = StringIO.StringIO()
sortby = 'cumulative'
ps = pstats.Stats(prof, stream = s).sort_stats(sortby)
ps.print_stats()
print s.getvalue()
return retval
return wrapper |
註冊回調函數
下面這個示例展示了通過URL的路由來調用相關注冊的函數示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class MyApp():
def __init__( self ):
self .func_map = {}
def register( self , name):
def func_wrapper(func):
self .func_map[name] = func
return func
return func_wrapper
def call_method( self , name = None ):
func = self .func_map.get(name, None )
if func is None :
raise Exception( "No function registered against - " + str (name))
return func() app = MyApp() @app .register( '/' ) def main_page_func():
return "This is the main page." @app .register( '/next_page' ) def next_page_func():
return "This is the next page." print app.call_method( '/' ) print app.call_method( '/next_page' ) |
注意:
1)上面這個示例中,用類的實例來做decorator。
2)decorator類中沒有__call__(),但是wrapper返回了原函數。所以,原函數沒有發生任何變化。
給函數打日誌
下面這個示例演示了一個logger的decorator,這個decorator輸出了函數名,參數,返回值,和運行時間。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from functools import wraps def logger(fn):
@wraps (fn)
def wrapper( * args, * * kwargs):
ts = time.time()
result = fn( * args, * * kwargs)
te = time.time()
print "function = {0}" . format (fn.__name__)
print " arguments = {0} {1}" . format (args, kwargs)
print " return = {0}" . format (result)
print " time = %.6f sec" % (te - ts)
return result
return wrapper @logger def multipy(x, y):
return x * y @logger def sum_num(n):
s = 0
for i in xrange (n + 1 ):
s + = i
return s print multipy( 2 , 10 ) print sum_num( 100 ) print sum_num( 10000000 ) |
上面那個打日誌還是有點粗糙,讓我們看一個更好一點的(帶log level參數的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import inspect def get_line_number():
return inspect.currentframe().f_back.f_back.f_lineno def logger(loglevel):
def log_decorator(fn):
@wraps (fn)
def wrapper( * args, * * kwargs):
ts = time.time()
result = fn( * args, * * kwargs)
te = time.time()
print "function = " + fn.__name__,
print " arguments = {0} {1}" . format (args, kwargs)
print " return = {0}" . format (result)
print " time = %.6f sec" % (te - ts)
if (loglevel = = 'debug' ):
print " called_from_line : " + str (get_line_number())
return result
return wrapper
return log_decorator |
但是,上面這個帶log level參數的有兩具不好的地方,
1) loglevel不是debug的時候,還是要計算函數調用的時間。
2) 不同level的要寫在一起,不易讀。
我們再接着改進:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import inspect def advance_logger(loglevel):
def get_line_number():
return inspect.currentframe().f_back.f_back.f_lineno
def _basic_log(fn, result, * args, * * kwargs):
print "function = " + fn.__name__,
print " arguments = {0} {1}" . format (args, kwargs)
print " return = {0}" . format (result)
def info_log_decorator(fn):
@wraps (fn)
def wrapper( * args, * * kwargs):
result = fn( * args, * * kwargs)
_basic_log(fn, result, args, kwargs)
return wrapper
def debug_log_decorator(fn):
@wraps (fn)
def wrapper( * args, * * kwargs):
ts = time.time()
result = fn( * args, * * kwargs)
te = time.time()
_basic_log(fn, result, args, kwargs)
print " time = %.6f sec" % (te - ts)
print " called_from_line : " + str (get_line_number())
return wrapper
if loglevel is "debug" :
return debug_log_decorator
else :
return info_log_decorator |
你可以看到兩點,
1)我們分了兩個log level,一個是info的,一個是debug的,然後我們在外尾根據不同的參數返回不同的decorator。
2)我們把info和debug中的相同的代碼抽到了一個叫_basic_log的函數裏,DRY原則。
一個MySQL的Decorator
下面這個decorator是我在工作中用到的代碼,我簡化了一下,把DB連接池的代碼去掉了,這樣能簡單點,方便閱讀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | import umysql from functools import wraps class Configuraion:
def __init__( self , env):
if env = = "Prod" :
self .host = "coolshell.cn"
self .port = 3306
self .db = "coolshell"
self .user = "coolshell"
self .passwd = "fuckgfw"
elif env = = "Test" :
self .host = 'localhost'
self .port = 3300
self .user = 'coolshell'
self .db = 'coolshell'
self .passwd = 'fuckgfw' def mysql(sql):
_conf = Configuraion(env = "Prod" )
def on_sql_error(err):
print err
sys.exit( - 1 )
def handle_sql_result(rs):
if rs.rows > 0 :
fieldnames = [f[ 0 ] for f in rs.fields]
return [ dict ( zip (fieldnames, r)) for r in rs.rows]
else :
return []
def decorator(fn):
@wraps (fn)
def wrapper( * args, * * kwargs):
mysqlconn = umysql.Connection()
mysqlconn.settimeout( 5 )
mysqlconn.connect(_conf.host, _conf.port, _conf.user, \
_conf.passwd, _conf.db, True , 'utf8' )
try :
rs = mysqlconn.query(sql, {})
except umysql.Error as e:
on_sql_error(e)
data = handle_sql_result(rs)
kwargs[ "data" ] = data
result = fn( * args, * * kwargs)
mysqlconn.close()
return result
return wrapper
return decorator @mysql (sql = "select * from coolshell" ) def get_coolshell(data):
... ...
... .. |
線程異步
下面量個非常簡單的異步執行的decorator,注意,異步處理並不簡單,下面只是一個示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | from threading import Thread from functools import wraps def async(func):
@wraps (func)
def async_func( * args, * * kwargs):
func_hl = Thread(target = func, args = args, kwargs = kwargs)
func_hl.start()
return func_hl
return async_func if __name__ = = '__main__' :
from time import sleep
@async
def print_somedata():
print 'starting print_somedata'
sleep( 2 )
print 'print_somedata: 2 sec passed'
sleep( 2 )
print 'print_somedata: 2 sec passed'
sleep( 2 )
print 'finished print_somedata'
def main():
print_somedata()
print 'back in main'
print_somedata()
print 'back in main'
main() |
其它
關於更多的示例,你可以參看: Python Decorator Library
關於Python Decroator的各種提案,可以參看:Python Decorator Proposals
(全文完)