在 Python 中,装饰器(Decorator) 是一种极其强大且优雅的设计模式。它的核心作用是:在不修改原函数代码和调用方式的前提下,动态地为函数(或类)增加额外的功能。
通俗地说,就像给房子(原函数)加装防盗门或智能锁(新功能),房子本身的结构没变,但安全性提升了,而且住户(调用者)开门的方式也没变。
下面我将从底层原理、标准模板、进阶用法到实际场景,为你系统梳理 Python 装饰器。
1. 核心原理:闭包 + 高阶函数
装饰器本质上是一个高阶函数(接收函数作为参数,并返回一个新函数)。它利用了 Python 的闭包特性。
# 1. 定义一个装饰器(本质是函数)
def my_decorator(func):
# 2. 定义内部函数(闭包),接收原函数的参数
def wrapper(*args, **kwargs):
print("👉 [执行前] 准备调用函数...")
# 3. 调用原函数,并保存结果
result = func(*args, **kwargs)
print("👈 [执行后] 函数调用完毕!")
return result # 返回原函数的结果
# 4. 返回内部函数(注意:没有括号,返回的是函数对象本身)
return wrapper
# 5. 使用装饰器(语法糖 @)
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
# 调用时,实际执行的是 wrapper("Alice")
say_hello("Alice")输出:
👉 [执行前] 准备调用函数...
Hello, Alice!
👈 [执行后] 函数调用完毕!注:@my_decorator 只是 say_hello = my_decorator(say_hello) 的语法糖。
2. ⚠️ 必须掌握的标准模板:@wraps
上面的基础写法有一个致命缺陷:原函数的元信息(如函数名 __name__、文档字符串 __doc__)会被 wrapper 覆盖,这会导致调试困难和某些依赖元信息的库(如 FastAPI、Flask)报错。
✅ 标准做法:始终使用 functools.wraps。
import functools
import time
def timer_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
@timer_decorator
def heavy_task():
"""这是一个耗时任务"""
time.sleep(0.5)
heavy_task()
print("函数名:", heavy_task.__name__) # 输出: heavy_task (如果没有 @wraps,会输出 wrapper)
print("文档:", heavy_task.__doc__) # 输出: 这是一个耗时任务3. 进阶用法
① 带参数的装饰器(三层嵌套)
如果你希望装饰器本身也能接收参数(例如:指定重试次数、日志级别),你需要三层函数嵌套。
- 第一层:接收装饰器的参数。
- 第二层:接收被装饰的函数。
- 第三层:
wrapper,接收原函数的参数。
def retry(max_attempts: int = 3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"❌ 第 {attempt} 次尝试失败: {e}")
if attempt == max_attempts:
raise # 达到最大次数,抛出异常
return wrapper
return decorator
@retry(max_attempts=3)
def connect_to_db():
print("尝试连接数据库...")
raise ConnectionError("网络超时")
# connect_to_db() # 取消注释运行,会重试 3 次后抛出异常② 多个装饰器的执行顺序(洋葱模型)
当一个函数被多个装饰器修饰时,装饰的顺序是从下到上,执行的顺序是从外到内。
def decorator_a(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("A: 进入")
result = func(*args, **kwargs)
print("A: 退出")
return result
return wrapper
def decorator_b(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("B: 进入")
result = func(*args, **kwargs)
print("B: 退出")
return result
return wrapper
@decorator_a
@decorator_b
def my_func():
print(">>> 核心逻辑执行 <<<")
my_func()输出解析:
A: 进入 # 1. decorator_a 的 wrapper 开始
B: 进入 # 2. decorator_b 的 wrapper 开始
>>> 核心逻辑执行 <<< # 3. 原函数执行
B: 退出 # 4. decorator_b 的 wrapper 结束
A: 退出 # 5. decorator_a 的 wrapper 结束(等价于 decorator_a(decorator_b(my_func)))
③ 装饰类 (Class Decorator)
装饰器不仅可以装饰函数,还可以装饰类(常用于实现单例模式或为类的所有方法添加功能)。
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
@singleton
class DatabaseConnection:
def __init__(self):
print("初始化数据库连接...")
db1 = DatabaseConnection() # 输出: 初始化数据库连接...
db2 = DatabaseConnection() # 无输出,直接返回 db1
print(db1 is db2) # 输出: True4. 实际工程中的高频应用场景
- 权限校验:在 Web 框架(如 Flask/FastAPI)中,检查用户是否登录或是否有特定角色。
- 日志记录:自动记录函数的输入参数、返回值和执行时间(如上面的
timer_decorator)。 - 缓存 (Memoization):Python 内置了
@functools.lru_cache或@functools.cache,自动缓存函数的返回值,避免重复计算(极其适合递归或昂贵的 I/O 操作)。
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2: return n
return fibonacci(n-1) + fibonacci(n-2)- 事务管理:在数据库操作中,自动处理
commit和rollback。
5. 装饰器的最佳实践与避坑指南
- 始终使用
@functools.wraps:这是专业 Python 代码的标志。 - 保持装饰器轻量:装饰器内部的逻辑应该尽量简单。如果逻辑复杂,应该将其拆分为独立的辅助函数。
- 不要滥用:如果一个功能只能通过装饰器实现,但导致代码难以理解(例如过度嵌套的带参数装饰器),不如直接在函数内部调用一个普通的辅助函数。
- 注意异步函数:如果你要装饰
async def定义的异步函数,你的wrapper也必须是async def,或者使用专门的异步装饰器库(如asyncer)。
import functools
def async_timer(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs): # 👈 必须是 async
start = time.time()
result = await func(*args, **kwargs) # 👈 必须 await
print(f"Async 耗时: {time.time() - start}")
return result
return wrapper装饰器是 Python 迈向“高级”的必经之路。你可以尝试自己写一个 “如果函数执行时间超过 N 秒,就打印警告” 的装饰器来练手。如果有写不出来的地方,或者想了解某个具体框架(如 FastAPI)中的装饰器用法,随时告诉我!