在上一篇文章中我们提到了闭包,也就是将函数作为返回值返回。闭包搞懂了之后,接下来的内容就很简单了。
在定义了许多函数之后,我们希望扩展这些函数的功能,譬如在函数调用前后自动打印日志,但如果是一些通用的功能,修改每一个函数又会显得比较麻烦。最好的方法就是定义一个装饰器,给每个函数增加功能。这种在代码运行期间动态增加函数功能的方式,成为装饰器(Decorator)
一、初始函数
>>> from datetime import datetime
>>> def now():
print(datetime.now())
>>> now()
2016-05-31 17:04:40.946448
二、增加功能
>>> def now():
print('run %s().' % now.__name__)
print(datetime.now())
print('run %s() finished.' % now.__name__)
>>> now()
run now().
2016-05-31 17:13:15.216500
run now() finished.
这是最普通的增加函数功能的方式了,但如果将它变成通用功能呢?使用装饰器!
三、装饰器
>>> def decorator(fun):
def wrapper(*args,**kw):
print('run %s().' % fun.__name__)
outcome = fun(*args,**kw)
print('run %s() finished.' % fun.__name__)
return outcome
return wrapper
>>> now = decorator(now)
>>> now()
run now().
2016-05-31 17:15:08.001145
run now() finished.
这是装饰器最直观的表示了,定义一个装饰器函数,然后将自己的函数传入,输出的新函数即添加了新功能。
本质上来看,装饰器就是一个高阶函数。由于其接收参数为(*args,**kw)
,所以能够接受任何形式的调用。在wrapper函数内,调用传入的fun()
函数之外,就可以定义新的功能。
但是now = decorator(now)
这样的方式似乎很麻烦,所以Python提供了装饰器的语法糖@,使得能够简便地使用装饰器。
四、语法糖
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
>>> def decorator(fun):
def wrapper(*args,**kw):
print('run %s().' % fun.__name__)
outcome = fun(*args,**kw)
print('run %s() finished.' % fun.__name__)
return outcome
return wrapper
>>> @decorator
def now():
print(datetime.now())
>>> now()
run now().
2016-05-31 17:10:32.217998
run now() finished.
只需要把@decorator
放在函数定义的顶端,再调用函数即为添加功能的新函数。
五、__name__
问题
>>> now()
run now().
2016-05-31 17:10:32.217998
run now() finished.
>>> now.__name__
'wrapper'
调用完成之后,新的问题又出现了:函数的__name__
属性因为使用装饰器改变了,有些依赖函数签名的代码执行就会出错。
python内置的functools.wraps
能够将函数名称替换回来。一个完整的decorator写法如下:
>>> import functools
>>> def decorator(fun):
@functools.wraps(fun)
def wrapper(*args,**kw):
print('run %s().' % fun.__name__)
outcome = fun(*args,**kw)
print('run %s() finished.' % fun.__name__)
return outcome
return wrapper
你看,实际上这是对wrapper函数的一个装饰器。定义完成之后,就不会除问题了。
>>> @decorator
def now():
print(datetime.now())
>>> now()
run now().
2016-05-31 17:32:59.270645
run now() finished.
>>> now.__name__
'now'
六、带参数的Decorator
好了,现在我们已经知道如何实现一个完整的Decorator了,但是,针对不同的函数,我们希望同一个Decorator根据传入的参数,实现不同的功能,这应该怎么实现?
这种情况就需要更复杂的嵌套,首先,需要传入一个参数,然后返回一个装饰器函数,执行上面类似的工作,也就是说,在外面再嵌套一层就可以了。
>>> def decorator(arg):
def wrapper(func):
@functools.wraps(func)
def function(*args,**kw):
print('%s %s().' % (arg, func.__name__))
outcome = func(*args,**kw)
print('%s %s() finished.' % (arg, func.__name__))
return outcome
return function
return wrapper
decorator
接收参数,返回wrapper函数,而这个wrapper函数是一个装饰器。内部的运行如下:
>>> decorator('execute')
<function decorator.<locals>.wrapper at 0x0346B6F0>
>>> now = decorator('execute')(now)
>>> now()
execute now().
2016-05-31 19:15:46.935403
execute now() finished.
>>> now.__name__
'now'
decorator('execute')
返回的是wrapper
函数,然后再传入now
函数,最终返回带有新功能&新参数的函数。
最终,使用语法糖的简便写法如下:
>>> @decorator('execute')
def now():
print(datetime.now())
>>> now()
execute now().
2016-05-31 19:11:19.024181
execute now() finished.
>>> @decorator('run')
def now():
print(datetime.now())
>>> now()
run now().
2016-05-31 19:11:45.361102
run now() finished.
七、终极装饰器
那么,问题来了,我们能不能创建一个装饰器,能够传入参数,也可以不传入参数呢?
这需要对装饰器的运行过程有充分的了解,观察上面的两类装饰器,主要的区别在于:
- 没有参数的装饰器,最外层接收的是
func
函数,最终返回的函数是wrapper(*args,**kw)
,直接接收func
函数参数 - 带参数的装饰器,最外层接收的是参数,而最终返回的函数是
wrapper(func)
,接收func
函数
那么,我们就可以通过判定最外层的函数接收的是func
函数还是参数,来决定是有参数或者是无参数的装饰器,最终决定如何返回。尝试结果如下:
>>> def decorator(arg):
if callable(arg):
@functools.wraps(arg)
def wrapper(*args,**kw):
print('run %s().' % arg.__name__)
outcome = arg(*args,**kw)
print('run %s() finished.' % arg.__name__)
return outcome
return wrapper
else:
def wrapper(func):
@functools.wraps(func)
def function(*args,**kw):
print('%s %s().' % (arg,func.__name__))
outcome = func(*args,**kw)
print('%s %s() finished.' % (arg,func.__name__))
return outcome
return function
return wrapper
通过最外层传入的参数是否callable()
判断是函数还是参数,进而采取不一样的措施。
>>> @decorator
def now():
print(datetime.now())
>>> now()
run now().
2016-05-31 19:51:33.730241
run now() finished.
>>> @decorator('execute')
def now():
print(datetime.now())
>>> now()
execute now().
2016-05-31 19:53:09.032116
execute now() finished.
当然,还有另外一种思路,主体按照带参数的装饰器三层嵌套来写,但是最后返回的时候,需要进行判断:如果最初传入的是参数,那么就返回函数;如果最初传入的就是函数,那么就将第二层函数执行后返回。只不过会在内部替换参数的时候有一些局限。
>>> def decorator(arg):
text = arg if isinstance(arg,str) else ''
def wrapper(func):
@functools.wraps(func)
def function(*args,**kw):
print(text)
print('run %s().' % func.__name__)
outcome = func(*args,**kw)
print('run %s() finished.' % func.__name__)
return outcome
return function
return wrapper if isinstance(arg,str) else wrapper(arg)
总结
到这里,装饰器的各部分内容都已经很清晰了。了解装饰器首先一定要将闭包是将函数和自由变量共同返回这一概念透彻清晰,其次,装饰器的内部实现原理一定要很了解,这样就没问题了。
(By XuZhiyuan 2016-05-31 20点 @Hohai Rainy)