通过前面两篇文章(前两篇文章见基础篇, 进阶篇),读者们已经了解了到了python中的装饰器背后的实现逻辑,如何理解python中以@
标记的带装饰器函数,以及如何构造带参数函数的装饰器和动态生成装饰器。在接下来这篇文章中,我们将应用之前的知识来研究一个比较复杂的问题——如何构造装饰器的装饰器,以及装饰器的最佳实践。
在进阶篇中我们已经知道了如何构造一个能接受任意参数的装饰器函数,同时我们还能够通过构造装饰器工厂的方式来动态根据输入参数生成装饰器。接下来我们将应用这些知识来实现装饰器的装饰器。这一装饰器能够用来装饰其他装饰器函数,从而使得被装饰的装饰器函数能够接收任意输入参数(请认真读一读前面这句逻辑不太直观的句子,确认你已经理解了下面代码的目的)。
这一代码的有用之处在于,我们能够动态地将我们的任意一个装饰器变为一个能够接收参数的装饰器工厂。如进阶篇中所述,由于装饰器的函数签名是固定的——def decorator_func(func_to_decorate)
,我们无法在使用@
调用装饰器函数的时候动态传入参数,所以只能先定义一个装饰器工厂,来替我们接收参数并返回包含了参数的闭环(也就是装饰器)。而下面的装饰器将这一功能抽象了出来,使得其可以复用。
代码如下所示。
def decorator_for_decorator(decorator_to_enhance):
"""
这一函数用来作为一个装饰器工厂来动态生成装饰器。
生成的装饰器能够被用来装饰其他装饰器函数,使得被装饰的装饰器函数变为能够任意接收参数的装饰器。
"""
# 为了实现参数的传递,我们在这里动态生成了一个装饰器工厂,并作为返回值
# 这一装饰器工厂将返回一个闭环作为装饰器,其中包装了外界传入的装饰器参数
def decorator_maker(*args, **kwargs):
def decorator_wrapper(func):
# 这里使用了闭环来保证参数的传递
return decorator_to_enhance(func, *args, **kwargs)
return decorator_wrapper
return decorator_maker
有了这一装饰器之后,我们就可以动态改变其他装饰器了。
# 注意为了保证我们所包装的装饰器能够正确接收参数,我们需要保证其函数签名包含我们想要传入的参数
# 但这样的装饰器函数是无法直接用来装饰其他函数的
@decorator_for_decorator
def decorator_func(func, *args, **kwargs):
def wrapper(function_arg1, function_arg2):
print('Received arguments as {}, {}'.format(args, kwargs))
print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
return func(function_arg1, function_arg2)
return wrapper
# 此时,我们就可以给我们的装饰器传入参数啦
@decorator_func(3, 5)
def func(function_arg1, function_arg2):
print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func('ice cream', 'pizza')
# output:
# Received arguments as (3, 5), {}
# $3 per ice cream, $5 per pizza, what would you like to have?
# Hello, I would like to have ice cream and pizza
上面的代码可能逻辑上不是那么直观。我们接下来详细分析。
首先我们定义了一个叫做decorator_maker
的装饰器函数。这一函数实质上是一个能够返回工厂函数的函数。它返回一个能够返回装饰器的工厂函数。也就是说,被这一装饰器装饰过之后,原有的装饰器函数将变为一个装饰器工厂函数。而这一工厂函数,正如我们在进阶篇中所述,用来接收我们想要传入的参数并通过返回一个动态生成的闭环作为装饰器的方式来实现将参数传入装饰器中的目的。紧接着我们就可以带参数通过调用这个工厂函数的方式来实现装饰一个函数的过程。
进一步地,我们总是可以通过展开装饰器的方式来理解装饰的过程发生了什么。这一方法可以用来分析所有的装饰器装饰过的函数。以上面的代码为例。
# 我们使用@decorator_for_decorator的方法装饰了decorator_func函数。可以展开如下
def decorator_func(func, *args, **kwargs):
def wrapper(function_arg1, function_arg2):
print('Received arguments as {}, {}'.format(args, kwargs))
print('${} per {}, ${} per {}, what would you like to have?'.format(args[0], function_arg1, args[1], function_arg2))
return func(function_arg1, function_arg2)
return wrapper
decorator_func = decorator_for_decorator(decorator_func)
# 经过上面的步骤,decorator_func引用所指向的其实已经是decorator_for_decorator所返回的decorator_maker函数了。这一函数是一个装饰器工厂函数。
# 紧接着我们带参数调用这一工厂函数(@decorator_func(3,5))并使用其返回的装饰器函数来装饰另一个函数(func)。这一过程展开如下。
true_decorator = decorator_func(3,5)
# 上面的true_decorator引用指向的是decorator_maker所返回的闭环decorator_wrapper。
def func(function_arg1, function_arg2):
print('Hello, I would like to have {} and {}'.format(function_arg1, function_arg2))
func = true_decorator(func)
# 到上面这一步为止,我们已经完成了装饰func的任务,并且将参数通过闭环的方式传入了我们所使用的装饰器中。
functools.wraps()
来降低装饰器对debug的影响。python从2.5开始引入了functools
模块(module),其中包含的装饰器函数functools.wraps()
能够保证被装饰函数的函数名,模块名,以及文档字符串(docstring)被传入装饰器返回的包装函数中,从而保证了抛出的错误信息中能够包含正确的函数名,改善了debug体验。如下面的代码所示。
# python的stacktrace信息中包含函数的__name__属性来帮助debug
def foo():
pass
print(foo.__name__)
# output: foo
# 但是用了装饰器之后,__name__属性会发生变化
def bar(func):
def wrapper():
return func()
return wrapper
@bar
def foo():
pass
print(foo.__name__)
# output: wrapper
# 通过使用functools来改变这一状况
import functools
def bar(func):
@functools.wraps(func)
def wrapper():
return func()
return wrapper
@bar
def foo():
pass
print(foo.__name__)
# output: foo
装饰器的用法多种多样。举例来说,我们如果想要扩展一个第三方库中附带的函数的功能,但我们又无法修改该函数源代码的时候,我们就可以使用装饰器来实现这一目的。或者我们在debug的时候,为了避免对源代码进行多次修改,就可以用装饰器来附加我们想要的逻辑。换句话说,我们可以用装饰器实现所谓的“干修改”(Dry Change)。
import time
import functools
def benchmark(func):
"""
这是一个能够计算并打印一个函数运行时间的装饰器
"""
def wrapper(*args, **kwargs):
start_time = time.time()
res = func(*args, **kwargs)
end_time = time.time()
print('{} completed in {} seconds'.format(func.__name__, end_time - start_time))
return res
return wrapper
def logging(func):
"""
这是一个能够附加log功能的装饰器
"""
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print('{} executed with args: {} and kwargs: {}'.format(func.__name__, args, kwargs))
return res
return wrapper
def counter(func):
"""
这是一个能够对函数被调用次数进行计数的装饰器
"""
def wrapper(*args, **kwargs):
wrapper.count = wrapper.count + 1
res = func(*args, **kwargs)
print('{} has been called for {} times'.format(func.__name__, wrapper.count))
return res
wrapper.count = 0
return wrapper
@counter
@logging
@benchmark
def reverse_string(string):
return ''.join(reversed(string))
reverse_string('Tough times do not last, tough people do.')
# output:
# reverse_string completed in 3.814697265625e-06 seconds
# reverse_string executed with args: ('Tough times do not last, tough people do.',) and kwargs: {}
# reverse_string has been called for 1 times
# '.od elpoep hguot ,tsal ton od semit hguoT'
reverse_string('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.')
# reverse_string completed in 5.9604644775390625e-06 seconds
# reverse_string executed with args: ('Two things are infinite: the universe and human stupidity; and I am not sure about the universe.',) and kwargs: {}
# reverse_string has been called for 2 times
# '.esrevinu eht tuoba erus ton ma I dna ;ytidiputs namuh dna esrevinu eht :etinifni era sgniht owT'
实际上,python自身也提供了一些常用的装饰器供大家调用,例如property
,staticmethod
,等等。与此同时,一些常用的python后端框架,例如Django
及Pyramid
也使用装饰器来管理缓存以及视图(view)访问权限等。另外,装饰器有时候也用来在测试中来虚构异步请求。
总之,装饰器在实际开发中可以有许多灵活的应用。读者朋友们今后可以多加尝试。
Reference
文中部分内容翻译自如下文章。翻译部分版权归原作者所有。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a