Python装饰器:综合指南

当我开始使用 Python 编程时,如果我没记错的话,版本是 3.3。因此,当我开始编程时,装饰器已经在 Python 社区中存在很长时间了。

函数装饰器在 Python 2.2 版本中出现,类装饰器在 Python 2.6 版本中出现。

就我个人而言,我认为 Python 的装饰器功能是该语言的一个非常强大的功能。

实际上,我的目的是写一系列关于 Python 中最难理解的主题的文章。我计划逐一介绍这些主题(大约十多个)。

在本文中,我将尽可能涉及装饰器主题的每个部分。

1. 历史背景

  • 早期(Python 2.2 之前):在装饰器出现之前,修改函数或类通常需要手动包装或 monkey-patching,这很麻烦并且可读性较差。
  • 元类(Python 2.2):元类提供了一种控制类创建的方法,提供了装饰器以后提供的一些功能,但它们对于简单的修改来说很复杂。
  • PEP 318(Python 2.4):装饰器通过 PEP 318 在 Python 2.4 中正式引入。该提案受到 Java 中注释的启发,旨在提供一种更清晰、更具声明性的方式来修改函数和方法。
  • 类装饰器(Python 2.6):Python 2.6 将装饰器支持扩展至类,进一步增强了其多功能性。
  • 广泛采用:装饰器很快成为一种流行的功能,广泛应用于 Flask 和 Django 等框架的路由、身份验证等。
  • 2.什么是装饰器?

    本质上,装饰器是 Python 中的一种设计模式,它允许您修改函数或类的行为而不更改其核心结构。装饰器是一种元编程形式,您本质上是在编写操纵其他代码的代码。

    您知道 Python 使用以下顺序给出的范围来解析名称:

  • 当地的
  • 封闭
  • 全球的
  • 内置
  • 装饰器处于封闭范围之内,这与闭包概念密切相关。

    **关键思想**:装饰器以函数作为输入,为其添加一些功能,并返回修改后的函数。

    **类比**:将装饰器想象成礼品包装器。您有一份礼物(原始功能),并用装饰纸(装饰器)包裹它,使其看起来更漂亮或添加额外的功能(如蝴蝶结或卡片)。里面的礼物保持不变,但其呈现方式或相关操作得到了增强。

    A)装饰器变体:基于函数与基于类

    Python 中的大多数装饰器都是使用函数实现的,但您也可以使用类创建装饰器。

    基于函数的装饰器更常见且更简单,而基于类的装饰器提供了额外的灵活性。

    基于函数的基本装饰器语法

    def my_decorator(func):
        def wrapper(*args, **kwargs):
            # Do something before calling the decorated function
            print("Before function call")
            result = func(*args, **kwargs)
            # Do something after calling the decorated function
            print("After function call")
            return result
        return wrapper
    
    @my_decorator
    def say_hello(name):
        print(f"Hello, {name}!")
    
    say_hello("World")

    **解释:**

  • my_decorator 是装饰器函数。它以要装饰的函数 func 作为输入。
  • 包装器是一个内部函数,它包装了原始函数的调用。它可以执行原始函数之前和之后的代码。
  • @my_decorator 是装饰器语法。它相当于 say_hello = my_decorator(say_hello)。
  • 基于类的基本装饰器语法

    这些使用类而不是函数来定义装饰器。

    class MyDecorator:
        def __init__(self, func):
            self.func = func
    
        def __call__(self, *args, **kwargs):
            # Do something before calling the decorated function
            print("Before function call")
            result = self.func(*args, **kwargs)
            # Do something after calling the decorated function
            print("After function call")
            return result
    
    @MyDecorator
    def say_hello(name):
        print(f"Hello, {name}!")
    
    say_hello("World")

    **解释:**

  • MyDecorator 是一个充当装饰器的类。
  • __init__ 方法存储需要装饰的函数。
  • __call__ 方法使类实例可调用,允许它像函数一样使用。
  • B)实现一个简单的装饰器

    装饰器的基本概念是,它们是以另一个函数作为参数并扩展其行为而不显式修改它的函数。

    这是最简单的形式:

    def my_decorator(func):
        def wrapper():
            print("Something is happening before the function is called.")
            func()
            print("Something is happening after the function is called.")
        return wrapper
    
    # Using the decorator with @ syntax
    @my_decorator
    def say_hello():
        print("Hello!")
    
    # When we call say_hello()
    say_hello()
    
    # This is equivalent to:
    # say_hello = my_decorator(say_hello)

    C)实现带参数的装饰器

    让我们创建一个装饰器来记录函数的执行时间:

    def decorator_with_args(func):
        def wrapper(*args, **kwargs):    # Accept any number of arguments
            print(f"Arguments received: {args}, {kwargs}")
            return func(*args, **kwargs)  # Pass arguments to the original function
        return wrapper
    
    @decorator_with_args
    def greet(name, greeting="Hello"):
        print(f"{greeting}, {name}!")
    
    greet("Alice", greeting="Hi")  # Prints arguments then "Hi, Alice!"

    D)实现参数化装饰器

    这些是可以接受自己的参数的装饰器:

    def repeat(times):
        def decorator(func):
            def wrapper(*args, **kwargs):
                for _ in range(times):
                    result = func(*args, **kwargs)
                return result
            return wrapper
        return decorator
    
    @repeat(times=3)
    def greet(name):
        print(f"Hello {name}")
        return "Done"
    
    greet("Bob")  # Prints "Hello Bob" three times

    E)实现类装饰器

    def singleton(cls):
        instances = {}
        def get_instance(*args, **kwargs):
            if cls not in instances:
                instances[cls] = cls(*args, **kwargs)
            return instances[cls]
        return get_instance
    
    @singleton
    class DatabaseConnection:
        def __init__(self):
            print("Initializing database connection")
    
    # Creating multiple instances actually returns the same instance
    db1 = DatabaseConnection()  # Prints initialization
    db2 = DatabaseConnection()  # No initialization printed
    print(db1 is db2)  # True

    F)实现方法装饰器

    这些是专门为类方法设计的:

    def debug_method(func):
        def wrapper(self, *args, **kwargs):
            print(f"Calling method {func.__name__} of {self.__class__.__name__}")
            return func(self, *args, **kwargs)
        return wrapper
    
    class MyClass:
        @debug_method
        def my_method(self, x, y):
            return x + y
    
    obj = MyClass()
    print(obj.my_method(5, 3))

    G)实现装饰器链

    可以将多个装饰器应用于单个函数:

    def bold(func):
        def wrapper():
            return "" + func() + ""
        return wrapper
    
    def italic(func):
        def wrapper():
            return "" + func() + ""
        return wrapper
    
    @bold
    @italic
    def greet():
        return "Hello!"
    
    print(greet())  # Outputs: Hello!

    **解释:**

  • 装饰器从下往上应用。
  • 它更像我们在数学中做的:f(g(x))。
  • 先用斜体,然后加粗。
  • H)如果我们不使用@functools.wraps会发生什么?

    `functools.wraps` 装饰器(参见文档)是一个辅助函数,当您使用装饰器包装原始函数时,它会保留原始函数的元数据(如其名称、文档字符串和签名)。如果您不使用它,您将丢失这些重要信息。

    **例子:**

    def my_decorator(func):
        def wrapper(*args, **kwargs):
            """Wrapper docstring"""
            return func(*args, **kwargs)
        return wrapper
    
    @my_decorator
    def my_function():
        """My function docstring"""
        pass
    
    print(my_function.__name__)
    print(my_function.__doc__)

    **输出:**

    wrapper
    Wrapper docstring

    **问题:**

  • 原始函数的名称(my_function)和文档字符串(“My function docstring”)丢失。
  • 这会使调试和自省变得困难。
  • **解决方案:使用 functools.wraps):**

    import functools
    
    def my_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            """Wrapper docstring"""
            return func(*args, **kwargs)
        return wrapper
    
    @my_decorator
    def my_function():
        """My function docstring"""
        pass
    
    print(my_function.__name__)
    print(my_function.__doc__)

    **输出:**

    my_function
    My function docstring

    functools.wraps 的好处:

  • 保留函数元数据。
  • 提高代码的可读性和可维护性。
  • 使调试更容易。
  • 帮助使用自省工具和文档生成器。
  • 一、具有状态的装饰器

    装饰器还可以在函数调用之间保持状态。这对于缓存或计数函数调用等场景特别有用。

    python
    def count_calls(func):
        def wrapper(*args, **kwargs):
            wrapper.calls += 1
            print(f"Call {wrapper.calls} of {func.__name__}")
            return func(*args, **kwargs)
        wrapper.calls = 0
        return wrapper
    
    @count_calls
    def say_hello():
        print("Hello!")
    
    say_hello()  # Call 1 of say_hello
    say_hello()  # Call 2 of say_hello

    **输出:**

    Call 1 of say_hello
    Hello!
    Call 2 of say_hello
    Hello!

    解释:

    包装函数维护一个计数器(调用),每次调用被装饰的函数时,该计数器就会递增。

    这是一个如何使用装饰器来维护状态的简单示例。

    J)Python 装饰器的最佳实践

  • 使用 functools.wraps:始终在装饰器中使用 @functools.wraps 来保留原始函数的元数据。
  • 保持装饰器简单:理想情况下,装饰器应该只做一件具体的事情,并把它做好。这使得它们更易于重用和理解。
  • 记录您的装饰器:解释您的装饰器的作用、它接受的参数以及它返回什么。
  • 测试您的装饰器:编写单元测试以确保您的装饰器在各种场景中都能按预期工作。
  • 考虑链接的顺序:链接多个装饰器时要注意顺序,因为它会影响执行流程。
  • K)糟糕的实现(反模式)

  • 过于复杂的装饰器:避免创建过于复杂或试图做太多事情的装饰器。这会使它们难以理解、维护和调试。
  • 忽略 functools.wraps:忘记使用 @functools.wraps 会导致函数元数据丢失,从而引起自省和调试问题。
  • 副作用:装饰器除了修改被装饰的函数之外,最好不要有其他意想不到的副作用。
  • 硬编码值:避免在装饰器内对值进行硬编码。相反,使用装饰器工厂使它们可配置。
  • 未正确处理参数:如果装饰器要与多种函数一起使用,请确保包装函数可以使用 *args 和 **kwargs 处理任意数量的位置参数和关键字参数。
  • L)10. 真实世界用例

  • 日志记录:记录函数调用、参数和返回值以供调试或审计。
  • 计时:测量函数的执行时间以进行性能分析。
  • 缓存:存储昂贵的函数调用的结果以避免冗余计算(记忆)。
  • 身份验证和授权:在执行功能之前验证用户凭据或权限。
  • 输入验证:检查传递给函数的参数是否满足特定标准。
  • 速率限制:控制特定时间段内调用函数的次数。
  • 重试逻辑:如果由于临时错误而失败,则自动重试函数调用。
  • 框架特定任务:Flask 和 Django 等框架使用装饰器进行路由(将 URL 映射到函数)、注册插件等。
  • M)精选 Python 装饰器列表

    您可以在下面找到精选的 Python 装饰器列表:

  • 很棒的 Python 装饰器
  • Python 装饰器库
  • 11.结论

    装饰器是 Python 中一个强大而优雅的功能,它允许您以干净和声明的方式增强函数和类​​。

    通过了解原则、最佳实践和潜在的陷阱,您可以有效地利用装饰器来编写更模块化、更可维护、更具表现力的代码。

    它们是任何 Python 程序员工具库中的宝贵工具,尤其是在使用框架或构建可重用组件时。