0%

Python装饰器

Python 装饰器:从入门到工程实践

在 Python 的众多特性中,装饰器(Decorator)无疑是最具魅力但也最容易让人困惑的概念之一。很多初学者在第一次看到 @ 符号时,往往觉得它像是一种“魔法”。但实际上,装饰器并非魔法,而是一种优雅的设计模式,它允许我们在不修改原函数代码的情况下,动态地增强或修改函数的行为。

本文将从基础原理出发,带你深入理解装饰器的本质,并探讨如何在实际工程中编写高质量、可维护的装饰器。

类似于java的AOP

装饰器的本质:语法糖与闭包

要理解装饰器,首先要理解 Python 中的“函数是一等公民”。这意味着函数可以像变量一样被传递、作为参数,甚至作为返回值。

装饰器本质上是一个高阶函数:它接收一个函数作为参数,并返回一个新的函数。

让我们看一个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
def my_decorator(func):
def wrapper(*args, **kwargs):
print("在函数执行之前...")
result = func(*args, **kwargs)
print("在函数执行之后...")
return result
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()

当你运行这段代码时,输出会显示“在函数执行之前…”,然后是“Hello!”,最后是“在函数执行之后…”。

这里的 @my_decorator 实际上就是 say_hello = my_decorator(say_hello) 的语法糖。

核心机制解析:

  1. 闭包wrapper 函数定义在 my_decorator 内部,它“记住”了外部传入的 func。这就是闭包。
  2. 代理模式wrapper 并没有改变 say_hello 的逻辑,它只是“包裹”了原函数,在调用前后插入了额外的逻辑(日志记录)。

进阶:带参数的装饰器

在实际开发中,我们往往需要更灵活的装饰器,比如控制重试次数、设置日志级别等。这就需要装饰器本身也能接收参数。

这就引入了三层嵌套结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def repeat(num_times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")

greet("Alice")

逻辑拆解:

  1. 最外层 repeat 接收装饰器参数(如 num_times)。
  2. 中间层 decorator 接收被装饰的函数 func
  3. 最内层 wrapper 执行实际的业务逻辑。

这种结构虽然多了一层缩进,但它极大地扩展了装饰器的通用性。

工程实践中的“坑”与最佳实践

虽然装饰器很强大,但如果写得不规范,会给调试和维护带来灾难。以下是两个必须遵守的工程规范。

1. 必须使用 @functools.wraps

如果你直接打印被装饰函数的 __name__,你会发现它变成了 wrapper,原本的函数名和文档字符串(docstring)都丢失了。这会导致日志记录错误,或者让依赖反射的工具(如 IDE 提示、文档生成工具)失效。

正确写法:

1
2
3
4
5
6
7
import functools

def my_decorator(func):
@functools.wraps(func) # 关键在这里
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@functools.wraps(func) 的作用是将原函数的元数据(名称、文档、模块等)复制到包装函数上,确保装饰后的函数“看起来”还是原来的那个函数。

2. 异步函数的特殊处理

在现代 Python 开发(尤其是 Web 框架如 FastAPI、Sanic)中,异步编程非常普遍。普通的装饰器不能直接用于 async def 定义的函数,因为 wrapper 也必须支持异步。

异步装饰器模板:

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 asyncio
import time
import functools

# 1. 定义异步装饰器
def async_timer(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs): # 注意:这里必须是 async def
start = time.time()
# 注意:调用原函数必须加 await
result = await func(*args, **kwargs)
end = time.time()
print(f"⏱️ {func.__name__} 耗时: {end - start:.4f} 秒")
return result
return wrapper

# 2. 定义一个模拟的异步任务
@async_timer
async def fetch_data():
print("开始获取数据...")
# 模拟一个耗时的 I/O 操作(比如网络请求)
await asyncio.sleep(1)
print("数据获取完成")
return "模拟数据"

# 3. 运行异步代码
async def main():
data = await fetch_data()
print(f"结果: {data}")

if __name__ == "__main__":
asyncio.run(main())

如果你的装饰器需要同时支持同步和异步函数,可以使用 inspect.iscoroutinefunction(func) 进行判断,动态返回不同的包装器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio
import inspect
import functools

def universal_decorator(func):
# 判断原函数是否是异步函数
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
print("异步逻辑前置...")
return await func(*args, **kwargs)
return async_wrapper
else:
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
print("同步逻辑前置...")
return func(*args, **kwargs)
return sync_wrapper

典型应用场景

装饰器在 Python 生态中无处不在,掌握它可以让你写出更“Pythonic”的代码。

  • 权限验证:在 Flask 或 Django 中,使用装饰器检查用户是否登录或是否具有管理员权限。
  • 性能监控与计时:自动统计函数执行时间,无需在每个函数内部手动写计时代码。
  • 缓存机制:利用 functools.lru_cache 或自定义装饰器,缓存昂贵函数的计算结果,避免重复计算。
  • 重试机制:当网络请求失败时,自动重试指定次数。

结语

装饰器是 Python 中“横切关注点”分离的利器。它将日志、鉴权、缓存等通用逻辑从业务代码中抽离出来,使得代码更加整洁、模块化。

欢迎关注我的其它发布渠道