Kodokon kodokon.com

装饰器与高阶函数

把函数当作一等对象来使用,从而编写健壮、可带参数且透明的装饰器。

10 分钟 · 3 题

在 Kodokon 中打开本课

在 Python 中,函数就是一个和其他对象没什么两样的对象:你可以把它赋给变量、当作参数传递、把它返回。高阶函数接收或返回一个函数 - sorted(data, key=...) 就是其中之一。再加上闭包 - 内层函数捕获外层函数的变量,并在外层函数返回之后依然让它们存活 - 你就集齐了装饰器的全部要素。

PYTHON
def make_multiplier(factor):
    def multiply(value):
        return value * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))  # 20 30
一个闭包:multiply 捕获了 factor

装饰器是一个接收函数并返回另一个函数的函数 - 它通常返回一个 wrapper,在调用的前后添加行为。写在 def slow_sum 上方的 @timed 语法,严格来说只是 slow_sum = timed(slow_sum) 的语法糖。wrapper 接收 *args, kwargs,从而对被装饰函数的签名保持中立,并且要返回**被装饰函数的结果 - 忘掉这一步是个经典错误,它会把所有返回值都变成 None

PYTHON
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        delay = time.perf_counter() - start
        print(f"{func.__name__}: {delay:.4f}s")
        return result
    return wrapper

@timed
def slow_sum(n):
    return sum(range(n))

print(slow_sum(1_000_000))
一个可直接用于生产的计时装饰器

要让装饰器带参数(@repeat(times=3)),你还需要再多一层:一个工厂函数,它接收这些参数并返回装饰器,装饰器再返回 wrapper。三层嵌套函数 - 这就是灵活性的代价。如果状态变得复杂,一个带 __call__ 方法的类往往更易读。请把装饰器留给那些横切关注点:日志、缓存、重试、访问控制。一个会改变业务逻辑的装饰器,会把最关键的东西藏起来,让读代码的人看不见。

PYTHON
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello {name}")

greet("Ada")
带参数的装饰器:工厂函数、装饰器、wrapper

知识检测

确认你已牢记本课的重点内容。

  1. 写在 def slow_sum 上方的 @timed 语法等价于什么?
    • timed(slow_sum())
    • slow_sum = timed(slow_sum)
    • slow_sum.timed = True
  2. @functools.wraps 解决了什么问题?
    • 它加快了对被装饰函数的调用
    • 它保留了被装饰函数的名字和 docstring
    • 它让装饰器能兼容类的方法
  3. 在 @repeat(times=3) 中,repeat(times=3) 这次调用返回的是什么?
    • 被装饰函数的结果
    • 一个装饰器,准备好接收那个函数
    • 直接就是最终的 wrapper
    • 一个包含三个结果的列表