装饰器的执行时机

一句话速记

装饰器在模块导入时(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 after

5)常见面试题场景

题目:下面代码输出什么?

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) 赋值,不是调用时的魔法」

状态

  • 已背速记
  • 能讲通俗版
  • 能答多装饰器执行顺序