引言
通过前面两篇文章(前两篇文章见基础篇, 进阶篇),读者们已经了解了到了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的任务,并且将参数通过闭环的方式传入了我们所使用的装饰器中。
装饰器的最佳实践
- 装饰器实在python2.4中被引入的。所以要使用装饰器,需要确保我们使用的python版本>=2.4。
- 使用装饰器会减慢调用函数的速度。
- 一旦一个函数被装饰过之后,我们就无法在运行时再调用未装饰过的原函数了。
- 装饰器事实上只是一个接受函数作为输入的函数,并返回一个对原函数的包装函数。这一包装过程可能会使得debug过程更加复杂和困难。但在2.5(含)之后的python版本中我们可以使用
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
那么,装饰器到底有什么用呢?
未完待续...
网友评论