定义简单的装饰器函数

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习装饰器(decorators)是什么,以及它们在 Python 中是如何工作的。装饰器是一项强大的功能,它使你能够在不修改源代码的情况下修改函数的行为,并且它们在 Python 框架和库中被广泛使用。

你还将学习创建一个简单的日志记录装饰器,并实现一个更复杂的用于函数验证的装饰器。本实验涉及的文件包括 logcall.pysample.pyvalidate.py,其中 validate.py 需要进行修改。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132514{{"定义简单的装饰器函数"}} python/function_definition -.-> lab-132514{{"定义简单的装饰器函数"}} python/scope -.-> lab-132514{{"定义简单的装饰器函数"}} python/decorators -.-> lab-132514{{"定义简单的装饰器函数"}} end

创建你的第一个装饰器

什么是装饰器?

在 Python 中,装饰器(decorators)是一种特殊的语法,对初学者来说非常有用。它们允许你修改函数或方法的行为。你可以把装饰器看作是一个函数,它接受另一个函数作为输入,然后返回一个新的函数。这个新函数通常会扩展或改变原函数的行为。

装饰器使用 @ 符号来应用。你将这个符号和装饰器名称直接放在函数定义的上方。这是一种简单的方式,用于告诉 Python 你想在特定的函数上使用该装饰器。

创建一个简单的日志记录装饰器

让我们创建一个简单的装饰器,它在函数被调用时记录信息。日志记录是实际应用中常见的任务,使用装饰器来实现这一点是理解它们如何工作的好方法。

  1. 首先,打开 VSCode 编辑器。在 /home/labex/project 目录下,创建一个名为 logcall.py 的新文件。这个文件将包含我们的装饰器函数。

  2. 将以下代码添加到 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 函数将代替原函数使用,从而添加了日志记录功能。

使用装饰器

  1. 现在,在同一目录(/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 装饰器应用到 addsub 函数上。因此,每当调用这些函数时,装饰器添加的日志记录功能就会被执行。

测试装饰器

  1. 要测试你的装饰器,在 VSCode 中打开一个终端。首先,使用以下命令将目录更改为项目目录:
cd /home/labex/project

然后,启动 Python 解释器:

python3
  1. 在 Python 解释器中,导入 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

该文件包含三个类:

  1. Validator - 这是一个基类。基类提供了一个通用的框架或结构,其他类可以继承它。在这种情况下,它为验证提供了基本结构。
  2. Integer - 这个验证器类用于确保一个值是整数。如果你将非整数值传递给使用这个验证器的函数,它将引发一个错误。
  3. PositiveInteger - 这个验证器类确保一个值是正整数。因此,如果你传递一个负整数或零,它也将引发一个错误。

添加验证装饰器

现在,我们要在 validate.py 文件中添加一个名为 validated 的装饰器函数。这个装饰器将执行几个重要的任务:

  1. 它将检查函数的类型注解。类型注解就像小注释,告诉我们函数期望什么样的数据。
  2. 它将根据这些类型注解验证传递给函数的参数。这意味着它将检查传递给函数的值是否为正确的类型。
  3. 它还将根据函数的注解验证函数的返回值。因此,它确保函数返回它应该返回的数据类型。
  4. 如果验证失败,它将引发信息丰富的错误消息。这些消息将准确地告诉你哪里出了问题,比如哪个参数的类型错误。

将以下代码添加到 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 项目中应用这些技术,从而使代码更加简洁和易于维护。