Python中的装饰器
1. 开篇:为什么需要装饰器?
装饰器的核心思想是在不修改被装饰对象(通常是函数或方法)的源代码和调用方式的前提下,为其增加额外的功能。
一个简单的问题场景
假设我们有一个核心业务函数:
1 | def business_logic(): |
现在,我们有一个新的需求:在每次执行这个函数前后,打印一条日志,记录函数的开始和结束。
不使用装饰器的解决方案
方案一:直接修改函数代码(最差的方式)
1 | def business_logic(): |
- 弊端:
- 违反了开放/封闭原则:我们修改了函数的内部实现。
- 代码冗余:如果有很多函数都需要这个日志功能,我们就得在每个函数里重复添加这些代码,违反了 DRY (Don’t Repeat Yourself) 原则。
方案二:定义一个新的函数来包装(接近装饰器的思想)
1 | def logged_business_logic(): |
- 弊端:
- 我们必须使用一个新的函数名 (
logged_business_logic
),所有原来调用business_logic
的地方都需要修改,这在大型项目中是不可接受的。
- 我们必须使用一个新的函数名 (
我们需要一种方法,既能添加功能,又不改变 business_logic
的源码和调用方式。这就引出了装饰器的核心思想。
2. 核心前置知识:Python 函数是一等公民
在 Python 中,函数是“一等公民”(First-Class Citizens),这意味着它们可以像任何其他对象(如整数、字符串、列表)一样被对待。
a. 函数可以被赋值给变量
1 | def say_hello(): |
b. 函数可以作为参数传递
1 | def process_data(data, operation_func): |
c. 函数可以作为返回值(闭包)
这是理解装饰器最关键的一点。一个函数可以定义在另一个函数内部,并且内部函数可以引用外部函数的变量。当外部函数返回内部函数时,就形成了一个闭包(Closure)。
1 | def outer_function(msg): |
hello_func
和 goodbye_func
就是闭包,它们“记住”了创建它们时所处的环境(即 message
变量的值)。
3. 装饰器的诞生:从手动装饰到语法糖
有了上面的知识,我们现在可以完美地解决第 1 节中的问题了。
手动实现装饰器逻辑
我们可以编写一个函数,它接收一个函数作为参数,并返回一个“增强版”的新函数。
1 | def logger_decorator(original_function): |
我们成功了!但是,我们还是得调用一个新的函数名 decorated_business_logic
。不过,我们可以利用“函数可以被赋值给变量”的特性:
1 | business_logic = logger_decorator(business_logic) |
这行 business_logic = logger_decorator(business_logic)
就是装饰器工作的核心原理。
@
语法糖:让代码更优雅
Python 提供了一个专门的语法糖 @
来简化这个赋值过程。
1 |
|
上面这段代码和下面这句是完全等价的:
1 | def business_logic(): |
@decorator
放在函数定义前,就表示在函数定义之后,立即将这个函数作为参数传递给 decorator
,并将返回值重新赋给原函数名。
4. 构建一个通用的装饰器
我们上面的 logger_decorator
太简单了,它不能处理带参数和返回值的函数。
1 |
|
a. 处理被装饰函数的参数 (*args
, **kwargs
)
为了让我们的装饰器能够接受任意参数,我们需要在 wrapper
函数中使用 *args
和 **kwargs
。
1 | def general_decorator(func): |
b. 处理被装饰函数的返回值
如果被装饰函数有返回值,我们的 wrapper
也应该将它返回。
1 | def general_decorator_with_return(func): |
5. functools.wraps
:一个不可或缺的助手
到目前为止,我们的装饰器看起来很完美,但其实有一个隐藏的问题:它丢失了原函数的元信息(metadata),比如函数名、文档字符串(docstring)等。
未使用 wraps
的问题
1 | # 使用上面的 general_decorator_with_return |
这是因为 business_logic
这个名字现在指向的是 wrapper
函数,它的元信息自然是 wrapper
的。这对于调试、文档生成和代码自省非常不利。
@wraps
的作用和用法
functools
模块中的 wraps
本身也是一个装饰器,专门用来装饰我们的 wrapper
函数,它能将原始函数的元信息复制到 wrapper
函数上。
最终、最规范的装饰器模板:
1 | from functools import wraps |
记住:编写任何装饰器时,都应该使用 @functools.wraps
!
6. 进阶装饰器
a. 带参数的装饰器
如果我们想让装饰器本身可以接收参数,比如 @repeat(3)
,让函数执行 3 次。
这就需要一个三层嵌套的结构:一个函数(工厂函数)接收参数,返回一个装饰器,这个装饰器再返回 wrapper
函数。
1 | from functools import wraps |
执行过程分解:
repeat(num_times=3)
被首先调用。- 它返回
decorator_repeat
函数。 - Python 接着执行
@decorator_repeat
,即greet = decorator_repeat(greet)
。 decorator_repeat
返回wrapper
函数,并赋值给greet
。
所以,@repeat(3)
实际上是 @(repeat(3))
的效果。
b. 类装饰器
我们也可以用类来实现装饰器。这在需要维护状态时特别有用。一个类要成为装饰器,需要实现 __init__
和 __call__
方法。
__init__
:接收被装饰的函数。__call__
:实现wrapper
的逻辑。
1 | class CallCounter: |
c. 装饰器栈(多个装饰器)
一个函数可以被多个装饰器同时装饰。
1 | from functools import wraps |
执行顺序:
装饰器的应用顺序是从下到上(靠近函数的先应用)。
所以 my_function
首先被 decorator_2
包装,然后这个结果再被 decorator_1
包装。my_function = decorator_1(decorator_2(my_function))
而代码的执行顺序则是像洋葱一样,从外到内,再从内到外。
1 | my_function() |
7. 实际应用场景
- 日志记录 (Logging):在函数执行前后记录日志信息,如我们最初的例子。
- 性能计时 (Timing):记录函数执行所需的时间。
1
2
3
4
5
6
7
8
9
10import time
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"'{func.__name__}' took {end_time - start_time:.4f} seconds.")
return result
return wrapper - 权限校验 (Authorization):在 Web 框架(如 Flask, Django)中,检查用户是否登录或有特定权限。
1
2
3
4
5
6
7
8
9
10
11
12
13# Flask 示例
def login_required(f):
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
def secret_page():
return "这是秘密页面!" - 缓存 (Caching / Memoization):对于计算成本高的函数,缓存其结果。Python 3.9+ 提供了
functools.cache
。1
2
3
4
5
6
7
8
9
10
11
12
13
14import functools
import time
def fibonacci(n):
if n < 2:
return n
time.sleep(0.1) # 模拟耗时计算
return fibonacci(n-1) + fibonacci(n-2)
# 第一次调用会很慢
print(fibonacci(10))
# 第二次调用会立刻返回结果,因为结果被缓存了
print(fibonacci(10)) - 输入验证 (Input Validation):在函数执行前检查输入参数是否合法。
8. 总结
- 核心思想:装饰器是一个函数,它接收一个函数作为输入,并返回一个新的函数,旨在不修改原函数代码的情况下为其添加功能。
- 语法糖:
@decorator
是my_func = decorator(my_func)
的简写。 - 基础模板:装饰器内部通常定义一个
wrapper
函数,使用*args, **kwargs
来处理任意参数,并确保返回原函数的计算结果。 - 最佳实践:务必使用
@functools.wraps
来保留原函数的元信息。 - 灵活性:装饰器可以通过带参数、使用类、或层叠使用来满足复杂的需求。
- 强大功能:它们是 Python 中实现横切关注点(如日志、权限、缓存)的强大工具,让业务代码保持纯净和专注。