解释(带有示例和用例)

Python 装饰器详解

Python 装饰器是 Python 语言中一种极其强大的工具。 通过使用装饰器,我们能够修改函数的行为,而无需直接更改函数本身的代码。 这种机制通过将函数包装在另一个函数中实现,从而允许我们编写更加简洁、可维护且易于重用的代码。 本文不仅会详细介绍如何使用装饰器,还会深入探讨如何创建自定义的装饰器。

必备知识

为了更好地理解 Python 装饰器,你需要具备一些基础知识。 以下是一些你需要熟悉的概念,并附带相关资源以便回顾。

基础 Python 知识

装饰器属于中高级主题,因此在学习之前,你需要对 Python 的基础知识有充分的掌握,例如数据类型、函数、对象和类。 此外,了解面向对象编程中的 getter、setter 和构造函数等概念也是必要的。 如果你不熟悉 Python 编程,可以参考一些入门教程。

函数作为一等公民

除了基础知识,你还需要理解 Python 中一个更高级的概念:函数是一等公民。 这意味着函数在 Python 中被视为对象,就像整数、字符串一样。 由于它们是对象,因此可以进行以下操作:

  • 函数可以作为参数传递给其他函数。
  • 函数可以作为返回值从其他函数返回。
  • 函数可以被存储在变量中。

函数对象与其他对象的唯一区别是它包含魔术方法 __call__()。 希望你已经对这些预备知识感到满意,接下来,我们将深入探讨装饰器的核心概念。

什么是 Python 装饰器?

简单来说,Python 装饰器是一个函数,它接收一个函数作为输入,并返回一个经过修改的新函数。 换句话说,如果函数 decorate 接收函数 original 作为参数,那么 decorate 就是一个装饰器,它会返回一个基于 original 修改的函数 modifiedmodified 函数在调用 original 函数之前或之后可以执行其他操作。以下代码示例能够更直观地说明这个过程:

    # decorate 是一个装饰器,它接收另一个函数 original 作为参数
    def decorate(original):
        # 创建 modified,这是 original 的修改版本
        # modified 会调用 original,并且可以在调用前后执行其他操作
        def modified():
            # 在调用 original 之前,打印一些信息
            print("Before Original function")
            # 调用 original
            original()
            # 在调用 original 之后,打印一些信息
            print("After Original function")
        # 最后,decorate 返回 modified,这是 original 的修改版本
        return modified

如何在 Python 中创建装饰器?

为了更好地理解如何在 Python 中创建和使用装饰器,我们将通过一个简单的日志记录装饰器示例来演示。 此装饰器将在每次运行时记录被装饰的函数的名称。

首先,我们需要创建装饰器函数。装饰器函数接收一个名为 func 的参数,该参数表示被装饰的函数。

    def create_logger(func):
        # 函数体在这里

在装饰器函数中,我们将创建一个经过修改的函数。此修改后的函数将在调用原始函数之前,记录原始函数的名称。

    # 在 create_logger 内部
    def modified_func():
        print("Calling: ", func.__name__)
        func()

接下来,create_logger 函数将返回修改后的函数。因此,完整的 create_logger 函数如下所示:

    def create_logger(func):
        def modified_func():
            print("Calling: ", func.__name__)
            func()
        return modified_func

至此,我们已经成功创建了一个装饰器。 create_logger 函数是一个简单的装饰器示例。 它接收要装饰的函数 func,并返回另一个函数 modified_funcmodified_func 在运行 func 之前会记录 func 的名称。

如何在 Python 中使用装饰器

要使用装饰器,我们需要使用 @ 语法,如下所示:

    @create_logger
    def say_hello():
        print("Hello, World!")

现在,当我们在脚本中调用 say_hello() 时,输出应该是:

    Calling:  say_hello
    "Hello, World"

@create_logger 实际上在做什么呢? 它正在将装饰器应用到 say_hello 函数。为了更好地理解其背后的机制,下面的代码实现的效果与在 say_hello 前面加上 @create_logger 是一样的。

    def say_hello():
        print("Hello, World!")

    say_hello = create_logger(say_hello)

换句话说,在 Python 中,使用装饰器的一种方式是显式地调用装饰器函数,并将要装饰的函数作为参数传入,就像我们在上面的代码中所做的那样。另一种更加简洁的方式是使用 @ 语法。

在本节中,我们已经介绍了如何在 Python 中创建装饰器。

稍微复杂一点的例子

上面的例子是一个简单的示例。还有一些稍显复杂的例子,例如当被装饰的函数接收参数时。另一种更复杂的情况是当你想装饰整个类时。我将在这里涵盖这两种情况。

当函数接受参数时

当被装饰的函数接收参数时,修改后的函数也应该接收这些参数,并在最终调用原始函数时将它们传递过去。如果这让你感到困惑,我们可以用 foo-bar 术语来解释一下。

回想一下,foo 是装饰器函数,bar 是我们正在装饰的函数,而 baz 是被装饰后的 bar。在这种情况下,bar 将接收参数,并在调用 baz 时将它们传递给 baz。以下代码示例能够帮助巩固这个概念:

    def foo(bar):
        def baz(*args, **kwargs):
            # 你可以在这里做一些事情
            ...
            # 然后,调用 bar,并将 args 和 kwargs 传递给它
            bar(*args, **kwargs)
            # 你也可以在这里做一些事情
            ...
        return baz

如果 *args**kwargs 看起来不太熟悉,它们分别是指向位置参数和关键字参数的指针。

需要注意的是,baz 可以访问参数,因此可以在调用 bar 之前对参数执行一些验证。

举个例子,如果我们有一个装饰器函数 ensure_string,它将确保传递给它所装饰的函数的参数是一个字符串,我们可以像这样实现它:

    def ensure_string(func):
        def decorated_func(text):
            if type(text) is not str:
                 raise TypeError('argument to ' + func.__name__ + ' must be a string.')
            else:
                 func(text)
        return decorated_func

我们可以像这样装饰 say_hello 函数:

    @ensure_string
    def say_hello(name):
        print('Hello', name)

然后,我们可以使用以下代码进行测试:

    say_hello('John')  # 应该可以正常运行
    say_hello(3)      # 应该抛出异常

它应该会产生以下输出:

    Hello John
    Traceback (most recent call last):
       File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # should throw an exception
       File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argument to + func._name_ + must be a string.')
    TypeError: argument to say hello must be a string. $0

正如预期的那样,该脚本成功打印了 “Hello John”,因为 “John” 是一个字符串。而尝试打印 “Hello 3” 时会抛出异常,因为 “3” 不是字符串。 ensure_string 装饰器可用于验证任何需要字符串的函数的参数。

装饰类

除了装饰函数,我们还可以装饰类。 当你给类添加装饰器时,装饰方法会替换类的构造函数/启动器方法 (__init__)。

回到 foo-bar 示例,假设 foo 是我们的装饰器,Bar 是我们要装饰的类,那么 foo 会装饰 Bar.__init__。 这在你希望在实例化 Bar 类型的对象之前执行某些操作时会非常有用。

这意味着下面的代码:

    def foo(func):
        def new_func(*args, **kwargs):
            print('Doing some stuff before instantiation')
            func(*args, **kwargs)
        return new_func

    @foo
    class Bar:
        def __init__(self):
            print("In initiator")

等同于:

    def foo(func):
        def new_func(*args, **kwargs):
            print('Doing some stuff before instantiation')
            func(*args, **kwargs)
        return new_func

    class Bar:
        def __init__(self):
            print("In initiator")

    Bar.__init__ = foo(Bar.__init__)

实际上,实例化 Bar 类的对象(使用这两种方式中的任何一种定义)都会得到相同的输出:

    Doing some stuff before instantiation
    In initiator

Python 中的示例装饰器

虽然你可以定义自己的装饰器,但 Python 中已经内置了一些装饰器。 以下是一些在 Python 中你可能会遇到的常见装饰器:

@staticmethod

静态方法用于类上,表示它修饰的方法是一个静态方法。 静态方法是无需实例化类即可运行的方法。 在下面的代码示例中,我们创建了一个带有静态方法 barkDog 类。

    class Dog:
        @staticmethod
        def bark():
            print('Woof, woof!')

现在,可以像这样访问 bark 方法:

    Dog.bark()

运行代码会产生以下输出:

    Woof, woof!

正如我在“如何使用装饰器”一节中提到的,有两种方法可以使用装饰器。 @ 语法更加简洁,是其中一种。 另一种方法是调用装饰器函数,并将我们要装饰的函数作为参数传入。 这意味着上面的代码实现了与下面的代码相同的功能:

    class Dog:
        def bark():
            print('Woof, woof!')
    Dog.bark = staticmethod(Dog.bark)

我们仍然可以用同样的方式使用 bark 方法:

    Dog.bark()

它会产生相同的输出:

    Woof, woof!

如你所见,第一种方法更加清晰,而且在开始阅读代码之前,就更明显地看出该函数是一个静态函数。 因此,对于其余示例,我将使用第一种方法。 但请记住,第二种方法也是一种选择。

@classmethod

此装饰器用于表示它修饰的方法是一个类方法。 类方法类似于静态方法,因为它们都不需要在调用它们之前实例化类。

然而,主要的区别在于类方法可以访问类属性,而静态方法则不能。 这是因为无论何时调用类方法,Python 都会自动将类作为第一个参数传递给类方法。要在 Python 中创建类方法,我们可以使用 classmethod 装饰器。

    class Dog:
        @classmethod
        def what_are_you(cls):
            print("I am a " + cls.__name__ + "!")

要运行代码,我们只需要调用方法,而无需实例化类:

    Dog.what_are_you()

输出是:

    I am a Dog!

@property

property 装饰器用于将方法标记为属性的获取器。 回到我们的 Dog 示例,让我们创建一个方法来检索 Dog 的名字。

    class Dog:
        # 创建一个构造函数方法,接收狗的名字作为参数
        def __init__(self, name):
            # 创建一个私有属性 name
            # 双下划线使该属性变为私有
            self.__name = name

        @property
        def name(self):
            return self.__name

现在,我们可以像访问普通属性一样访问狗的名字:

    # 创建类的一个实例
    foo = Dog('foo')

    # 访问 name 属性
    print("The dog's name is:", foo.name)

运行代码的结果是:

    The dog's name is: foo

@property.setter

property.setter 装饰器用于为我们的属性创建设置器方法。 要使用 @property.setter 装饰器,你需要将 property 替换为你正在为其创建设置器的属性的名称。 例如,如果你正在为属性 foo 的方法创建设置器,那么你的装饰器将是 @foo.setter。 这里有一个 Dog 的例子来阐明:

    class Dog:
        # 创建一个构造函数方法,接收狗的名字作为参数
        def __init__(self, name):
             # 创建一个私有属性 name
             # 双下划线使该属性变为私有
             self.__name = name

        @property
        def name(self):
            return self.__name

        # 为我们的 name 属性创建一个设置器
        @name.setter
        def name(self, new_name):
             self.__name = new_name

要测试设置器,我们可以使用以下代码:

    # 创建一条新的狗
    foo = Dog('foo')

    # 更改狗的名字
    foo.name="bar"

    # 将狗的新名字打印到屏幕上
    print("The dog's new name is:", foo.name)

运行代码将产生以下输出:

    The dogs's new name is: bar

装饰器在 Python 中的重要性

现在,我们已经了解了装饰器的本质,并且你已经看到了一些装饰器的示例,接下来我们可以讨论为什么装饰器在 Python 中如此重要。 装饰器之所以重要,有以下几个原因:

  • 它们支持代码的重用性:在上面给出的日志记录示例中,我们可以在任何我们想要函数上使用 @create_logger。 这允许我们为所有函数添加日志记录功能,而无需为每个函数手动编写。
  • 它们允许你编写模块化的代码:再次回到日志示例,通过使用装饰器,你可以将核心功能(在本例中为 say_hello)与你需要的其他功能(在本例中为日志记录)分开。
  • 它们增强了框架和库:装饰器广泛用于 Python 框架和库中,以提供附加的功能。 例如,在 Flask 或 Django 等 Web 框架中,装饰器用于定义路由、处理身份验证或将中间件应用于特定视图。

总结

装饰器非常有用,你可以使用它们来扩展功能,而无需更改它们的核心实现。 这在你想要为函数的性能计时、记录调用函数的时间、在调用函数之前验证参数,或在运行函数之前验证权限时非常有用。 掌握装饰器之后,你将能够以更加简洁的方式编写代码。

接下来,你可能想阅读我们关于元组和在 Python 中使用 cURL 的文章。