装饰器(decorator)是 Python 里”看起来很魔法,其实很简单”的语法。你在 Flask、Django、FastAPI、pytest 的代码里到处都能看到 @ 符号,那就是装饰器。前面 作用域与闭包 和 匿名函数 lambda 与高阶函数 讲的东西合体,就是装饰器。这一篇讲清楚装饰器的原理、语法糖 @、functools.wraps 的作用,进阶部分(带参数、类装饰器、lru_cache)留到 装饰器进阶。
一、装饰器是什么
一句话定义:装饰器是一个”接受函数、返回函数”的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| def uppercase(func): def wrapper(): result = func() return result.upper() return wrapper
def greet(): return "hello"
greet = uppercase(greet) print(greet())
|
语义:”用 uppercase 包装一下 greet,让它返回值自动大写。”
二、@ 语法糖
@decorator 就是”把下面这个函数传给 decorator,再把返回值绑回同名”:
1 2 3 4 5 6 7 8
| @uppercase def greet(): return "hello"
def greet(): return "hello" greet = uppercase(greet)
|
@ 只是让代码更短、意图更明显,本质没差。
三、带参数的被装饰函数
真实场景函数都有参数,wrapper 用 *args、**kwargs 通吃:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def uppercase(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() if isinstance(result, str) else result return wrapper
@uppercase def greet(name): return f"hello {name}"
print(greet("咖飞"))
|
被装饰后,函数的名字和 docstring 会被替换:
1 2 3 4 5 6 7
| @uppercase def greet(name): """给某人打招呼""" return f"hello {name}"
print(greet.__name__) print(greet.__doc__)
|
functools.wraps 修复这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import functools
def uppercase(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() if isinstance(result, str) else result return wrapper
@uppercase def greet(name): """给某人打招呼""" return f"hello {name}"
print(greet.__name__) print(greet.__doc__)
|
规则:只要写装饰器,就无脑加 @functools.wraps(func)。
五、经典装饰器:计时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import time import functools
def timed(func): """打印函数耗时。""" @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} 用了 {elapsed:.4f}s") return result return wrapper
@timed def compute(n): return sum(i * i for i in range(n))
compute(1_000_000)
|
六、经典装饰器:日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import functools import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") log = logging.getLogger(__name__)
def logged(func): @functools.wraps(func) def wrapper(*args, **kwargs): log.info(f"调用 {func.__name__}(args={args}, kwargs={kwargs})") try: result = func(*args, **kwargs) except Exception as e: log.exception(f"{func.__name__} 抛出 {e}") raise log.info(f"{func.__name__} 返回 {result!r}") return result return wrapper
@logged def divide(a, b): return a / b
divide(10, 2) divide(10, 0)
|
七、经典装饰器:重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import functools import time
def retry(times=3, delay=1): """遇到异常重试。""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): last_exc = None for attempt in range(1, times + 1): try: return func(*args, **kwargs) except Exception as e: last_exc = e print(f"[{attempt}] {func.__name__} 失败:{e}") if attempt < times: time.sleep(delay) raise last_exc return wrapper return decorator
@retry(times=3, delay=1) def flaky_api(): import random if random.random() < 0.7: raise RuntimeError("network error") return "ok"
print(flaky_api())
|
这已经是”带参数的装饰器”——外层 retry(times, delay) 返回真正的装饰器。三层嵌套是这个模式的标配。带参数装饰器我们在 装饰器进阶 深入讲。
八、内置装饰器
Python 有几个常用装饰器:
@staticmethod / @classmethod / @property
在类里用,我们在 类方法、静态方法与 property 讲。
自动缓存函数结果:
1 2 3 4 5 6 7 8 9 10 11
| from functools import lru_cache, cache
@lru_cache(maxsize=128) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2)
@cache def fib(n): ...
|
注意:lru_cache 要求参数可哈希(不能是 list/dict)。
给类补齐比较方法:定义 __eq__ 和一个顺序方法(如 __lt__),自动生成剩下 3 个。见 特殊方法。
@dataclass
自动生成 __init__、__repr__ 等,见 类与对象基础。
九、多层装饰器
装饰器可以叠加,从下往上执行:
调用 f() 时,实际是 a 的 wrapper 最外层调用。想象成”洋葱”——最外层先接触,逐层剥入到 f。
十、装饰类的方法
装饰器可以用在类的方法上,但要注意 self:
1 2 3 4 5 6 7 8 9 10 11 12 13
| def logged(func): @functools.wraps(func) def wrapper(self, *args, **kwargs): print(f"[LOG] {self.__class__.__name__}.{func.__name__}") return func(self, *args, **kwargs) return wrapper
class Service: @logged def run(self, task): print(f"运行 {task}")
Service().run("build")
|
写 def wrapper(*args, **kwargs) 也行,self 就是 args[0]。
十一、装饰器的调试小技巧
被装饰后调试有时候会迷惑:
1 2 3 4 5
| @timed def foo(): ...
foo.__wrapped__ foo.__name__
|
如果你想临时”取消装饰”,可以 foo.__wrapped__() 调用原函数。
十二、装饰器的常见错误
错误 1:忘了写 wrapper
1 2
| def bad(func): return func()
|
装饰器必须返回一个函数,不是”函数的返回值”。
错误 2:wrapper 没接收参数
1 2 3 4 5 6 7 8 9
| def bad(func): def wrapper(): return func() return wrapper
@bad def add(a, b): return a + b
add(1, 2)
|
错误 3:忘了 return
1 2 3 4 5
| def bad(func): @functools.wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) return wrapper
|
被装饰的函数返回值丢了。
被装饰后 func.__name__、func.__doc__ 都乱了,pytest / Sphinx / 日志都会受影响。
十三、一个完整例子:权限校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| """装饰器实现的简单权限校验。"""
import functools
class Unauthorized(Exception): pass
current_user = {"name": "guest", "roles": {"user"}}
def require_role(role: str): """要求当前用户拥有指定 role。""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if role not in current_user["roles"]: raise Unauthorized( f"{current_user['name']} 缺少 {role} 权限") return func(*args, **kwargs) return wrapper return decorator
@require_role("admin") def delete_user(id: int): print(f"删除用户 {id}")
@require_role("user") def read_profile(): print("查看资料")
read_profile() try: delete_user(42) except Unauthorized as e: print(e)
current_user["roles"].add("admin") delete_user(42)
|
Flask 的 @login_required、Django 的 @permission_required 都是这个思路的产业级实现。
十四、装饰器 vs 上下文管理器 vs 中间件
三者常做类似的事情(”包一层”):
- 装饰器:包”函数”;
- 上下文管理器(with):包”代码块”;
- 中间件:包”请求-响应流”。
选择规则:
- 想给一个函数加”计时/日志/权限/重试” → 装饰器;
- 想给一段代码加”资源打开/关闭” → 上下文管理器;
- 想在框架级给每个请求加逻辑 → 中间件。
十五、小结与延伸阅读
- 装饰器就是”接受函数、返回函数”的函数;
@decorator 只是 func = decorator(func) 的语法糖;
- wrapper 用
*args, **kwargs 通吃参数;
- 一定要
@functools.wraps(func) 保留元信息;
- 常见装饰器:timed、logged、retry、cached、permission;
- 装饰器可以叠加,从下往上;
- 带参数装饰器需要三层嵌套。
延伸阅读:
下一篇 模块与包管理 我们要跳出单文件,学会把代码组织成模块和包。