欢迎,来自IP地址为:216.73.217.85 的朋友


Python 装饰器 允许在不修改函数和方法实际代码的情况下,修改或扩展它们的行为。使用 Python 装饰器时,实际上是用另一个函数包装一个函数,该函数接受原始函数作为参数,并返回其修改后的版本。这种技术提供了一种在 Python 中实现高阶函数的简单方法,从而提高了代码的重用性和可读性。

Python 函数

要理解装饰器,首先必须了解函数的一些基本工作原理。函数有很多方面,但在装饰器的上下文中,函数会根据给定的参数返回一个值。以下是一个简单的示例:

def add_one(number):
    return number + 1
    
add_one(2)

通常情况下,Python 中的函数不仅仅是将输入转换为输出,还可能产生其他作用。”print()” 函数就是一个例子:它返回”None”,同时具有向控制台输出内容。若要理解装饰器,只需要将函数视为将给定参数转换为值的工具即可。

一级对象

在函数式编程中,我们几乎完全使用没有其他效果的纯函数。虽然 Python 并非纯粹的函数式编程语言,但它支持许多函数式编程概念,其中包括将函数视为一级对象。

这意味着函数可以像字符串(str)、整数(int)、浮点数(float)、列表(list)等其他任何对象一样,被传递和作为参数。请看以下三个函数:

def say_hello(name):
    return f"Hello {name}"

def be_awesome(name):
    return f"Yo {name}, together we're the awesomest!"

def greet_bob(greeter_func):
    return greeter_func("Bob"))

在此示例中,”say_hello()”和”be_awesome()”是常规函数,它们接收一个字符串形式的名称作为参数。而”greet_bob()”函数则接收一个函数作为参数。例如,可以将”say_hello()”或”be_awesome()”函数传递给它。

若要测试这些函数,则可以通过如下方式调用”greet_bob()”:

print(greet_bob(say_hello))

print(greet_bob(be_awesome))

注意,”greet_bob(say_hello)” 涉及 “greet_bob()”和”say_hello”这两个函数,但它们的使用方式不同。”say_hello”函数在书写时没有带括号,这意味着传递的仅仅是该函数的引用,而函数本身并未执行。相比之下,”greet_bob()”函数带有括号,因此会按常规方式被调用。

这是一个重要的区别,对于理解函数如何作为”一级对象”(first-class objects)运作至关重要。不带括号的函数名代表对函数的引用,而带有括号的函数名则表示调用该函数并指向其返回值。

内部函数

可以在其他函数内部定义函数。这类函数被称为内部函数。下面是一个包含两个内部函数的示例:

def parent():
    print("Printing from parent()")

    def first_child():
        print("Printing from first_child()")

    def second_child():
        print("Printing from second_child()")

    second_child()
    first_child()

parent()

调用”parent()”函数时会发生什么?请先思考一下,代码的输出结果如下:

注意,内部函数的定义顺序无关紧要。与其他任何函数一样,只有在执行这些内部函数时,才会进行打印输出。

此外,内部函数只有在父函数被调用时才会定义。它们的作用域局限于”parent()”内部,这意味着它们仅作为局部变量存在于”parent()”函数内部。每当调用”parent()”时,内部函数”first_child()”和”second_child()”也会被调用。但由于它们处于局部作用域内,因此在”parent()”函数外部无法访问这些函数。

函数作为返回值

Python 也允许从函数中返回其他函数。在下面的示例中,我们重写”parent()”函数,使其返回其中一个内部函数:

def parent(num):
    def first_child():
        return "Hi, I'm Elias"

    def second_child():
        return "Call me Ester"

    if num == 1:
        return first_child
    else:
        return second_child

请注意,这里返回的”first_child”没有带括号,这意味着返回的是对函数”first_child”的引用;相反,带括号的”first_child()”指的是函数执行的结果。可以在以下示例中看到这一点:

first = parent(1)
second = parent(2)
print(first)
print(second)
/* Output */
<function parent.<locals>.first_child at 0x0000000001393110>
<function parent.<locals>.second_child at 0x0000000001393270>

这种略显晦涩的输出结果意味着,第一个结果指向”parent()”内部的局部函数”first_child()”,而第二个结果则指向”second_child()”。

当然,我们可以像使用普通函数那样来使用”first”和”second”,尽管我们无法直接访问它们所指向的那些函数:

print(first())
print(second())
/* Output */
Hi, I'm Elias
Call me Ester

这样就可以得到在”parent()”内部定义的那些内部函数的返回值。

最后请注意,在之前的示例中,只是在父函数内部执行了这些内部函数(例如”first_child()”);而在后一个示例中,在返回这些内部函数(如”first_child”)时并没有加上括号。这样一来,就获得的是每个函数的引用,以便日后调用。

Python 中的简单装饰器

既然已经了解了 Python 中的函数与其他对象并无二致,现在就可以继续探索 Python 中的”神器”——装饰器了。我们将从一个示例开始:

def 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

def say_whee():
    print("Whee!")

say_whee = decorator(say_whee)
print(say_whee())

在这里,先定义了两个普通函数”decorator()”和”say_whee()”,以及一个内部函数”wrapper()”。随后,我们重新定义了”say_whee”,将”decorator()”应用于原本的”say_whee()”。

我想大家能猜出调用”say_whee()”时会发生什么,使用了装饰器的输出结果如下:

要理解这里发生的事情,请回顾之前的例子。所谓的”装饰”发生在下面这一行:

say_whee = decorator(say_whee)

实际上,名称”say_whee”现在指向了”wrapper()”内部函数。请记住,当调用”decorator(say_whee)”时,实际返回的是”wrapper”函数。

然而,”wrapper()”函数引用了原始的”say_whee()”函数,并将其命名为”func”,它会在两次调用”print()”函数之间调用该函数。

简而言之,装饰器会包装一个函数,从而改变其行为。

在继续之前,我们来看下面的例子。由于”wrapper()”是一个普通的 Python 函数,装饰器修改函数的方式可以动态改变。例如为了不打扰邻居,下面的例子将只在白天运行被装饰的代码:

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)
print(say_whee())

如果在”7-22时”之外时间尝试调用”say_whee()”,将不会有任何反应,此时,”say_whee()”不会输出任何内容。这是因为”if”条件判断未通过,导致包装器没有调用”func()”(即原始的”say_whee()”)。

添加语法糖

回想一下,在之前示例中编写的代码,装饰”say_whee()”的方式显得有些繁琐:首先,需要三次输入”say_whee”这个名称;其次,装饰逻辑被放置在函数定义之后,显得有些隐蔽。

Python 允许使用”@”符号(有时被称为 pie 语法)以更简洁的方式使用装饰器。下面的示例实现的功能与第一个装饰器示例完全相同:

def 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

@decorator
def say_whee():
    print("Whee!")

print(say_whee())

所以,”@decorator”只是”say_whee = decorator(say_whee)”的一种简写形式,这就是将装饰器应用于函数的方式。

复用装饰器

回想一下,装饰器本质上只是普通的 Python 函数,因此所有用于代码复用的常规手段同样适用于装饰器。接下来,我们将创建一个专门存放装饰器的模块,以便在其他多个函数中复用它们。示例代码如下:

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice

@do_twice
def say_whee():
    print("Whee!")

print(say_whee())

“do_twice()”装饰器会调用被装饰的函数两次。

注意:我们可以随意命名内部函数,通常使用像”wrapper()”这样的通用名称也没问题。

运行此示例时,应该会看到原始的”say_whee()”被执行了两次。

带有参数函数的装饰器

假设现在有一个接收参数的函数。还能给它添加装饰器吗?不妨试一试:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")

print(greet("DAEHUB"))

由于”greet()”函数带有参数,于是在装饰器的内部包装函数中使用 *args 和 **kwargs,这样它就可以接受任意数量的位置参数和关键字参数。wrapper_do_twice() 内部函数现在可以接受任意数量的参数,并将它们传递给它所修饰的函数。

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

@do_twice
def say_whee():
    print("Whee!")

@do_twice
def greet(name):
    print(f"Hello {name}")

print(say_whee())
print(greet('DAEHUB'))

如上面的示例代码所示,通过使用接收参数形式的接收器,就可以使用了同一个装饰器”@do_twice”来装饰两个不同的函数。这体现了装饰器的强大之处之一:它们可以为多个不同的函数添加可应用的行为。

从装饰函数返回值

被装饰函数的返回值会怎样呢?这取决于装饰器如何处理。假设像下面这样装饰了一个简单的函数:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

print(return_greeting('DAEHUB'))

此时,装饰器虽然可以接收被装饰函数的参数,但它并不能返回被装饰函数的返回值,想要得到被装饰函数的返回值,则对代码修改如下:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

print(return_greeting('DAEHUB'))
print(return_greeting)

然而,在被装饰之后,被装饰函数却”迷失”了自我。它会报告自己是装饰器内部的函数。虽然从技术角度看这确实没错,但这并非特别有用的信息。

为了解决这个问题,装饰器应当使用”@functools.wraps”装饰器,它能保留原始函数的相关信息。再次更新示例代码如下:

import functools

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)

    return wrapper_do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

print(return_greeting('DAEHUB'))
print(return_greeting)

这样好多了!现在,”say_whee()”在被装饰之后依然保持着它原本的特征。

注意:”@functools.wraps”装饰器利用”functools.update_wrapper()”来更新诸如”__name__”和”__doc__”这样的特殊属性,这些属性在进行内省(introspection)时会被用到。

至此,我们已经掌握了创建装饰器的基础知识。不过,”@do_twice”并不是一个特别出彩的装饰器,其实际应用场景也相当有限。之后,我们将实现几个装饰器,它们既能巩固目前所学的知识,也能在实际代码中派上用场。

几个实际应用示例

接下来,我们将探讨几个更实用的装饰器示例。仔细研究后会发现,它们主要遵循的依然是迄今为止所学的相同模式:

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

这一模式是构建更复杂装饰器的一个很好的样板。

计时函数

首先,我们需要创建一个”@timer”装饰器。它将测量函数的执行时间,并将耗时打印到控制台。代码如下:

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__}() in {run_time:.4f} secs")
        return value
    return wrapper_timer

该装饰器的工作原理是:首先记录函数开始运行前的时间,之后记录函数运行结束后的时间。随后,计算这两个时间点之差,即为函数的运行时间。这里使用了 `time.perf_counter()`,它非常适合用于测量时间间隔。

现在,添加一个名为”waste_some_time()”的函数作为示例(该函数会耗费一定时间),以便测试”@timer”装饰器。以下是一些计时示例:

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__}() in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([number**2 for number in range(10_000)])

waste_some_time(100)

试着自己运行一下代码,逐行研读”@timer”的定义,确保理解其工作原理。不过,如果一时无法完全理解也不必担心——装饰器属于进阶概念。不妨先放一放,或者画个图来梳理程序的执行流程。

注意:如果只是想大致了解函数的运行时间,”@timer”装饰器非常实用。但若需要对代码进行更精确的测量,则应考虑使用标准库中的”timeit”模块;该模块会暂时禁用垃圾回收并进行多次运行测试,从而消除短函数调用带来的干扰噪声。

调试代码

以下”@debug”装饰器会在每次调用函数时,打印出该函数的参数及其返回值:

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__}() returned {repr(value)}")
        return value
    return wrapper_debug

该签名是通过拼接所有参数的字符串表示形式生成的:

  • 首先创建一个包含位置参数的列表。使用”repr()”获取每个参数的良好字符串表示形式
  • 然后创建一个包含关键字参数的列表。使用 f-string 将每个参数格式化为”key=value”的形式,同样使用”repr()”来表示参数值
  • 之后将位置参数列表和关键字参数列表合并为一个签名字符串,各参数之间用逗号分隔
  • 最后在函数执行后打印其返回值

现在,让我们通过将该装饰器应用于一个包含一个位置参数和一个关键字参数的简单函数,来看看它在实际中是如何运作的:

import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__}() returned {repr(value)}")
        return value
    return wrapper_debug

@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you're growing up!"

make_greeting("DAEHUB",20)

这个例子乍看之下似乎没什么实际用处,因为 `@debug` 装饰器仅仅是原样重复了你编写的代码。然而,当它应用于那些你不会直接调用的辅助性小函数时,便能发挥更大的威力。

下面的例子展示了如何计算数学常数 e 的近似值。

根据自然常数 e 的计算公式:

编写如下计算程序:

import functools
import math

def debug(func):
    """Print the function signature and return value"""

    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__}() returned {repr(value)}")
        return value

    return wrapper_debug

math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))

print(approximate_e(5))

在这个例子中,仅需累加5 项,就能得到真实值 e ≈ 2.718281828 的相当不错的近似值。

让代码运行变慢

现在,我们将创建一个能让代码运行变慢的装饰器。这看起来似乎没什么用处——毕竟,为什么要特意让 Python 代码运行变慢呢?

最常见的应用场景之一,是对那些持续检查资源(例如网页)是否发生变化的函数进行速率限制。”@slow_down”装饰器会在调用被装饰的函数之前,先让程序暂停(休眠)一秒钟:

import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

在”@slow_down”中,调用了”time.sleep()”函数,让代码在调用被装饰的函数之前先暂停一下。为了观察”@slow_down”装饰器是如何工作的,可以创建一个”countdown()”函数,就可以直观感受代码变慢的效果:

import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)

countdown(3)

在”countdown()”函数中,首先检查”from_number”是否小于 1。如果小于 1,则打印”Liftoff!”。否则,打印当前数字并继续计数。

注意:”countdown()”函数是一个递归函数。换句话说,它是一个调用自身的函数。”@slow_down”装饰器始终会休眠​​一秒钟。

注册插件

装饰器不必包装它们所装饰的函数。它们也可以简单地注册一个函数的存在,并返回未包装的函数。例如,我们可以使用此方法创建轻量级的插件架构:

PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

“@register”装饰器仅将对被装饰函数的引用存储在全局”PLUGINS”字典中。之后,就可以按如下方式注册函数:

PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we're the awesomest!"

print(say_hello("DAEHUB"))
print(be_awesome("DAEHUB"))

注意,PLUGINS 字典中已经包含了对每个已注册为插件的函数对象的引用。

Python 在定义函数时会应用装饰器,因此 say_hello() 和 be_awesome() 会立即注册。然后,就可以使用插件来调用这些函数:

import random

PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we're the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)

print(randomly_greet("DAEHUB"))

“randomed_greet()”函数会随机选择一个已注册的函数来使用。这种简单的插件架构的主要优点在于,无需维护插件列表。该列表会在插件注册时自动创建。因此,添加新插件非常简单:只需定义函数并使用”@register”注解即可。

如果熟悉 Python 中的”globals()”函数,那么可能会发现它与插件架构有一些相似之处。使用”globals()” 函数,就可以访问当前作用域中的所有全局变量。

用户身份验证

在介绍一些更高级的装饰器之前,最后一个示例通常用于 Web 框架。在这个示例中,我们将使用 Flask 设置一个 /secret 网页,该网页仅对已登录或已通过身份验证的用户可见:

import functools
from flask import Flask, g, request, redirect, url_for

app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    @functools.wraps(func)
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
    pass

虽然这能让我们了解如何为 Web 框架添加身份验证,但通常情况下,我们不应该自己编写这类装饰器。对于 Flask,可以使用 Flask-Login 扩展,它能提供更高的安全性和更丰富的功能。

高级装饰器

到目前为止,我们已经了解了如何创建简单的装饰器。也已经对装饰器的概念和工作原理有了相当不错的理解。

装饰类

有两种不同的方法可以在类上使用装饰器。第一种方法与之前对函数所做的非常相似:可以装饰类的方法。这正是当初引入装饰器的初衷之一。

一些常用的装饰器甚至是 Python 内置的,包括”@classmethod”、”@staticmethod”和”@property”都可以用来装饰类方法。

同样,也可以定义一个类,并使用之前提到的”@debug”和”@timer”装饰器来装饰其中的某些方法。

在类上使用装饰器的另一种方法是装饰整个类。例如,”dataclasses”模块就是这样做的:

from dataclasses import dataclass

@dataclass
class PlayingCard:
    rank: str
    suit: str

这种语法的含义与函数装饰器类似。在示例中,我们也可以通过编写”PlayingCard = dataclass(PlayingCard)”来装饰该类。

类装饰器的一个常见用途是作为元类(metaclass)某些应用场景的更简单替代方案。在这两种情况下,都是在动态地修改类的定义。

编写类装饰器与编写函数装饰器非常相似。唯一的区别在于,装饰器接收的参数是一个类,而不是函数。事实上,前面提到的所有装饰器都可以用作类装饰器。不过,当把它们应用于类而非函数时,其效果可能并非所预期的那样。在下面的示例中,”@timer”装饰器被应用于一个类:

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""

    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__}() in {run_time:.4f} secs")
        return value

    return wrapper_timer

@timer
class TimeWaster:
    def __init__(self, max_num):
        self.max_num = max_num

    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i ** 2 for i in range(self.max_num)])

装饰一个类并不会装饰其方法。请记住,”@timer”只是 “TimeWaster = timer(TimeWaster)”的简写形式。在此例中,”@timer”仅测量实例化该类所需的时间。

嵌套装饰器

我们可以通过将多个装饰器层叠放置,同时将它们应用于同一个函数:

import functools

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={repr(v)}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__}() returned {repr(value)}")
        return value
    return wrapper_debug

@debug
@do_twice
def greet(name):
    print(f"Hello {name}")

greet("DAEGUB")

我们现在已经学习了如何创建装饰器的基础知识。然而,”@do_twice”并不是一个很强大的装饰器,它的应用场景也不多。下面,我们将实现几个装饰器,这些装饰器将展示目前所学的知识,并可以在自己的代码中使用。

定义带参数的装饰器

有时,向装饰器传递参数会很有用。例如,”@do_twice”装饰器可以扩展为”@repeat(num_times)”装饰器。然后,可以将要执行的函数的次数作为参数传递。

如果定义了”@repeat”,则可以这样做:

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

想想该如何实现”@repeat”函数呢。

目前为止,”@”后面的名称指的是一个可以被其他函数调用的函数对象。为了保持一致性,就需要”repeat(num_times=4)”返回一个可以作为装饰器的函数对象。幸运的是,我们已经知道如何返回函数了!一般来说,需要类似这样的结果:

def repeat(num_times):
    def decorator_repeat(func):
        ...  # Create and return a wrapper function
    return decorator_repeat

通常,装饰器会创建并返回一个内部包装函数,因此如果将该示例完整写出来,就会出现”函数套函数”的结构。这听起来可能像是编程界的《盗梦空间》(Inception),但很快就能理清其中的逻辑:

import functools

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

greet("DAEHUB")

这看起来可能有点杂乱,但其实只是把那个早已司空见惯的装饰器模式,放进了一个额外的”def”函数中,用来处理装饰器的参数。

首先,我们来看看最内层的函数”wrapper_repeat()”函数接收任意参数,并返回被装饰函数”func()”的执行结果。该包装函数内部包含一个循环,负责将被装饰函数调用”num_times”次。这与之前见过的包装函数并无二致,唯一的区别在于它使用了必须由外部传入的”num_times”参数。

同样地,”decorator_repeat()”看起来与之前编写的装饰器函数完全一样,只是名称不同。这是因为我们将基础名称”repeat()”留给了最外层的函数,也就是用户将要调用的那个函数。

正如我们已经看到的,最外层函数会返回装饰器函数的引用”return decorator_repeat”。

使用类作为装饰器

在 Python 中,维护状态的典型做法是使用类。回想一下,装饰器语法”@decorator”实际上只是”func = decorator(func)”的一种简写形式。因此,如果”decorator”是一个类,它需要在其 “.__init__()”初始化方法中接收”func”作为参数。此外,该类的实例必须是可调用的(callable),以便能够替代被装饰的函数。

注意:到目前为止,我们见过的所有装饰器都是作为函数定义的。这是创建装饰器最常见的方式。不过,也可以使用任何可调用的表达式作为装饰器。

若要使类实例可调用,需要实现特殊的”.__call__()”方法:

class Counter:
    def __init__(self, start=0):
        self.count = start

    def __call__(self):
        self.count += 1
        print(f"Current count is {self.count}")

每当我们尝试调用类的实例时,就会执行”.__call__()”方法。每次调用”counter()”时,计数值就会增加,状态也会发生变化。因此,装饰器类的典型实现应当包含”.__init__()”和”.__call__()”方法:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__}()")
        return self.func(*args, **kwargs)

“.__init__()”方法必须存储该函数的引用,同时也可以执行其他必要的初始化操作。”.__call__()”方法将被调用,以替代被装饰的函数;它的作用本质上与之前示例中的”wrapper()”函数相同。请注意,这里需要使用”functools.update_wrapper()”函数,而不是”@functools.wraps”。

这个”@CountCalls”装饰器的工作方式与之前的装饰器相同:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__}()")
        return self.func(*args, **kwargs)

@CountCalls
def say_whee():
    print("Whee!")

say_whee()
say_whee()

每次调用”say_whee()”都会被计数并记录下来。

这一路走来收获颇丰!本教程伊始,我们深入探讨了函数,特别是如何在其他函数内部定义函数,以及如何像对待其他任何 Python 对象一样传递它们。随后,学习了装饰器及其编写方法,以确保它们:

  • 可被复用
  • 能够装饰带有参数和返回值的函数
  • 利用”@functools.wraps”使得装饰器在行为特征上更像被装饰的函数

之后,我们接触了更高级的装饰器,并学习了如何:

  • 装饰类
  • 嵌套装饰器
  • 为装饰器添加参数
  • 在装饰器内部维护状态
  • 使用类作为装饰器

定义装饰器通常涉及定义一个返回”包装函数”(wrapper function)的函数。该包装函数利用”*args”和”**kwargs”将参数传递给被装饰的函数。若需让装饰器自身也接受参数,则需将包装函数嵌套在另一个函数内部;在这种情况下,代码中通常会出现三个”return”语句。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注