Python Decorator
由于没时间编写,就把几张写的不错的文章摘录整合到一起。原文地址:Python Decorator python decorator 心得体会 可爱的 Python: Decorator 简化元编程
Python 之美 --Decorator 深入详解 (一) Python Decorators(二):Decorator 参数 Python Decorator 初体验
Python 的修饰器的英文名叫 Decorator,当你看到这个英文名的时候,你可能会把其跟 Design Pattern 里的 Decorator 搞混了,其实这是完全不同的两个东西。在认识装饰器之前,我们先来点感性认识,看一个 Python 修饰器的 Hello World 的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 下面是代码:文件名:hello.py 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 | 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 return real_decorator @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() 写得层层叠叠,但是,已经了解了本质的我们觉得写得很自然。
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 初识Decorator Decorator,修饰符,是在Python2. 4 中增加的功能,也是pythoner实现元编程的最新方式,同时它也是最简单的元编程方式。为什么是“最简单”呢?是的,其实在Decorator之前就已经 有 classmethod ()和 staticmethod ()内置函数,但他们的缺陷是会导致函数名的重复使用(可以看看David Mertz的Charming Python: Decorators make magic easy ), 以下是摘自他本人的原文: class C: def foo( cls , y): print "classmethod" , cls , y foo = classmethod (foo) 是的, classmethod 做的只是函数转换,但是它却让foo这个名字另外出现了 2 次。记得有一句话是:人类因懒惰而进步。Decorator的诞生,让foo少出现 2 次。 class C: @classmethod def foo( cls , y): print "classmethod" , cls , y 读者也许已经想到Decorator在python中是怎么处理的了(如果还没头绪的,强烈建议先去看看limodou写的Decorator学习笔记 )。下面我列出 4 种用法。 单个 Decorator,不带参数 设想一个情景,你平时去买衣服的时候,跟售货员是怎么对话的呢?售货员会先向你问好,然后你会试穿某件你喜爱的衣服。 def salesgirl(method): def serve( * args): print "Salesgirl:Hello, what do you want?" , method.__name__ method( * args) return serve @salesgirl def try_this_shirt(size): if size < 35 : print "I: %d inches is to small to me" % (size) else : print "I:%d inches is just enough" % (size) try_this_shirt( 38 ) 结果是: Salesgirl:Hello, what do you want? try_this_shirt I: 38 inches is just enough 这只是一个太简单的例子,以至一些“细节”没有处理好,你试穿完了好歹也告诉salesgirl到底要不要买啊。。。这样try_this_shirt方法需要改成带返回值 (假设是 bool 类型, True 就是要买, False 就是不想买),那么salesgirl中的serve也应该带返回值,并且返回值就是 method( * args)。 修改后的salesgirl def salesgirl(method): def serve( * args): print "Salesgirl:Hello, what do you want?" , method.__name__ return method( * args) return serve @salesgirl def try_this_shirt(size): if size < 35 : print "I: %d inches is to small to me" % (size) return False else : print "I:%d inches is just enough" % (size) return True result = try_this_shirt( 38 ) print "Mum:do you want to buy this?" , result 结果是: Salesgirl:Hello, what do you want? try_this_shirt I: 38 inches is just enough Mum:do you want to buy this? True 现在我们的salesgirl还不算合格,她只会给客人打招呼,但是客人要是买衣服了,也不会给他报价;客人不买的话,也应该推荐其他款式! 会报价的salesgirl: def salesgirl(method): def serve( * args): print "Salesgirl:Hello, what do you want?" , method.__name__ result = method( * args) if result: print "Salesgirl: This shirt is 50$." else : print "Salesgirl: Well, how about trying another style?" return result return serve @salesgirl def try_this_shirt(size): if size < 35 : print "I: %d inches is to small to me" % (size) return False else : print "I:%d inches is just enough" % (size) return True result = try_this_shirt( 38 ) print "Mum:do you want to buy this?" , result 结果是: Salesgirl:Hello, what do you want? try_this_shirt I: 38 inches is just enough Salesgirl: This shirt is 50 $. Mum:do you want to buy this? True 这样的salesgirl总算是合格了,但离出色还很远,称职的salesgirl是应该对熟客让利,老用户总得有点好处吧? 单个 Decorator,带参数 会报价并且带折扣的salesgirl: def salesgirl(discount): def expense(method): def serve( * args): print "Salesgirl:Hello, what do you want?" , method.__name__ result = method( * args) if result: print "Salesgirl: This shirt is 50$.As an old user, we promised to discount at %d%%" % (discount) else : print "Salesgirl: Well, how about trying another style?" return result return serve return expense @salesgirl ( 50 ) def try_this_shirt(size): if size < 35 : print "I: %d inches is to small to me" % (size) return False else : print "I:%d inches is just enough" % (size) return True result = try_this_shirt( 38 ) print "Mum:do you want to buy this?" , result 结果是: Salesgirl:Hello, what do you want? try_this_shirt I: 38 inches is just enough Salesgirl: This shirt is 50 $.As an old user, we promised to discount at 50 % Mum:do you want to buy this? True 这里定义的salesgirl是会给客户 50 % 的折扣,因为salesgirl描述符是带参数,而参数就是折扣。如果你是第一次看到这个 salesgirl, 会被她里面嵌套的 2 个方法而感到意外,没关系,当你习惯了Decorator之后,一切都变得很亲切啦 |
你看,Python 的 Decorator 就是这么简单,没有什么复杂的东西,你也不需要了解过多的东西,使用起来就是那么自然、体贴,但是你觉得上面那个带参数的 Decorator 的函数嵌套太多了,你受不了。好吧,没事,我们看看下面的方法。
class 式的 Decorator(重点)
1、首先,先得说一下,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() print "inside myDecorator.__call__()" @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 函数时被调用的。
上面输出可以看到整个程序的执行顺序。这看上去要比“函数式”的方式更易读一些。
2、下面,我们来看看用类的方式来重写上面的 html.py 的代码: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 + ">" return wrapped @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 的这个问题,这里就不展开了)
3、用 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) return wrap_function @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) return wrap_function @decorate_B def print_message_B( str , * args, * * kwargs): print ( str ) 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 19 | 文件名:hello.py from functools import wraps def hello(fn): @wraps (fn) def wrapper(): print "hello, %s" % fn.__name__ fn() print "goodby, %s" % fn.__name__ return wrapper @hello def foo(): '''foo help doc''' print "i am foo" pass 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 print "Args: %s" % getargspec(func)[ 0 ] # 输出: # 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 | mport 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() |