第 27 课 · 进阶 · 20 分钟

Python 装饰器

用可复用的行为包装函数,而不修改函数体。

装饰器包装函数functools.wraps@ 语法

每张订单都要过同样的三步:校验、收款、打印。Maya 想给这条路径计时,又不想改它。包装函数该放在哪里?

装饰器是一个接收函数并返回替代函数的函数。@ 语法是在定义时应用这种替代的简洁写法。包装函数适合做日志、计时、校验和权限检查,但应保留原函数的元数据。 你还会写带参数的装饰器、转发参数,并理解叠加顺序。

函数可以作为值传递

函数也是对象。可以把它存进变量、传给另一个函数,或作为返回值。装饰器正是利用这一点:接收原函数,并在它外面构造一个新的可调用对象。

def greet():
    return "hello"

def call(func):
    return func()

print(call(greet))

把函数作为参数

用 @ 包装函数

包装函数通过闭包记住 func,并在被包装函数调用时执行它。把 @announce 放在 greet 上方,等价于在定义之后写 greet = announce(greet)。

def announce(func):
    def wrapper():
        return "Before: " + func()
    return wrapper

@announce
def greet():
    return "hello"

print(greet())

装饰器语法

保留原函数的名字

包装函数替换了原函数对象,所以它的 __name__ 通常会变成 wrapper。functools.wraps 会把重要元数据复制回来。打算长期使用的装饰器都应该加上它。

from functools import wraps

def announce(func):
    @wraps(func)
    def wrapper():
        return func().upper()
    return wrapper

@announce
def greet():
    return "hello"

print(greet())
print(greet.__name__)

保留函数身份

装饰器工厂返回装饰器

装饰器需要配置时,在外面再加一层函数。repeat(3) 返回真正的装饰器,后者再接收函数。三层容易混淆;只有行为确实需要参数时才用工厂。

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"hello {name}"

print(greet("Ada"))

配置在外,函数在内

转发参数和返回值

包装函数应接受 *args 和 **kwargs,并返回原函数的结果。否则装饰器会悄悄改变函数接口。再配合 functools.wraps,调试和文档仍能看到原来的名字。

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(func.__name__, args, kwargs, "->", result)
        return result
    return wrapper

@trace
def add(a, b=0):
    return a + b

print(add(2, b=3))

保持接口灵活,保留返回值

叠加顺序是从下往上

离 def 最近的装饰器先应用。@bold 在 @upper 上方,意味着 upper 先包装函数,然后 bold 再包装结果。预测输出时从下往上读,虽然代码是从上往下写的。

def bold(func):
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def upper(func):
    def wrapper():
        return func().upper()
    return wrapper

@bold
@upper
def title():
    return "menu"

print(title())

下面的装饰器先运行

本课要点

  • 装饰器接收一个函数,并返回一个可调用对象。
  • @deco 放在 def f 上方,等价于 f = deco(f)。
  • 内部包装函数会在被装饰函数调用时运行。
  • functools.wraps 会保留原函数的元数据。
  • 工厂返回装饰器;包装函数转发参数;叠加顺序从下往上。

常见错误

  • 装饰器忘记返回包装函数。
  • 在定义包装函数时就调用原函数,而不是在包装函数内部调用。
  • 忘记 functools.wraps,导致原名和文档字符串丢失。
  • 装饰器需要参数时,忘记多写一层函数。
轮到你

写一个装饰器,在函数返回的字符串两边加上方括号。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← 生成器上下文管理器 →