认识装饰器
装饰器简单来讲就是一个接收被装饰的函数作为固定参数的函数(或称为callable,比如一个对象具有call方法),并且对被装饰的函数做些处理。举个例子:def decorated_by(func):
func.__doc__ += '\nDecorated by decorated_by.'
return func
def add(x, y):
"""Return the sum of x and y."""
return x + y
if __name__ == '__main__':
add = decorated_by(add)
print add.__doc__
# output
Return the sum of x and y.
Decorated by decorated_by.
大多数情况下,我们只关心最终被装饰的函数,而持有装饰器函数的引用基本上是没必要的。所以,在Python 2.5中引入了对装饰器的特殊语法——装饰器的应用可以通过在被装饰的函数上面一行添加@字符,其后紧跟装饰器函数名的这种方式:比如针对上面的例子,我们就可以写作:
def add(x, y):
"""Return the sum of x and y."""
return x + y
if __name__ == '__main__':
# add = decorated_by(add)
add(1, 2)
print add.__doc__
# output
Return the sum of x and y.
Decorated by decorated_by.
添加@+装饰器名的这种方式就等价于 add = decorated_by(add)。这种方式看起来更简洁明了。
装饰器应用的顺序
当@语法被使用时,装饰器会在被装饰的callable被创建后立即调用(即装饰器的代码是在应用到被装饰的函数上时执行,而不是被装饰的函数调用时执行)。就上面的例子而言,就是add函数被创建后,紧接着decorated_by函数被应用。那么如果对于单个callable使用@语法应用多个装饰器时(Python中是支持这种场景的),装饰器的应用顺序有事怎样的?答案就是:从下往上,按顺序执行。举个例子:
我们有另外的一个函数叫做also_decorated_by,也是在func.doc后面添加一段话,然后对add函数应用该装饰器:def also_decorated_by(func):
func.__doc__ += '\nAlso decorated by also_decorated_by.'
return func
def add(x, y):
"""Return the sum of x and y."""
return x + y
按照从下往上的顺序,我们知道当我们调用add后,执行decorated_by相当于add = decorated_by(add) ,然后对此时的add应用also_decorated_by,就相当于add = also_decorated_by(decorated_by(add))。最终的结果正是:if __name__ == '__main__':
# add = decorated_by(add)
add(1, 2)
print add.__doc__
Return the sum of x and y.
Decorated by decorated_by.
Also decorated by also_decorated_by.
装饰器的应用场景
标准库中的很多模块都包含有装饰器,许多常见的工具和框架都将其用于常见的功能。例如,如果要在类上创建一个方法,调用该方法时不需要该类的实例,则可以使用@classmethod或@staticmethod装饰器,这是标准库中的一个简单例子。
常用的工具中也是用装饰器,比如常见的Python Web框架Django中,使用@login_required作为装饰器允许开发者指定用户在访问特定页面时必须要登录。另外一个Web框架Flask中使用@app.route来注册指定的URI被访问到时要执行的函数。再比如,Celery中使用一个复杂的@task装饰器来标识一个函数为一个异步任务,这个装饰器的返回实际上是一个Task类的实例,展示出了如何用装饰器来制作方便快捷的API。
为什么要使用装饰器
有了装饰器,你就可以做到在某些特定的地方使用具体的、可复用的功能——如果代码写得好,装饰器就是模块化的、明确的。正是由于装饰器的模块化,使得它们非常适合避免重复的模版设置和拆解代码,同时由于装饰器仅与被装饰的函数本身有交互,所以非常擅长在其他地方注册功能。
在Python应用程序和模块中编写装饰器有几个非常好的用例——
- 附加功能 - 在被装饰的方法前后执行额外的附加功能
- 数据清洗或添加 - 对传入被装饰的函数的参数做一下清晰,确保参数类型一致性或者使参数值村从一定的模式,比如@requires_ints
- 功能注册
动手写几个装饰器
纸上得来终觉浅,绝知此事要躬行。写下来就要动手写几个装饰器的例子。
功能注册
先上代码打个样:
class Registry(object): |
运行时封装代码(wrap code)
上面装饰器的例子比较简单,因为被装饰的函数作为参数传递过去时并没有被修改。然而,有些时候当我们运行被装饰的方法时,希望做些额外的功能,那么我们可以通过返回不同的可调用函数(callable)来添加相应的功能,并且(通常)在执行过程中调用装饰方法。举几个例子:
类型检查
def requires_int(decorated): |
上面的例子中,我们使用装饰器@requires_int对foo的参数进行检查,如果入参中有任何不是int类型的参数,就抛出一个异常。
如果此时我们执行help(foo)会发现,得到的结果是:Help on function inner in module __main__:
inner(*args, **kwargs)
Get any values that may have been sent as keyword arguments.
很奇怪吧,为什么显示的是inner的函数名及doc而不是foo呢?因为此时inner函数已经被赋值给了foo,如果我们执行foo,其实就是执行inner——首先执行了类型检查,然后运行被装饰的方法,因为inner通过return decorated(args, *kwargs)调用了被装饰的函数。如果没有return这个调用,被装饰的方法就会被忽略。
保留原函数的帮助信息
经过上面的例子,我们会自然而然的思考一问题,如果当我们运行help()去查看函数的帮助信息时,希望看到的是被装饰的原函数的文档,而不是装饰器的文档,该怎么做呢?这里,我们的解决方案就是使用装饰器@functools.wraps ,它可以将一个函数重要的内部元素拷贝到另一个函数中:import functools
def requires_int(decorated):
def inner(*args, **kwargs):
""" Get any values that may have been sent as keyword arguments.
"""
kwarg_values = [i for i in kwargs.values()]
...
那么此时查看help(foo),我们得到的输出结果是:Help on function foo in module __main__:
foo(*args, **kwargs)
Return the sum of x and y
注:我这里运行的是Python2.7,如果实在Python3下,看到的结果应该是
foo(x, y)
。
用户校验
import functools |
我们定义了一个User类,一个AnonymousUser类集成User类,注意,在AnonymousUser中,我们使用了nonzero方法,这个方法定义了对类的实例调用bool()时的行为,即在require_user的inner方法中,if user and isinstance(user, User)时的if user,相当于if bool(user),因为在AnonymousUser中我们定义了nonzero返回False,所以这里就没有办法通过if检查,从而rasie了异常。
格式化输出
除了为函数的输入清理数据,装饰器的另一个作用就是清理/格式化输出数据。比如我们希望函数的输出以JSON格式,在每个相关的函数的最后手动去格式化输出为JSON显得特别冗余和笨重。此时,装饰器就站出来了。
import functools |
打印日志
最后一个例子,打印日志,记录调用了哪个方法、什么时间调用、方法执行时长以及方法的返回值:import functools
import logging
import time
logging.basicConfig(evel=logging.DEBUG)
LOG = logging.getLogger(__name__)
def logged(method):
def inner(*args, **kwargs):
start = time.time()
return_value = method(*args, **kwargs)
end = time.time()
delta = end - start
LOG.warn('Called method %s at %.02f; execution time %.02f '
'seconds; result %r.' %
(method.__name__, start, delta, return_value))
return return_value
return inner
def sleep_and_return(return_value):
time.sleep(2)
return return_value
if __name__ == '__main__':
print sleep_and_return(27)
# output
WARNING:__main__:Called method sleep_and_return at 1505889774.61; execution time 2.00 seconds; result 27.
27
小节结语
本篇文章的重点是简单的认识一下装饰器,了解一下装饰器的简单应用。
目前上面的例子,装饰器除了被装饰的函数作为参数之外,都不接收其他的参数,但是很多情况下,装饰器本身接收其他参数是很有必要的。我们后面再展开。