装饰器的执行时机
一句话速记
装饰器在模块导入时(import 时)立即执行,不是在被装饰函数被调用时。@decorator 等价于 func = decorator(func)——这行代码在 class body 或 module top-level 被解析时就运行了。被装饰函数的实际逻辑(wrapper 函数)在调用时才执行。这个区别决定了装饰器的副作用时机,以及为什么 FastAPI 路由注册在启动时而不是请求时完成。
通俗解释(5 分钟版)
def my_decorator(func):
print(f"[装饰器] 正在装饰 {func.__name__}") # 这行在 import 时执行
def wrapper(*args, **kwargs):
print("[wrapper] 调用前")
result = func(*args, **kwargs)
print("[wrapper] 调用后")
return result
return wrapper
@my_decorator
def hello():
print("hello!")
# 输出(import 或执行到这里时立即打印):
# [装饰器] 正在装饰 hello
hello()
# 调用时才打印:
# [wrapper] 调用前
# hello!
# [wrapper] 调用后等价写法:
# @my_decorator 等价于:
hello = my_decorator(hello) # 立即执行 my_decorator(hello)
# my_decorator 返回 wrapper,之后 hello 这个名字指向 wrapper关键细节
1)装饰器的三个层次
# 层次 1:最简单的装饰器(两层函数)
def timer(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.time()-start:.3f}s")
return result
return wrapper
# 层次 2:带参数的装饰器(三层函数)
def retry(max_times=3):
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(max_times):
try:
return func(*args, **kwargs)
except Exception as e:
if i == max_times - 1:
raise
print(f"第 {i+1} 次重试...")
return wrapper
return decorator
@retry(max_times=3) # 等价于 func = retry(3)(func)
def risky_call(): ... # retry(3) 在 import 时执行,返回 decorator
# decorator(risky_call) 也在 import 时执行
# 层次 3:类装饰器(__call__)
class memoize:
def __init__(self, func):
self.func = func
self.cache = {} # import 时创建(每个被装饰函数一个)
def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)
return self.cache[args]
@memoize
def fib(n): ... # memoize(fib) 在 import 时执行,创建 memoize 实例2)FastAPI 路由注册的原理
app = FastAPI()
@app.get("/users/{id}")
async def get_user(id: int):
...
# 等价于:
# async def get_user(id: int): ...
# get_user = app.get("/users/{id}")(get_user)
# ↑ import 时执行,把路由注册到 app 的路由表里所以:FastAPI 的所有路由在应用启动时(import 时)就注册好了,不是在第一次请求时。这也是为什么 uvicorn main:app 后不需要”热加载路由”的原因。
3)@functools.wraps 的必要性
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str):
"""返回问候语"""
return f"Hello, {name}"
# 不加 @wraps 的问题:
print(greet.__name__) # 输出 "wrapper"(不是 "greet")
print(greet.__doc__) # 输出 None(文档字符串丢失)
help(greet) # 显示 wrapper 的签名,不是 greet 的
# 加 @functools.wraps 后:
import functools
def my_decorator(func):
@functools.wraps(func) # 把 func 的元信息复制到 wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
print(greet.__name__) # "greet" ✓
print(greet.__doc__) # "返回问候语" ✓实际影响:Sphinx 文档生成、FastAPI OpenAPI schema、日志中的函数名——都依赖 __name__/__doc__,忘加 @wraps 会导致文档和日志混乱。
4)装饰器的执行顺序(多个装饰器)
@decorator_a
@decorator_b
@decorator_c
def func(): ...
# 等价于:
func = decorator_a(decorator_b(decorator_c(func)))
# 执行顺序:
# 1. decorator_c(func) → 从内到外应用装饰器
# 2. decorator_b(...)
# 3. decorator_a(...)
#
# 调用时执行顺序:
# decorator_a 的 wrapper 最先运行(最外层)
# decorator_c 的 wrapper 最后运行(最内层)# 验证:
def a(f):
print("a 装饰中")
def wrapper(*args):
print("a before")
f(*args)
print("a after")
return wrapper
def b(f):
print("b 装饰中")
def wrapper(*args):
print("b before")
f(*args)
print("b after")
return wrapper
@a # 输出(import 时):
@b # b 装饰中
def func(): # a 装饰中
print("func")
func()
# a before
# b before
# func
# b after
# a after5)常见面试题场景
题目:下面代码输出什么?
registry = []
def register(func):
registry.append(func)
return func
@register
def foo(): pass
@register
def bar(): pass
print(len(registry)) # ?答案:2。@register 在 import 时执行,foo 和 bar 都被 append 到 registry,所以 len=2。这是 Flask 路由注册的原理。
6)类方法上的装饰器
class MyClass:
@staticmethod # 装饰器,把函数变成静态方法(无 self)
def static_method(): ...
@classmethod # 装饰器,把函数变成类方法(cls 代替 self)
def class_method(cls): ...
@property # 装饰器,把方法变成属性访问
def name(self): ...
# 这些都在 class body 解析时(import 时)执行
# property 还支持 .setter / .deleter 装饰器链延伸追问
- Q:装饰器可以给被装饰的函数添加属性吗?
→ 可以:在
decorator(func)里直接func.attr = value,然后 return func(不一定要 wrapper)。很多框架用这种方式给函数打标签(如@app.route把路径信息挂在函数上)。 - Q:如何写一个既能
@decorator又能@decorator()调用的装饰器? → 检查第一个参数是不是函数:def flexible(func=None, *, option=False): def decorator(f): @functools.wraps(f) def wrapper(*args, **kwargs): return f(*args, **kwargs) return wrapper if func is not None: # @flexible(无括号) return decorator(func) return decorator # @flexible()(有括号) - Q:LRU cache 是装饰器吗?它的缓存什么时候清空?
→
@functools.lru_cache(maxsize=128)是带参数的装饰器(三层函数)。缓存绑定在函数对象上(func.cache_info()),在程序运行期间一直存在,除非手动func.cache_clear()或函数对象被垃圾回收。
我的记法
- 装饰器在 import 时执行(
@deco=func = deco(func),立即运行) - wrapper 里的逻辑在调用时才跑
- 三层 = 带参数装饰器(
@retry(3)=retry(3)(func)) - 必加
@functools.wraps,否则__name__/__doc__丢失 - 多个装饰器:从内到外应用,从外到内执行
- FastAPI/Flask 路由用装饰器在 import 时注册路由表
- 一句话:「装饰器是 import 时的 func = deco(func) 赋值,不是调用时的魔法」
状态
- 已背速记
- 能讲通俗版
- 能答多装饰器执行顺序