先把结论放在前面:装饰器其实就是一个"能接收函数、还能返回函数"的闭包。它像给函数套上一层"滤镜",不改原函数一行代码,却能悄悄加功能。你把这句话吃透了,装饰器的雾就散了一半。
这篇文章我不背书、不掉书袋,按"能用、好用、少踩坑"的顺序来。你会看到清晰的心智模型、贴合实战的范式、以及一堆能直接拷走的代码模板。认真读完,你能做到两件事: 1)看懂别人写的装饰器 ;2)按需自己写一个。

一、装饰器到底在解决什么问题?
写代码最怕两件事:重复 和侵入。
- 重复:同样的日志/鉴权/计时逻辑,N 个函数都要写一遍;
- 侵入:为加这点逻辑,你得把每个函数的源码都打开改一遍,风险大、回滚难。
装饰器的使命就是:把"通用增强"与"业务逻辑"分离。 它像给函数穿外套:今天穿"日志外套",明天换"缓存外套",业务本体不动分毫。这就是"非侵入式扩展"的威力。
二、两把钥匙:作用域 & 闭包(超短复习版)
1)作用域:变量各有地盘
函数里的变量默认只在函数里生效(局部),文件级别的是全局。局部对全局有"隔离墙"。你可以用 global
硬撞墙,但易出事故,不推荐。
2)闭包:把"局部变量"搬出函数的正规方法
闭包是函数里再定义函数 ,内层函数"记住"了外层函数的变量。外层把内层函数返回出去,你在外面调用内层函数,就能"间接操作"那块原本局部的变量。这就是装饰器的底层基建。
心里装个模型:装饰器 = 以函数为参数的闭包 + @ 语法糖。
三、从 0 到 1:把闭包"拧成"装饰器
先分享一个完全可运行的"三步走"心法:
Step1:写一个"外层函数"接收被增强的函数 Step2:在外层里写一个 wrapper
,负责加料(日志/校验/计时...) Step3:返回 wrapper
,并用 @
套在目标函数上
语法糖记忆法:
@decorator
等价于func = decorator(func)
。
四、5 个高频实战场景(可直接落地)
1)日志记录:谁在什么时候、带着什么参数,调用了哪个函数?
python
import functools
import time
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ts = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] CALL {func.__name__} args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[{ts}] RET {func.__name__} -> {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 5)
要点:
functools.wraps
必须用,保留原函数的名字、文档、注解,不然调试、堆栈、工具链都不友好(后面详讲)。
2)性能测试:测函数执行时间(原文代码,原样保留)
python
import time
import functools
def timing_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time() # 开始时间
result = func(*args, **kwargs)
end = time.time() # 结束时间
print(f"{func.__name__}执行时间:{end - start:.4f}秒")
return result
return wrapper
# 测试一个耗时函数
@timing_decorator
def slow_func():
time.sleep(2) # 模拟耗时操作
slow_func() # 输出:slow_func执行时间:2.0005秒
3)权限验证:先验权再执行函数(原文代码,原样保留)
python
import functools
# 带参数的装饰器:指定需要的权限
def permission_required(permission):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 假设user_has_permission是实际的权限检查函数
if not user_has_permission(permission):
raise Exception("没有权限执行此操作!")
return func(*args, **kwargs)
return wrapper
return decorator
# 假设的权限检查逻辑
def user_has_permission(permission):
return permission == "admin" # 模拟当前用户是管理员
# 只有管理员能删除用户
@permission_required("admin")
def delete_user(user_id):
print(f"用户{user_id}已删除")
delete_user(123) # 正常执行;如果权限不是admin,会抛异常
4)参数校验:入口就把坏参数挡在门外(原文代码,原样保留)
python
import functools
def positive_check(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 校验位置参数
for arg in args:
if arg <= 0:
raise ValueError(f"参数{arg}必须是正数")
# 校验关键字参数
for val in kwargs.values():
if val <= 0:
raise ValueError(f"参数{val}必须是正数")
return func(*args, **kwargs)
return wrapper
@positive_check
def multiply(a, b):
return a * b
multiply(2, 3) # 正常返回6
multiply(-1, 3) # 抛异常:参数-1必须是正数
5)结果缓存:重复调用不重复计算(原文代码,原样保留)
python
import functools
def cache_decorator(func):
cache = {} # 缓存字典,存参数和结果的对应关系
@functools.wraps(func)
def wrapper(*args):
if args in cache:
print(f"使用缓存:{args}")
return cache[args]
result = func(*args)
cache[args] = result # 存入缓存
print(f"缓存新增:{args}")
return result
return wrapper
# 模拟复杂计算(比如斐波那契数列)
@cache_decorator
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
fib(5) # 第一次计算,缓存新增
fib(5) # 第二次调用,直接用缓存
生产里可直接用官方的
functools.lru_cache(maxsize=...)
,一行搞定,还更稳。
五、带参数的装饰器:再加一层"工厂"
你可能注意到"权限验证"示例,外面还包了一层 permission_required(permission)
。这就是装饰器工厂:先接配置,再返回真正装饰器。
通俗点:第一层接参数,第二层接函数,第三层是 wrapper 执行增强 。 记忆口诀:三层套娃。
六、多个装饰器叠加:执行顺序怎么记?
python
def A(func):
def w(*a, **k):
print("A-前")
r = func(*a, **k)
print("A-后")
return r
return w
def B(func):
def w(*a, **k):
print("B-前")
r = func(*a, **k)
print("B-后")
return r
return w
@A
@B
def work():
print("干活")
work()
记忆法 :离函数最近的装饰器(这里是 B
)先包住函数,最外层(A
)最后包。调用时"外到内",日志打印是:A-前 → B-前 → 干活 → B-后 → A-后。
另一个角度:装饰器在定义阶段 自上而下应用,但执行阶段由外到内。
七、必须会的 8 个技巧(真·避坑指南)
1)永远用 @functools.wraps(func)
不然 __name__
、__doc__
、__annotations__
、__wrapped__
都丢失,调试困难,配合工具(如单测、依赖注入)会出妖。
2)参数兜底:*args, **kwargs
别偷懒 装饰器要像"透明胶带",不改变被装饰函数的签名行为。除非你明确要改。
3)闭包变量的"晚绑定"陷阱 在循环里生成多个装饰器或 wrapper 时,注意把循环变量"固化"到默认参数里,否则全都引用最后一个值。
python
funcs = []
for i in range(3):
def deco(func, i=i): # 用默认参数绑定当前 i
def w(*a, **k):
print(i)
return func(*a, **k)
return w
@deco
def f(): pass
funcs.append(f)
# 正确打印 0、1、2
4)装饰 async def
时要用异步 wrapper 否则你会得到一个"没被 await 的协程"。示例:
python
import functools
import asyncio
def async_timing(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs): # 注意 async
start = asyncio.get_event_loop().time()
result = await func(*args, **kwargs)
end = asyncio.get_event_loop().time()
print(f"{func.__name__}耗时:{end - start:.4f}s")
return result
return wrapper
5)类方法的 self/cls 别弄丢 给实例方法加装饰器时,签名里要接住 self
;给类方法(@classmethod
)加装饰器要接住 cls
。
6)缓存要考虑"可哈希性"和"过期策略" 用自制 dict
缓存时,只能缓存可哈希的参数(如 tuple
、int
)。生产里优先 lru_cache
,或引入 TTL/清理策略。
7)异常要么上抛、要么统一处理 别在装饰器里偷偷吞异常。吞了等于"把锅焊死",查不出问题。
8)别把装饰器写成黑魔法 目标是读得懂、测得了、改得起。能用普通函数抽取就别强行上装饰器;复杂逻辑建议用类装饰器 或中间件模式。
八、类装饰器:当你需要"带状态"的增强
当装饰器需要维护状态(比如统计调用次数、限流),用类更直观:
python
import functools
class CountCalls:
def __init__(self, func):
functools.wraps(func)(self) # 把元数据贴到实例上
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 第 {self.count} 次调用")
return self.func(*args, **kwargs)
@CountCalls
def greet(name):
return f"Hi, {name}"
greet("Alice")
greet("Bob")
优点:语义清晰,状态天然落在实例属性里,测试也更容易。
九、标准库里"现成能用"的装饰器
functools.lru_cache(maxsize=128)
:函数级缓存,线程安全、性能好;functools.cached_property
:把耗时计算变成一次性属性(类上用);functools.singledispatch
:函数"泛型",按参数类型分发;dataclasses.dataclass
:类转数据类,自动生成__init__
、__repr__
等;@property / @staticmethod / @classmethod
:面向对象三件套。
结论:能用现成的别自己造轮子,稳定、可读、踩过坑。
十、类型提示进阶(可选):优雅保留签名
想让装饰器对类型检查更友好,可以用 typing.ParamSpec
与 TypeVar
:
python
from typing import TypeVar, ParamSpec, Callable
import functools
P = ParamSpec("P")
R = TypeVar("R")
def logged(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
这段的收益是:被装饰后的函数,在 IDE/静态检查里仍保留原始签名与返回值类型,开发体验更丝滑。
十一、生产可复用模板(复制即用)
通用装饰器模板(同步)
python
import functools
def decorator_name(*dargs, **dkwargs):
"""装饰器工厂,可带配置;不带配置时也兼容"""
def _decorator(func):
@functools.wraps(func)
def _wrapper(*args, **kwargs):
# 前置逻辑(可用 dargs/dkwargs 配置)
result = func(*args, **kwargs)
# 后置逻辑
return result
return _wrapper
# 兼容无参写法:@decorator_name
if callable(dargs[0]) if dargs else False:
func = dargs[0]
return _decorator(func)
return _decorator
通用装饰器模板(异步)
python
import functools
import asyncio
def async_decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
# 前置
res = await func(*args, **kwargs)
# 后置
return res
return wrapper
十二、装饰器 vs 继承 vs 组合:怎么选?
- 装饰器:不改业务函数源码,只想加"切面"能力(日志、鉴权、计时、缓存、限流、重试)→ 选它;
- 继承/组合:对象级别的大改造、复杂状态机、跨多方法的协作 → 选类。
- 中间件:有明确的"请求-响应"链路(Web/爬虫/数据流),统一处理更自然。
一句话:函数增强用装饰器,复杂行为用类或中间件。
十三、10 个常见坑(配对照答案)
- 忘记
return wrapper
→ 装饰后函数变成None
。 - 忘记
*args, **kwargs
→ 一调用非空参数就炸。 - 不加
@wraps
→ 名字/文档丢失,inspect
/IDE 体验差。 - 在循环里闭包捕获同一个变量 → 用默认参数固化。
- 装饰器里偷偷吞异常 → 线上排障地狱。
- 同时装同步/异步函数 → 分开写两个版本,或检测
iscoroutinefunction
。 - 缓存没考虑"不可哈希参数" → 用
lru_cache
或自定义 key。 - 叠加装饰器没想清顺序 → 把"最外层切面"写在最上面。
- 在装饰器里修改全局可变对象 → 引发难以定位的副作用。
- 在类方法上装饰器忘记接
self/cls
→ 位置参数错位。
十四、给新手的实话
- 不用急着精通:先会看懂,会使用几个模板;真正的理解来自"用过"。
- 日常多复用:标准库的装饰器已经非常强大,优先用。
- 复杂度上升就"上岸" :当装饰器逻辑越来越厚重,果断切到类或中间件,别强撑。
十五、速查清单(收藏版)
- 记忆模型:装饰器 = 闭包 + 接受函数 + 返回函数 +
@
语法糖 - 三层套娃:配置层 → 装饰器层 → 包装层(wrapper)
- 必备工具:
functools.wraps
、lru_cache
、cached_property
、singledispatch
- 顺序规则:定义时自上而下应用,执行时由外到内
- 同步/异步:不要混;分别写,或自动分流
- 最佳实践:无侵入、可读性优先、异常透明、状态最小化
结尾彩蛋:把"闭包→装饰器"的桥,搭得更直白
还记得我们前面说"装饰器其实就是闭包吗"?看这两句你就彻底通了:
- 闭包版调用:
enhanced = outer(add); enhanced(1, 2)
- 语法糖版:在
add
上方写@outer
,然后直接add(1, 2)
。
差别只有"写法",本质完全一致。