每张订单都要过同样的三步:校验、收款、打印。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 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。