简介
在这个实验中,你将学习装饰器(decorators)是什么,以及它们在 Python 中是如何工作的。装饰器是一项强大的功能,它使你能够在不修改源代码的情况下修改函数的行为,并且它们在 Python 框架和库中被广泛使用。
你还将学习创建一个简单的日志记录装饰器,并实现一个更复杂的用于函数验证的装饰器。本实验涉及的文件包括 logcall.py
、sample.py
和 validate.py
,其中 validate.py
需要进行修改。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将学习装饰器(decorators)是什么,以及它们在 Python 中是如何工作的。装饰器是一项强大的功能,它使你能够在不修改源代码的情况下修改函数的行为,并且它们在 Python 框架和库中被广泛使用。
你还将学习创建一个简单的日志记录装饰器,并实现一个更复杂的用于函数验证的装饰器。本实验涉及的文件包括 logcall.py
、sample.py
和 validate.py
,其中 validate.py
需要进行修改。
在 Python 中,装饰器(decorators)是一种特殊的语法,对初学者来说非常有用。它们允许你修改函数或方法的行为。你可以把装饰器看作是一个函数,它接受另一个函数作为输入,然后返回一个新的函数。这个新函数通常会扩展或改变原函数的行为。
装饰器使用 @
符号来应用。你将这个符号和装饰器名称直接放在函数定义的上方。这是一种简单的方式,用于告诉 Python 你想在特定的函数上使用该装饰器。
让我们创建一个简单的装饰器,它在函数被调用时记录信息。日志记录是实际应用中常见的任务,使用装饰器来实现这一点是理解它们如何工作的好方法。
首先,打开 VSCode 编辑器。在 /home/labex/project
目录下,创建一个名为 logcall.py
的新文件。这个文件将包含我们的装饰器函数。
将以下代码添加到 logcall.py
中:
## logcall.py
def logged(func):
print('Adding logging to', func.__name__)
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
让我们来分析一下这段代码的作用:
logged
函数是我们的装饰器。它接受另一个函数(我们称之为 func
)作为参数。这个 func
就是我们想要添加日志记录功能的函数。logged
函数内部,我们定义了一个名为 wrapper
的内部函数。这个 wrapper
函数将取代原函数。
wrapper
函数会打印一条消息,表明该函数正在被调用。func
)。*args
和 **kwargs
用于接受任意数量的位置参数和关键字参数。logged
函数返回 wrapper
函数。现在,这个 wrapper
函数将代替原函数使用,从而添加了日志记录功能。/home/labex/project
)下,创建另一个名为 sample.py
的文件,并添加以下代码:## sample.py
from logcall import logged
@logged
def add(x, y):
return x + y
@logged
def sub(x, y):
return x - y
这里的 @logged
语法非常重要。它告诉 Python 将 logged
装饰器应用到 add
和 sub
函数上。因此,每当调用这些函数时,装饰器添加的日志记录功能就会被执行。
cd /home/labex/project
然后,启动 Python 解释器:
python3
sample
模块并测试被装饰的函数:>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()
注意,当你导入 sample
模块时,会打印出 “Adding logging to...” 消息。这是因为装饰器在模块导入时就会被应用。每次调用被装饰的函数时,都会打印出 “Calling...” 消息。这表明装饰器正在按预期工作。
这个简单的装饰器展示了装饰器的基本概念。它在不改变原函数代码的情况下,用额外的功能(这里是日志记录)包装了原函数。这是 Python 中一个强大的特性,你可以在许多不同的场景中使用它。
在这一步中,我们将创建一个更实用的装饰器。Python 中的装饰器是一种特殊类型的函数,它可以修改另一个函数的行为。我们要创建的装饰器将根据类型注解(type annotations)来验证函数的参数。类型注解是一种指定函数参数和返回值预期数据类型的方式。这在实际应用中是一个常见的用例,因为它有助于确保函数接收到正确的输入类型,从而避免许多错误。
我们已经为你创建了一个名为 validate.py
的文件,它包含了一些验证类。验证类用于检查一个值是否满足某些条件。要查看这个文件的内容,你需要在 VSCode 编辑器中打开它。你可以在终端中运行以下命令来实现:
cd /home/labex/project
code validate.py
该文件包含三个类:
Validator
- 这是一个基类。基类提供了一个通用的框架或结构,其他类可以继承它。在这种情况下,它为验证提供了基本结构。Integer
- 这个验证器类用于确保一个值是整数。如果你将非整数值传递给使用这个验证器的函数,它将引发一个错误。PositiveInteger
- 这个验证器类确保一个值是正整数。因此,如果你传递一个负整数或零,它也将引发一个错误。现在,我们要在 validate.py
文件中添加一个名为 validated
的装饰器函数。这个装饰器将执行几个重要的任务:
将以下代码添加到 validate.py
文件的末尾:
## Add to validate.py
import inspect
import functools
def validated(func):
sig = inspect.signature(func)
print(f'Validating {func.__name__} {sig}')
@functools.wraps(func)
def wrapper(*args, **kwargs):
## Bind arguments to the signature
bound = sig.bind(*args, **kwargs)
errors = []
## Validate each argument
for name, value in bound.arguments.items():
if name in sig.parameters:
param = sig.parameters[name]
if param.annotation != inspect.Parameter.empty:
try:
## Create an instance of the validator and validate the value
if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
validator = param.annotation()
bound.arguments[name] = validator.validate(value)
except Exception as e:
errors.append(f' {name}: {e}')
## If validation errors, raise an exception
if errors:
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
## Call the function
result = func(*bound.args, **bound.kwargs)
## Validate the return value
if sig.return_annotation != inspect.Signature.empty:
try:
if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
validator = sig.return_annotation()
result = validator.validate(result)
except Exception as e:
raise TypeError(f'Bad return: {e}') from None
return result
return wrapper
这段代码使用了 Python 的 inspect
模块。inspect
模块允许我们获取关于实时对象(如函数)的信息。在这里,我们使用它来检查函数的签名,并根据类型注解验证参数。我们还使用了 functools.wraps
。这是一个辅助函数,它可以保留原函数的元数据,如函数名和文档字符串。元数据就像是关于函数的额外信息,有助于我们理解函数的作用。
让我们创建一个文件来测试我们的验证装饰器。我们将创建一个名为 test_validate.py
的新文件,并向其中添加以下代码:
## test_validate.py
from validate import Integer, PositiveInteger, validated
@validated
def add(x: Integer, y: Integer) -> Integer:
return x + y
@validated
def pow(x: Integer, y: Integer) -> Integer:
return x ** y
## Test with a class
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def cost(self):
return self.shares * self.price
@validated
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
现在,我们将在 Python 解释器中测试我们的装饰器。首先,导航到项目目录并在终端中运行以下命令来启动 Python 解释器:
cd /home/labex/project
python3
然后,在 Python 解释器中,我们可以运行以下代码来测试我们的装饰器:
>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 75, in wrapper
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
x: Expected <class 'int'>
y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 83, in wrapper
raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 75, in wrapper
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
nshares: Expected value > 0
>>> exit()
如你所见,我们的 validated
装饰器成功地对函数参数和返回值执行了类型检查。这非常有用,因为它使我们的代码更加健壮。我们可以在函数边界处捕获类型错误,而不是让它们在代码中进一步传播并导致难以发现的错误。
在这个实验中,你学习了 Python 中的装饰器,包括它们是什么以及如何工作。你还掌握了创建一个简单的日志记录装饰器,以便为函数添加行为,并构建了一个更复杂的装饰器,用于根据类型注解验证函数参数。此外,你学会了使用 inspect
模块来分析函数签名,以及使用 functools.wraps
来保留函数元数据。
装饰器是 Python 中一个强大的特性,它能让你编写更易于维护和复用的代码。它们在 Python 框架和库中常用于处理诸如日志记录、访问控制和缓存等横切关注点。现在,你可以在自己的 Python 项目中应用这些技术,从而使代码更加简洁和易于维护。