来源:https://medium.com/python-monkey/function-decorators-74c08b9493bf
作者:Nikhil Chauhan
装饰器语法,在PEP 318中有详细说明,是使用“ @”符号为功能添加额外功能的一种简便方法。添加内容的目的是确保在定义函数并修饰它时(实质上是单个定义),一次引用该函数名,以使代码更清晰。除了语法,装饰器很重要,因为它们使我们可以在每个函数中重复使用某些重复的代码,而不必在每个函数中编写相同的代码。装饰器的常见用途是在调用函数时记录信息,执行诸如用户身份验证之类的检查或检查所提供的参数。无论如何,如果发现许多函数需要相同的功能并且倾向于在每个函数中重复进行操作,将一部分代码移到装饰器中总是很方便的
| 编写一个基本的装饰器
就编写它们而言,装饰器可以定义为一个接受一个函数并返回一个新函数的函数。对于我们的第一个装饰器,我们将编写一个在调用该函数时打印的装饰器。让我们从编写一个函数开始:
def func():
print('calling func')
现在,对于装饰器,我们说它必须是一个接受函数的函数,因此必须在其参数列表中。它还必须返回一个新函数,因此我们需要在装饰器中定义一个新函数。让我们从一个示例开始,稍后我们将讨论其工作方式:
def log_calls(func):
def wrapper():
name = func.__name__
print(f'before {name} was called')
func()
print(f'after {name} was called')
return wrapper
让我们通过调用装饰器来“装饰”原始函数:
func = log_calls(func)
当我们调用装饰的时候func:
>>> func()
before func was called
calling func
after func was called
如您所见,wrapper函数中的代码已执行。这是因为当我们调用装饰器时,返回的函数实际上是wrapper。特殊的wrapper是它是在范围内定义的log_calls。这意味着函数可以func访问参数列表中的参数wrapper,这意味着可以调用它。装饰器就是这样工作的。
| “语法糖”
短语“语法糖”总是与修饰语相关联。正如我们在一开始提到的那样,装饰器语法使用@函数定义上方的符号来装饰它们。此行为是语法糖,因为@decorator在函数定义上方进行编写与在函数定义f = decorator(f)之后进行调用相同。
| 传递参数
使这种行为对我们有用,但我们编写的大多数函数都带有参数。让我们编写一个接受参数并修饰它的函数:
@log_calls
def func_with_args(a, b):
return a + b
现在,调用函数:
>>> func_with_args(1, 2)
TypeError: wrapper() takes 0 positional arguments but 2 were given
引发了一个异常,很清楚为什么:wrapper我们返回的函数不接受任何参数,但是func_with_args可以接受。那么我们如何解决这个问题呢?好吧,我们需要做的就是简单地修改装饰器,让它接受参数。最好的方法是使用args 并*kwargs 确保带有任何类型参数的任何函数都可以工作:
def log_calls(func):
def wrapper(*args, **kwargs):
name = func.__name__
print(f'before {name} was called')
func(*args, **kwargs)
print(f'after {name} was called')
return wrapper
让我们再试一次:
>>> f1(1,2)
before func_with_args was called
after func_with_args was called
那行得通,但是我们还有另一个问题:我们的函数没有返回它的值。这是因为包装函数调用func(*args, *kwargs),但未将其返回值设置为任何值。通过将该行更改为r = func(args, **kwargs),然后在末尾wrapper添加return r来返回此值,可以轻松解决此问题。进行此更改,我们的功能将按预期工作:
>>> f1(1,2)
before func_with_args was called
after func_with_args was called
3
|一个更现实的例子
因此,既然我们知道装饰器是如何工作的以及如何编写它们,那么让我们继续写一个更实际的示例(因为知道何时调用函数非常有用!)。这次,让我们编写一个函数,将函数的结果格式化为form £00.00。在使用金钱作为整数或浮点数但以这种形式显示所有值的程序中,这可能很有用。首先,装饰器:
def money_format(func):
def wrapper(*args, **kwargs):
r = func(*args, **kwargs)
formatted = '£{:.2f}'.format(r)
return formatted
return wrapper
以及快速的“加税器”功能:
@money_format
def add_tax(value, tax_percentage):
return value * (1 + (tax_percentage / 100))
让我们测试一下修饰后的功能:
>>> add_tax(52, 20)
'£62.40'
>>> add_tax(10, 10)
'£11.00'
|带参数的装饰器
使用装饰器的核心好处是它们提供了更好的代码可重用性。更进一步,可以编写接受参数以及装饰函数的装饰器,从而使装饰器更加通用。
让我们坚持将函数返回值格式化为合适格式的想法,但是这次我们将创建一个称为的装饰器prefix,毫不奇怪,该装饰器将返回值作为前缀。具有参数的装饰器具有3个“级别”:参数级别,函数级别和包装器级别。我们已经在本文前面看到了最后2个,每个“级别”都是其自己的功能。因此,只有包括所有三个级别才需要三个功能才有意义:
外部函数是接受参数的函数。它返回一个装饰器。
中间函数是由外部函数返回的装饰器:它接受一个函数并返回包装器函数来装饰提供的函数。
内部函数是包装器函数。它通常应接受提供给要修饰的函数的任意数量的参数。这是添加额外功能的地方。
现在让我们看一个如何编写这个更复杂的装饰器的示例:
def prefix(value):
def decorator(func):
def wrapper(*args, **kwargs):
r = func(*args, **kwargs)
return str(value) + str(r)
return wrapper
return decorator
现在让我们创建一个装饰函数:
>>> @prefix('£')
... def generate_bonus():
... return 250
...
>>> generate_bonus()
'£250'
|题外话:装饰器作为类
尽管通常使用函数来编写装饰器,但是可以使用类来编写装饰器,因为它们可以具有call使它们无论如何都像函数一样工作的方法。让我们看一下money_format我们之前作为类编写的装饰器的示例:
class MoneyFormat:
def __init__(self, func):
self.func = function def __call__(self, *args, **kwargs):
r = self.func(*args, **kwargs)
return '£{:.2f}'.format(r)
如您所见,添加功能的代码进入call方法,并init接受并设置功能
|@ functools.wraps
通常,在使用Python工作时,我们需要调试代码。这通常涉及print在代码中的各个点进行调用以查看调用了哪些函数。我们可以查看name和doc属性:
>>> @money_format
... def double(x):
... """Doubles a number."""
... return x * 2
...
>>> double
.wrapper at 0x...>
>>> double.__name__
'wrapper'
>>> double.__doc__
>>>
这就是问题所在:使用装饰器时,包装函数的签名(例如其签名)name会丢失,并由包装函数的签名取代。为避免这种情况,请functools.wraps发挥作用。它可以用作装饰包装器函数的装饰器(是的,另一个!),以保留包装的函数的签名。让我们看看如何使用它:
from functools import wraps
def money_format(func):
@wraps(func)
def wrapper(*args, **kwargs):
r = func(*args, **kwargs)
formatted = '£{:.2f}'.format(r)
return formatted
return wrapper
但是,当将装饰器作为类编写时,由于在call创建任何实例之前就已定义了该方法,因此无法使用:
import functoolsclass MoneyFormat:
def __init__(self, function):
self.f = function
# the error occurs in the next line
@functools.wraps(self.f)
def __call__(self, *args, **kwargs):
return ‘£{:.2f}’.format(self.f(*args, **kwargs))
NameError: name 'self' is not defined
一种解决方案是使用functools.update_wrapperinside init 提供self函数的必要属性:
import functoolsclass MoneyFormat:
def __init__(self, function):
self.f = function
functools.update_wrapper(self, self.f)
def __call__(self, *args, **kwargs):
return ‘£{:.2f}’.format(self.f(*args, **kwargs))
|摘要:我们学到了什么?
在本文介绍如何用Python编写装饰器的文章中,我们介绍了:
- 装饰器的一些常见用途
- 如何编写一个简单的装饰器
- 如何使用@decorator语法以及等同于调用的事实func = decorator(func)
- 如何将参数传递给装饰器
- 如何将装饰器编写为类
- 为什么functools.wraps在编写装饰器时有用