先说个真实故事。
去年我接了一个接口日志项目,需要记录每个API的调用时间、请求参数和返回状态。团队里有个"老司机"说用装饰器简单,写个@log就搞定了。我照着文档抄了一遍,运行——直接报错:TypeError: wrapper() missing 1 required positional argument。
我对着屏幕发了半小时呆。文档明明就是这样写的啊?哪里出问题了?
后来排查半天才发现,问题出在我抄的博客太老了,用的是Python 2的写法。到了Python 3,连个像样的报错提示都不给,就扔给你一句"缺少参数"。那个项目差点因此延期。
那次经历让我下定决心把装饰器彻底搞清楚——不是半懂不懂地照抄,而是从原理到实战全部拿下。
今天把我踩过的坑、总结的经验、提炼的套路一次性讲清楚。不堆概念,不念教材,直接上你在代码里会遇到的问题。
装饰器到底是什么?
网上说"装饰器是一种闭包",对,但看完你还是不会写。
换个理解方式。装饰器就是一个函数的"包装膜"。
你去水果店买水果,水果本身没变,但套上包装盒之后:可以保鲜,可以印logo,可以加个防伪标签。装饰器干的就是这个事——在不改原函数代码的前提下,给函数加点额外的功能。
最常见的场景:计时、登录校验、日志记录、性能监控。这些需求如果每个函数都手写一遍,代码会变得又臭又长。装饰器让你只写一次,到处贴标签。
import timedef timer(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@timerdef slow_function(): time.sleep(1) return "Done"slow_function()
slow_function 本身没有任何改动,贴上@timer之后,自动拥有了计时能力。这就是装饰器的核心价值——增强函数,但不污染函数本身。
它为什么能工作?
很多人会用装饰器,但不知道为什么能这样用。看懂下面这段,你就能自己创造装饰器了:
def slow_function(): return "Done"def timer(func): def wrapper(): start = time.time() result = func() end = time.time() print(f"耗时 {end - start:.4f} 秒") return result return wrapperslow_function = timer(slow_function) # 这一行就是 @timer 的本质
装饰器的语法糖@timer就是上面那行手动包装的简写。
所以装饰器的本质就是:一个高阶函数,输入是函数,输出还是函数。
踩坑实录:那些让新手崩溃的瞬间
坑一:*args, **kwargs 写错一个符号
def my_decorator(func): def wrapper(*args, **kwargs): # 星号必须带 print("开始执行") return func(*args, **kwargs) # 这里也是 return wrapper
把*args写成args,或者把**kwargs写成kwargs,Python会报一个很隐晦的错误。是解包操作,func(*args)是把元组里的每个元素拆开传给函数。写func(args)是把整个元组当一个参数传进去,函数签名就对不上了。
自检方法:把装饰器在复杂签名的函数上先测一遍:
@my_decoratordef complex_func(a, b=10, *args, **kwargs): print(f"a={a}, b={b}, args={args}, kwargs={kwargs}")complex_func(1, 2, 3, 4, name="test")
坑二:装饰器带参数,写成了套娃
def log(level="INFO"): def decorator(func): def wrapper(*args, **kwargs): print(f"[{level}] 调用 {func.__name__}") return func(*args, **kwargs) return wrapper return decorator@log("ERROR")def run(): pass
记忆法:装饰器带参数,就是"工厂的工厂"。
拆解三层各自负责什么:
- 第一层
log("ERROR")负责接收参数,返回一个装饰器 - 第二层
decorator负责接收被装饰的函数,返回包装函数
@log("ERROR")等价于run = log("ERROR")(run)。
坑三:装饰器改变了原函数的"身份"
@timerdef add(a, b): """两数相加,返回结果""" return a + bprint(add.__name__) # 输出什么?wrapper ← 函数名丢了print(add.__doc__) # 输出什么?None ← 文档丢了
很多框架会根据函数名和文档字符串来生成路由或校验规则。元数据丢了,轻则路由名变成wrapper,重则整个应用行为异常。
解法只有一行:
import functoolsdef timer(func): @functools.wraps(func) # 把原函数的元数据复制过来 def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"耗时 {end - start:.4f} 秒") return result return wrapper
加上@functools.wraps(func)这一行,原函数的__name__、__doc__、__annotations__全部保留。这行代码没有理由不写,养成习惯。
坑四:装饰器返回了None
def bad_decorator(func): def wrapper(*args, **kwargs): print("做一些操作") func(*args, **kwargs) # 没有return! return wrapper@bad_decoratordef get_data(): return [1, 2, 3]result = get_data()print(result) # 输出:None ← 返回值丢了
记住:装饰器的wrapper永远要return原函数的执行结果。
实战:写一个带缓存的装饰器
import functools, timedef memoize(ttl=300): """ 带过期时间的内存缓存装饰器 ttl: 缓存有效期(秒),默认5分钟 """ cache = {} def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): key = str(args) + str(sorted(kwargs.items())) now = time.time() if key in cache: val, t, expire = cache[key] if now - t < expire: print(f"命中缓存 (剩余 {expire - (now - t):.0f}s)") return val result = func(*args, **kwargs) cache[key] = (result, now, ttl) print("缓存未命中,执行函数") return result return wrapper return decorator@memoize(ttl=10)def fetch_api(user_id, include_history=True): time.sleep(2) # 假设每次请求耗时2秒 return {"user_id": user_id, "data": [1, 2, 3]}data1 = fetch_api(1001) # 耗时2秒data2 = fetch_api(1001) # 0秒返回,命中缓存data3 = fetch_api(1002) # 耗时2秒,不同参数
同样参数10秒内重复调用,2秒请求变成0秒。配合Redis,可以做成分布式缓存。
类装饰器:不是函数也可以被装饰
单例模式装饰器
def singleton(cls): instances = {} @functools.wraps(cls) def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance@singletonclass DatabaseConnection: def __init__(self): print("创建数据库连接...") self.connected = Truedb1 = DatabaseConnection()db2 = DatabaseConnection()db3 = DatabaseConnection()print(db1 is db2 is db3) # True,只初始化一次
调用三次DatabaseConnection(),但__init__只执行了一次。适合场景:数据库连接池、配置管理器、线程池、日志实例。
路由注册装饰器
class Router: routes = {} @classmethod def route(cls, path): def decorator(func): cls.routes[path] = func print(f"注册路由: {path} -> {func.__name__}") return func return decoratorclass MyApp(Router): @Router.route("/home") def home(self): return "Welcome home" @Router.route("/about") def about(self): return "About us"print(MyApp.routes)
多个装饰器的叠加顺序
@log_decorator@timer_decoratordef process(): time.sleep(0.1) return "Done"
从下往上依次执行。 可以想象从里到外穿衣服:先穿内衣@timer_decorator,再套外套@log_decorator。
反过来看调用:process()先经过外套log_decorator,再进入内衣timer_decorator,最后到达process本体。这就是为什么内层装饰器加@functools.wraps那么重要——它确保log_decorator看到的是真实的函数名。
真实项目的两个实用场景
场景一:重试机制
import functools, time, logginglogger = logging.getLogger(__name__)def retry(max_attempts=3, delay=1): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts - 1: raise logger.warning(f"第{attempt+1}次失败,{delay}s后重试: {e}") time.sleep(delay) return wrapper return decorator@retry(max_attempts=3, delay=2)def call_api(url): import random if random.random() < 0.7: raise ConnectionError("网络超时") return "成功"
场景二:权限校验
def requires_auth(permission): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): user = get_current_user() if permission not in user.permissions: raise PermissionError(f"需要权限: {permission}") return func(*args, **kwargs) return wrapper return decorator@requires_auth("admin")def delete_user(user_id): pass@requires_auth("finance")def export_report(): pass
权限逻辑和业务逻辑完全分离,改一处不影响另一处。
行动框架
第一步,理解闭包的原理。 装饰器背后就是闭包——函数可以记住定义时的变量。理解了闭包,装饰器就是顺水推舟。
第二步,动手写。 从@timer开始,重点跑通@functools.wraps这一步。然后试带参数的装饰器,再试类装饰器。
第三步,读优质源码。 Flask的@app.route、Django的@login_required、FastAPI的路径装饰器——都是非常高质量的参考。
第四步,警惕过度封装。 同一个逻辑需要在多处使用时,才值得抽取成装饰器。只在某个函数里加一行日志,手动写就行。
别再把装饰器当成高深莫测的高级语法来回避了。它的本质就是一个函数包裹函数的技巧,搞清楚三件事就够了:谁包裹谁、参数怎么传递、元数据怎么保留。
下次再遇到@开头的代码,别跳过。花五分钟读一下,看看它给函数包了什么功能。你会发现,看懂别人的装饰器,比你想象的要简单得多。