Python 装饰器深度指南:从语法糖到元编程利器

2827 字
14 分钟
Python 装饰器深度指南:从语法糖到元编程利器

你可能已经用过 @login_required@app.route('/')@property,但你真的理解装饰器在做什么吗?

大多数 Python 教程把装饰器讲成”语法糖”然后一笔带过。但装饰器是 Python 最强大的元编程工具之一——理解它,你就打开了框架设计、AOP(面向切面编程)、插件系统的大门。

这篇文章不是另一个”装饰器入门”。我会从底层原理讲起,一路推到生产级用法,每一步都有完整可运行的代码。

一、装饰器的本质:函数是一等公民#

在理解装饰器之前,你得先理解一件事:在 Python 中,函数是对象

def greet(name):
return f"Hello, {name}!"
# 函数可以赋值给变量
say_hello = greet
print(say_hello("World")) # Hello, World!
# 函数可以作为参数传递
def call_twice(func, arg):
return func(arg) + " " + func(arg)
print(call_twice(greet, "Python")) # Hello, Python! Hello, Python!
# 函数可以作为返回值
def make_greeter(greeting):
def greeter(name):
return f"{greeting}, {name}!"
return greeter
hi = make_greeter("Hi")
print(hi("装饰器")) # Hi, 装饰器!

这三个特性——赋值、传参、返回——构成了装饰器的基础。装饰器本质上就是一个接受函数作为参数、返回新函数的高阶函数。

二、最简装饰器:揭开 @ 语法糖的面纱#

def timer(func):
import time
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
@timer
def slow_function():
import time
time.sleep(0.5)
return "done"
# 等价于:slow_function = timer(slow_function)
result = slow_function()
# ⏱️ slow_function 执行耗时: 0.5012s

@timer 这一行做了什么?Python 解释器在定义 slow_function 后,自动执行了 slow_function = timer(slow_function)。就这么简单。

为什么用 *args, **kwargs#

因为你不知道被装饰的函数接受什么参数。用 *args, **kwargs 做透传,装饰器就能适配任意函数签名:

@timer
def add(a, b):
return a + b
@timer
def fetch_data(url, timeout=30, headers=None):
# 模拟网络请求
return {"status": 200}
add(1, 2) # 正常工作
fetch_data("/api") # 也正常工作

三、functools.wraps:别让装饰器吞掉函数身份#

装饰器有个经典坑——被装饰后,函数的 __name____doc__ 等元信息会丢失:

def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def hello():
"""Say hello."""
return "hello"
print(hello.__name__) # wrapper ← 不是 hello!
print(hello.__doc__) # None ← 文档字符串也丢了!

这在调试、日志、API 文档生成时会制造大麻烦。解决方案很简单:

from functools import wraps
def my_decorator(func):
@wraps(func) # 一行搞定
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def hello():
"""Say hello."""
return "hello"
print(hello.__name__) # hello ✅
print(hello.__doc__) # Say hello. ✅

规则:写装饰器时,永远加 @wraps(func) 没有例外。这是 Python 社区的硬性共识。

四、带参数的装饰器:三层嵌套的秘密#

当你需要给装饰器本身传参数时,事情变得有趣了:

from functools import wraps
def retry(max_attempts=3, delay=1):
"""失败重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"⚠️ {func.__name__}{attempt}次失败: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5)
def unstable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("网络抖动")
return {"data": "success"}
# 调用时自动重试,最多5次
result = unstable_api_call()

三层嵌套看起来有点吓人,但逻辑很清晰:

层级函数名接收什么返回什么
第1层retry()装饰器参数真正的装饰器
第2层decorator()被装饰的函数wrapper
第3层wrapper()原函数的参数原函数的返回值

记住这个模型:带参数的装饰器 = 装饰器工厂。 retry(max_attempts=5) 先执行,返回一个装饰器,然后那个装饰器再去装饰函数。

五、类装饰器:当函数不够用时#

有时候装饰器需要维护状态(比如调用计数、缓存),用类实现更优雅:

from functools import wraps, update_wrapper
class CallCounter:
"""记录函数被调用次数的装饰器"""
def __init__(self, func):
self.func = func
self.count = 0
update_wrapper(self, func) # 保持元信息
def __call__(self, *args, **kwargs):
self.count += 1
print(f"📊 {self.func.__name__} 已被调用 {self.count} 次")
return self.func(*args, **kwargs)
def reset(self):
"""重置计数器"""
self.count = 0
@CallCounter
def process_data(data):
return [x * 2 for x in data]
process_data([1, 2, 3]) # 📊 process_data 已被调用 1 次
process_data([4, 5, 6]) # 📊 process_data 已被调用 2 次
print(f"总调用次数: {process_data.count}") # 总调用次数: 2
process_data.reset() # 可以重置!

类装饰器的优势:

  • 状态管理天然:实例属性就是状态
  • 方法扩展:可以给被装饰函数添加额外方法(如 reset()
  • 更好的可读性:复杂逻辑用类组织比三层嵌套清晰

六、装饰器堆叠:执行顺序的陷阱#

多个装饰器可以叠加使用,但执行顺序是个经典面试题:

from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("World"))
# <b><i>Hello, World</i></b>

装饰顺序是自下而上,执行顺序是自外而内。 上面的代码等价于:

greet = bold(italic(greet))

所以 italic 先包装 greet,然后 bold 再包装结果。调用时,bold 的 wrapper 先执行,它内部调用 italic 的 wrapper,最后才调用原始 greet

💡 实用口诀:装饰器堆叠就像穿衣服——先穿内衣(最靠近函数的),再穿外套(最外层的)。脱衣服(执行)时反过来。

七、实战:生产级装饰器模式#

7.1 缓存装饰器(带过期时间)#

from functools import wraps
import time
def cache(ttl=60):
"""带 TTL 的简易缓存装饰器"""
def decorator(func):
_cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# 构造缓存键(注意:kwargs 需要排序以保证一致性)
key = (args, tuple(sorted(kwargs.items())))
if key in _cache:
result, timestamp = _cache[key]
if time.time() - timestamp < ttl:
print(f"🎯 缓存命中: {func.__name__}")
return result
else:
del _cache[key] # 过期清除
result = func(*args, **kwargs)
_cache[key] = (result, time.time())
return result
# 暴露清除缓存的方法
wrapper.clear_cache = lambda: _cache.clear()
return wrapper
return decorator
@cache(ttl=10)
def get_user_profile(user_id):
"""模拟数据库查询"""
print(f"🔍 查询数据库: user_id={user_id}")
time.sleep(0.1) # 模拟延迟
return {"id": user_id, "name": f"User_{user_id}"}
# 第一次调用:查数据库
profile = get_user_profile(42)
# 🔍 查询数据库: user_id=42
# 第二次调用:走缓存
profile = get_user_profile(42)
# 🎯 缓存命中: get_user_profile

当然,Python 3.9+ 内置了 @functools.cache@functools.lru_cache,简单场景直接用标准库。但理解原理能帮你在需要自定义策略(如 TTL、按条件失效)时游刃有余。

7.2 权限校验装饰器#

from functools import wraps
def require_role(*roles):
"""检查用户角色的装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 假设第一个参数是 request 对象
request = kwargs.get('request') or (args[0] if args else None)
if not request or not hasattr(request, 'user'):
raise PermissionError("未登录")
user_role = getattr(request.user, 'role', None)
if user_role not in roles:
raise PermissionError(
f"需要 {'/'.join(roles)} 权限,当前角色: {user_role}"
)
return func(*args, **kwargs)
return wrapper
return decorator
# 使用示例
@require_role("admin", "superadmin")
def delete_user(request, user_id):
return f"用户 {user_id} 已删除"
@require_role("editor", "admin")
def publish_article(request, article_id):
return f"文章 {article_id} 已发布"

这个模式在 Django、Flask 等框架中随处可见。理解了原理,你可以根据自己的业务逻辑自定义任何权限校验。

7.3 日志 + 性能监控组合装饰器#

from functools import wraps
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def monitor(log_args=True, warn_threshold=1.0):
"""生产级监控装饰器:记录调用日志 + 慢查询告警"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 构造日志上下文
func_name = f"{func.__module__}.{func.__qualname__}"
if log_args:
args_repr = [repr(a) for a in args[:3]] # 最多记录3个位置参数
kwargs_repr = [f"{k}={v!r}" for k, v in list(kwargs.items())[:3]]
signature = ", ".join(args_repr + kwargs_repr)
if len(args) > 3 or len(kwargs) > 3:
signature += ", ..."
else:
signature = "..."
logger.info(f"▶️ 调用 {func_name}({signature})")
start = time.perf_counter()
try:
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
if elapsed > warn_threshold:
logger.warning(
f"🐢 慢调用 {func_name}: {elapsed:.3f}s "
f"(阈值: {warn_threshold}s)"
)
else:
logger.info(f"✅ {func_name} 完成: {elapsed:.3f}s")
return result
except Exception as e:
elapsed = time.perf_counter() - start
logger.error(
f"❌ {func_name} 异常: {type(e).__name__}: {e} "
f"(耗时: {elapsed:.3f}s)"
)
raise
return wrapper
return decorator
@monitor(warn_threshold=0.5)
def query_database(sql, params=None):
time.sleep(0.8) # 模拟慢查询
return [{"id": 1}]
query_database("SELECT * FROM users WHERE id = %s", params=(42,))
# WARNING - 🐢 慢调用 __main__.query_database: 0.801s (阈值: 0.5s)

八、装饰器 vs 其他方案:什么时候不该用装饰器#

装饰器不是万能的。以下场景你应该考虑其他方案:

场景装饰器更好的替代方案
简单的一次性逻辑过度设计直接写在函数里
需要访问类实例状态不方便使用 Mixin 或基类方法
复杂的控制流(如事务)嵌套地狱上下文管理器 with
需要动态决定是否应用静态绑定策略模式 / 中间件

装饰器最适合的场景:横切关注点(cross-cutting concerns)——日志、缓存、权限、重试、性能监控——这些逻辑跟业务无关但到处都要用。

九、进阶技巧:可选参数装饰器#

有没有注意到,带参数和不带参数的装饰器用法不一致?

@retry # 不带括号
@retry() # 带空括号
@retry(max=3) # 带参数

让三种写法都支持的技巧:

from functools import wraps
def smart_retry(_func=None, *, max_attempts=3, delay=1):
"""同时支持 @smart_retry 和 @smart_retry(max_attempts=5) 两种写法"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"重试 {attempt}/{max_attempts}...")
time.sleep(delay)
return wrapper
if _func is not None:
# @smart_retry 不带括号的情况
return decorator(_func)
# @smart_retry() 或 @smart_retry(max_attempts=5) 的情况
return decorator
# 三种写法都可以!
@smart_retry
def api_call_v1():
pass
@smart_retry()
def api_call_v2():
pass
@smart_retry(max_attempts=5, delay=0.5)
def api_call_v3():
pass

核心技巧是利用 _func=None 做判断:如果第一个参数是函数(不带括号调用),直接装饰;否则返回装饰器等待下一步调用。

十、常见坑和避坑清单#

坑 1:装饰器在导入时就执行#

def register(func):
print(f"注册函数: {func.__name__}") # 导入模块时就会打印!
return func
@register
def my_handler():
pass
# 即使没有调用 my_handler(),"注册函数: my_handler" 也会打印

这是特性,不是 bug——Flask 的 @app.route 就是利用这个特性在导入时收集路由的。但如果你不需要这个行为,确保副作用只在 wrapper 内部。

坑 2:装饰器破坏类方法的 self#

# ❌ 错误示范
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调用: {func.__name__}")
return func(*args, **kwargs)
return wrapper
class MyService:
@log_call
def process(self):
print(f"self = {self}") # self 是正常的!
# 其实 *args 会自动捕获 self,所以这里没问题

其实 *args 会自动捕获 self,所以上面的代码是正确的。真正的坑是忘了 *args,硬编码了参数。

坑 3:在循环中创建装饰器闭包#

# ❌ 经典闭包陷阱
decorators = []
for i in range(3):
def make_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"装饰器 {i}") # i 始终是 2!
return func(*args, **kwargs)
return wrapper
decorators.append(make_decorator)
# ✅ 正确做法:用默认参数捕获当前值
for i in range(3):
def make_decorator(func, _i=i): # _i=i 在定义时求值
@wraps(func)
def wrapper(*args, **kwargs):
print(f"装饰器 {_i}")
return func(*args, **kwargs)
return wrapper
decorators.append(make_decorator)

总结#

装饰器的核心就三件事:

  1. 函数是对象——可以传来传去
  2. 闭包捕获变量——wrapper 能访问外层作用域
  3. @ 是语法糖——@deco 等于 func = deco(func)

掌握这三点,你就能:

  • 写出干净的 AOP 代码,把横切关注点从业务逻辑中剥离
  • 读懂 Flask/Django/FastAPI 等框架的源码
  • 设计自己的插件系统和中间件

装饰器不是炫技,是 Python 程序员的基本功。从今天开始,试着把你项目里重复的 try-except、日志打印、权限校验抽成装饰器——你会发现代码变得前所未有的干净。

📌 行动建议:打开你手头的项目,找出至少 3 处重复的”样板代码”,尝试用装饰器重构。这是掌握装饰器最快的方式。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Python 装饰器深度指南:从语法糖到元编程利器
https://boke.hackerdream.xyz/posts/python-decorators-deep-dive/
作者
晴天
发布于
2026-04-25
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
Python 上下文管理器深度实战:从 with 语句到自定义资源管控
Python入门进阶 深入解析 Python 上下文管理器的工作原理,从 __enter__/__exit__ 协议到 contextlib 工具箱,涵盖数据库连接池、临时环境变量、性能计时器等 6 个生产级实战案例。
2
Python 类型提示完全实战指南:从「动态一时爽」到「重构火葬场」的救赎之路
Python入门进阶 深入解析 Python 类型提示(Type Hints)的实战用法,涵盖基础语法、泛型、Protocol、TypeGuard、dataclass 集成、mypy 配置,帮助你写出更安全可维护的 Python 代码。
3
Python match/case 结构化模式匹配:从入门到真正会用
Python入门进阶 深入解析 Python 3.10+ match/case 语法,涵盖值匹配、解构、守卫条件、类模式等实战技巧,用真实场景告诉你 if-elif 的终结者到底强在哪。
4
Python asyncio 异步编程实战:从回调地狱到优雅并发
Python实战 深入讲解 Python asyncio 核心机制、async/await 语法、并发模式与实战技巧,附完整可运行代码示例和性能对比数据,帮你从同步思维跃迁到异步世界。
5
Python 生成器深度实战:用 yield 优雅处理百万级数据
Python入门进阶 深入解析 Python 生成器的工作原理、yield 与 yield from 的区别、生成器表达式的性能优势,结合大文件处理、数据管道、协程通信等实战场景,教你写出内存友好的 Pythonic 代码。
随机文章 随机推荐
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录