装饰器链式调用与参数化装饰器

PythonPythonBeginner
立即练习

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

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

简介

在这个实验中,你将学习 Python 中的装饰器,这是一个强大的特性,可以修改函数和方法的行为。装饰器通常用于日志记录、性能测量、访问控制和类型检查等任务。

你将学习如何链式调用多个装饰器、创建接受参数的装饰器、在使用装饰器时保留函数元数据,以及将装饰器应用于不同类型的类方法。你将使用的文件有 logcall.pyvalidate.pysample.py


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/lambda_functions("Lambda Functions") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/class_static_methods("Class Methods and Static Methods") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} python/function_definition -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} python/lambda_functions -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} python/scope -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} python/classes_objects -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} python/class_static_methods -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} python/decorators -.-> lab-132515{{"装饰器链式调用与参数化装饰器"}} end

在装饰器中保留函数元数据

在 Python 中,装饰器是一个强大的工具,可用于修改函数的行为。然而,当你使用装饰器包装函数时,会出现一个小问题。默认情况下,原始函数的元数据(如函数名、文档字符串(docstring)和注解)会丢失。元数据很重要,因为它有助于内省(检查代码结构)和生成文档。让我们先来验证这个问题。

在 WebIDE 中打开你的终端。我们将运行一些 Python 命令,看看使用装饰器时会发生什么。以下命令将创建一个用装饰器包装的简单函数 add,然后打印该函数及其文档字符串。

cd ~/project
python3 -c "from logcall import logged; @logged
def add(x,y):
    'Adds two things'
    return x+y
    
print(add)
print(add.__doc__)"

当你运行这些命令时,你会看到类似如下的输出:

<function wrapper at 0x...>
None

注意,函数名显示为 wrapper,而不是 add。并且文档字符串本应是 'Adds two things',现在却为 None。当你使用依赖这些元数据的工具(如内省工具或文档生成器)时,这可能会成为一个大问题。

使用 functools.wraps 解决问题

Python 的 functools 模块可以解决这个问题。它提供了一个 wraps 装饰器,可帮助我们保留函数元数据。让我们看看如何修改 logged 装饰器以使用 wraps

  1. 首先,在 WebIDE 中打开 logcall.py 文件。你可以在终端中使用以下命令导航到项目目录:
cd ~/project
  1. 现在,用以下代码更新 logcall.py 中的 logged 装饰器。@wraps(func) 装饰器是关键所在。它将原始函数 func 的所有元数据复制到包装函数中。
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
  1. @wraps(func) 装饰器起着重要作用。它从原始函数 func 中获取所有元数据(如名称、文档字符串和注解),并将其附加到 wrapper 函数上。这样,当我们使用被装饰的函数时,它将拥有正确的元数据。

  2. 让我们测试改进后的装饰器。在终端中运行以下命令:

python3 -c "from logcall import logged; @logged
def add(x,y):
    'Adds two things'
    return x+y
    
print(add)
print(add.__doc__)"

现在你应该会看到:

<function add at 0x...>
Adds two things

太棒了!函数名和文档字符串都被保留下来了。这意味着我们的装饰器现在按预期工作,原始函数的元数据完好无损。

修复 validate.py 中的装饰器

现在,让我们对 validate.py 中的 validated 装饰器应用同样的修复。这个装饰器用于根据函数的注解验证函数参数和返回值的类型。

  1. 在 WebIDE 中打开 validate.py 文件。

  2. @wraps 装饰器更新 validated 装饰器。以下代码展示了如何操作。在 validated 装饰器内部的 wrapper 函数上添加 @wraps(func) 装饰器,以保留元数据。

from functools import wraps

class Integer:
    @classmethod
    def __instancecheck__(cls, x):
        return isinstance(x, int)

def validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ## Get function annotations
        annotations = func.__annotations__
        ## Check arguments against annotations
        for arg_name, arg_value in zip(func.__code__.co_varnames, args):
            if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
                raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')

        ## Run the function and get the result
        result = func(*args, **kwargs)

        ## Check the return value
        if 'return' in annotations and not isinstance(result, annotations['return']):
            raise TypeError(f'Expected return value to be {annotations["return"].__name__}')

        return result
    return wrapper
  1. 让我们测试一下,看看 validated 装饰器现在是否保留了元数据。在终端中运行以下命令:
python3 -c "from validate import validated, Integer; @validated
def multiply(x: Integer, y: Integer) -> Integer:
    'Multiplies two integers'
    return x * y
    
print(multiply)
print(multiply.__doc__)"

你应该会看到:

<function multiply at 0......>
Multiplies two integers

现在,loggedvalidated 这两个装饰器都能正确保留它们所装饰函数的元数据。这确保了你在使用这些装饰器时,函数仍然会保留其原始名称、文档字符串和注解,这对代码的可读性和可维护性非常有用。

✨ 查看解决方案并练习

创建带参数的装饰器

到目前为止,我们一直在使用 @logged 装饰器,它总是打印一条固定的消息。但如果你想自定义消息格式呢?在本节中,我们将学习如何创建一个可以接受参数的新装饰器,让你在使用装饰器时拥有更多的灵活性。

理解带参数的装饰器

带参数的装饰器是一种特殊类型的函数。它并不直接修改另一个函数,而是返回一个装饰器。带参数的装饰器的一般结构如下:

def decorator_with_args(arg1, arg2, ...):
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ## Use arg1, arg2, ... here
            ## Call the original function
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

当你在代码中使用 @decorator_with_args(value1, value2) 时,Python 首先调用 decorator_with_args(value1, value2)。这个调用会返回实际的装饰器,然后该装饰器会应用到 @ 语法后面的函数上。这个两步过程是带参数的装饰器工作的关键。

创建 logformat 装饰器

让我们创建一个 @logformat(fmt) 装饰器,它接受一个格式字符串作为参数。这将使我们能够自定义日志消息。

  1. 在 WebIDE 中打开 logcall.py 文件,并添加新的装饰器。下面的代码展示了如何定义现有的 logged 装饰器和新的 logformat 装饰器:
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def logformat(fmt):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(fmt.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorator

logformat 装饰器中,外部函数 logformat 接受一个格式字符串 fmt 作为参数。然后它返回 decorator 函数,这是实际修改目标函数的装饰器。

  1. 现在,让我们通过修改 sample.py 文件来测试我们的新装饰器。以下代码展示了如何在不同的函数上使用 loggedlogformat 装饰器:
from logcall import logged, logformat

@logged
def add(x, y):
    "Adds two numbers"
    return x + y

@logged
def sub(x, y):
    "Subtracts y from x"
    return x - y

@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x, y):
    "Multiplies two numbers"
    return x * y

在这里,addsub 函数使用了 logged 装饰器,而 mul 函数使用了带有自定义格式字符串的 logformat 装饰器。

  1. 运行更新后的 sample.py 文件以查看结果。打开终端并运行以下命令:
cd ~/project
python3 -c "import sample; print(sample.add(2, 3)); print(sample.mul(2, 3))"

你应该会看到类似如下的输出:

Calling add
5
sample.py:mul
6

这个输出表明,logged 装饰器按预期打印了函数名,而 logformat 装饰器使用自定义格式字符串打印了文件名和函数名。

使用 logformat 重新定义 logged 装饰器

既然我们有了一个更灵活的 logformat 装饰器,我们可以使用它来重新定义我们原来的 logged 装饰器。这将帮助我们复用代码并保持一致的日志格式。

  1. 用以下代码更新 logcall.py 文件:
from functools import wraps

def logformat(fmt):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(fmt.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorator

## Define logged using logformat
logged = lambda func: logformat("Calling {func.__name__}")(func)

在这里,我们使用一个 lambda 函数根据 logformat 装饰器来定义 logged 装饰器。这个 lambda 函数接受一个函数 func,并使用特定的格式字符串应用 logformat 装饰器。

  1. 测试重新定义的 logged 装饰器是否仍然有效。打开终端并运行以下命令:
cd ~/project
python3 -c "from logcall import logged; @logged
def greet(name):
    return f'Hello, {name}'
    
print(greet('World'))"

你应该会看到:

Calling greet
Hello, World

这表明重新定义的 logged 装饰器按预期工作,并且我们成功复用了 logformat 装饰器来实现一致的日志格式。

✨ 查看解决方案并练习

将装饰器应用于类方法

现在,我们将探讨装饰器如何与类方法进行交互。这可能有点棘手,因为 Python 有不同类型的方法:实例方法、类方法、静态方法和属性方法。装饰器是一种函数,它接受另一个函数并扩展该函数的行为,而无需显式修改它。当将装饰器应用于类方法时,我们需要注意它们如何与这些不同类型的方法协同工作。

理解挑战

让我们看看将 @logged 装饰器应用于不同类型的方法时会发生什么。@logged 装饰器可能用于记录方法调用的信息。

  1. 在 WebIDE 中创建一个新文件 methods.py。这个文件将包含一个类,其中不同类型的方法都使用 @logged 装饰器进行装饰。
from logcall import logged

class Spam:
    @logged
    def instance_method(self):
        print("Instance method called")
        return "instance result"

    @logged
    @classmethod
    def class_method(cls):
        print("Class method called")
        return "class result"

    @logged
    @staticmethod
    def static_method():
        print("Static method called")
        return "static result"

    @logged
    @property
    def property_method(self):
        print("Property method called")
        return "property result"

在这段代码中,我们有一个名为 Spam 的类,它包含四种不同类型的方法。每个方法都使用 @logged 装饰器进行装饰,有些方法还使用了其他内置装饰器,如 @classmethod@staticmethod@property

  1. 让我们测试一下它的工作情况。我们将在终端中运行一个 Python 命令来调用这些方法并查看输出。
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"

当你运行这个命令时,你可能会注意到一些问题:

  • @property 装饰器与 @logged 装饰器可能无法正确协同工作。@property 装饰器用于将方法定义为属性,它有特定的工作方式。当与 @logged 装饰器结合使用时,可能会产生冲突。
  • 对于 @classmethod@staticmethod,装饰器的顺序很重要。装饰器的应用顺序会改变方法的行为。

装饰器的顺序

当你应用多个装饰器时,它们是从下到上依次应用的。这意味着离方法定义最近的装饰器最先应用,然后上面的装饰器按顺序依次应用。例如:

@decorator1
@decorator2
def func():
    pass

这相当于:

func = decorator1(decorator2(func))

在这个例子中,decorator2 首先应用于 func,然后 decorator1 应用于 decorator2(func) 的结果。

修正装饰器顺序

让我们更新 methods.py 文件来修正装饰器的顺序。通过改变装饰器的顺序,我们可以确保每个方法都能按预期工作。

from logcall import logged

class Spam:
    @logged
    def instance_method(self):
        print("Instance method called")
        return "instance result"

    @classmethod
    @logged
    def class_method(cls):
        print("Class method called")
        return "class result"

    @staticmethod
    @logged
    def static_method():
        print("Static method called")
        return "static result"

    @property
    @logged
    def property_method(self):
        print("Property method called")
        return "property result"

在这个更新后的版本中:

  • 对于 instance_method,顺序无关紧要。实例方法是在类的实例上调用的,@logged 装饰器可以以任何顺序应用,而不会影响其基本功能。
  • 对于 class_method,我们在 @logged 之后应用 @classmethod@classmethod 装饰器会改变方法的调用方式,在 @logged 之后应用它可以确保日志记录正常工作。
  • 对于 static_method,我们在 @logged 之后应用 @staticmethod。与 @classmethod 类似,@staticmethod 装饰器有其自身的行为,与 @logged 装饰器的顺序需要正确。
  • 对于 property_method,我们在 @logged 之后应用 @property。这可以确保在获得日志记录功能的同时,保持属性的行为。
  1. 让我们测试更新后的代码。我们将运行与之前相同的命令,看看问题是否得到解决。
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"

现在你应该会看到所有方法类型都有正确的日志记录:

Calling instance_method
Instance method called
instance result
Calling class_method
Class method called
class result
Calling static_method
Static method called
static result
Calling property_method
Property method called
property result

方法装饰器的最佳实践

在使用方法装饰器时,请遵循以下最佳实践:

  1. 在自定义装饰器之后应用方法转换装饰器(@classmethod@staticmethod@property)。这可以确保自定义装饰器首先执行其日志记录或其他操作,然后内置装饰器可以按预期转换方法。
  2. 要注意装饰器的执行是在类定义时发生的,而不是在方法调用时。这意味着装饰器中的任何设置或初始化代码将在类定义时运行,而不是在方法调用时。
  3. 对于更复杂的情况,你可能需要为不同的方法类型创建专门的装饰器。不同类型的方法有不同的行为,一刀切的装饰器可能并不适用于所有情况。
✨ 查看解决方案并练习

创建带参数的类型强制装饰器

在前面的步骤中,我们学习了 @validated 装饰器。这个装饰器用于在 Python 函数中强制实施类型注解。类型注解是一种指定函数参数和返回值预期类型的方式。现在,我们要更进一步。我们将创建一个更灵活的装饰器,它可以接受类型规范作为参数。这意味着我们可以更明确地为每个参数和返回值定义所需的类型。

理解目标

我们的目标是创建一个 @enforce() 装饰器。这个装饰器允许我们使用关键字参数指定类型约束。以下是它的工作示例:

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

在这个例子中,我们使用 @enforce 装饰器指定 add 函数的 xy 参数应该是 Integer 类型,返回值也应该是 Integer 类型。这个装饰器的行为与我们之前的 @validated 装饰器类似,但它让我们对类型规范有更多的控制权。

创建 enforce 装饰器

  1. 首先,在 WebIDE 中打开 validate.py 文件。我们将把新的装饰器添加到这个文件中。以下是我们要添加的代码:
from functools import wraps

class Integer:
    @classmethod
    def __instancecheck__(cls, x):
        return isinstance(x, int)

def validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ## Get function annotations
        annotations = func.__annotations__
        ## Check arguments against annotations
        for arg_name, arg_value in zip(func.__code__.co_varnames, args):
            if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
                raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')

        ## Run the function and get the result
        result = func(*args, **kwargs)

        ## Check the return value
        if 'return' in annotations and not isinstance(result, annotations['return']):
            raise TypeError(f'Expected return value to be {annotations["return"].__name__}')

        return result
    return wrapper

def enforce(**type_specs):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ## Check argument types
            for arg_name, arg_value in zip(func.__code__.co_varnames, args):
                if arg_name in type_specs and not isinstance(arg_value, type_specs[arg_name]):
                    raise TypeError(f'Expected {arg_name} to be {type_specs[arg_name].__name__}')

            ## Run the function and get the result
            result = func(*args, **kwargs)

            ## Check the return value
            if 'return_' in type_specs and not isinstance(result, type_specs['return_']):
                raise TypeError(f'Expected return value to be {type_specs["return_"].__name__}')

            return result
        return wrapper
    return decorator

让我们来分析一下这段代码的作用。Integer 类用于定义一个自定义类型。validated 装饰器根据函数的类型注解检查函数参数和返回值的类型。enforce 装饰器是我们正在创建的新装饰器。它接受关键字参数,这些参数指定了每个参数和返回值的类型。在 enforce 装饰器的 wrapper 函数内部,我们检查参数和返回值的类型是否与指定的类型匹配。如果不匹配,我们就抛出一个 TypeError 异常。

  1. 现在,让我们测试一下新的 @enforce 装饰器。我们将运行一些测试用例,看看它是否按预期工作。以下是运行测试的代码:
cd ~/project
python3 -c "from validate import enforce, Integer

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

## This should work
print(add(2, 3))

## This should raise a TypeError
try:
    print(add('2', 3))
except TypeError as e:
    print(f'Error: {e}')

## This should raise a TypeError
try:
    @enforce(x=Integer, y=Integer, return_=Integer)
    def bad_add(x, y):
        return str(x + y)
    print(bad_add(2, 3))
except TypeError as e:
    print(f'Error: {e}')"

在这段测试代码中,我们首先使用 @enforce 装饰器定义了一个 add 函数。然后,我们用有效的参数调用 add 函数,这应该能正常工作而不会出错。接下来,我们用一个无效的参数调用 add 函数,这应该会抛出一个 TypeError 异常。最后,我们定义了一个 bad_add 函数,它返回一个错误类型的值,这也应该抛出一个 TypeError 异常。

当你运行这段测试代码时,你应该会看到类似以下的输出:

5
Error: Expected x to be Integer
Error: Expected return value to be Integer

这个输出表明我们的 @enforce 装饰器工作正常。当参数或返回值的类型与指定的类型不匹配时,它会抛出一个 TypeError 异常。

比较两种方法

@validated@enforce 装饰器都实现了强制类型约束的目标,但它们的实现方式不同。

  1. @validated 装饰器使用 Python 的内置类型注解。以下是一个示例:

    @validated
    def add(x: Integer, y: Integer) -> Integer:
        return x + y

    使用这种方法,我们在函数定义中直接使用类型注解指定类型。这是 Python 的内置特性,在集成开发环境(IDE)中提供了更好的支持。IDE 可以使用这些类型注解来提供代码补全、类型检查和其他有用的功能。

  2. 另一方面,@enforce 装饰器使用关键字参数来指定类型。以下是一个示例:

    @enforce(x=Integer, y=Integer, return_=Integer)
    def add(x, y):
        return x + y

    这种方法更明确,因为我们直接将类型规范作为参数传递给装饰器。当与依赖其他注解系统的库一起使用时,这种方法很有用。

每种方法都有其优点。类型注解是 Python 的原生部分,提供了更好的 IDE 支持,而 @enforce 方法则给我们更多的灵活性和明确性。你可以根据正在进行的项目选择最适合你需求的方法。

✨ 查看解决方案并练习

总结

在这个实验中,你学习了如何有效地创建和使用装饰器。你学会了使用 functools.wraps 保留函数元数据,创建接受参数的装饰器,处理多个装饰器并理解它们的应用顺序。你还学会了将装饰器应用于不同的类方法,并创建一个接受参数的类型强制装饰器。

这些装饰器模式在 Flask、Django 和 pytest 等 Python 框架中经常使用。掌握装饰器将使你能够编写更易于维护和复用的代码。为了进一步学习,你可以探索上下文管理器、基于类的装饰器、使用装饰器进行缓存,以及使用装饰器进行高级类型检查。