Python: 装饰器与闭包
装饰器是 Python 最精妙的特性之一。它让你能在不修改原函数代码的前提下,给函数"加点料"——加日志、计时间、检查权限……很多框架和库都大量使用装饰器。理解它是 Python 进阶的重要里程碑。
1. 闭包
闭包(Closure)是装饰器的基础。简单说:内层函数记住了外层函数的变量,即使外层函数已经执行完了。
PYTHON
def make_multiplier(n):
"""创建一个乘 n 的函数"""
def multiplier(x):
return x * n # 内层函数记住了外层函数的变量 n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20
double 和 triple 虽然来自同一个工厂函数 make_multiplier,但它们各自记住了不同的 n 值(2 和 3)。这就是闭包——函数 + 它记住的外部变量。
💡 闭包的应用场景: ① 工厂函数(像上面那样创建不同配置的函数)。② 装饰器(标准实现方式)。③ 回调函数中保存上下文。
2. 装饰器基础
装饰器就是一个接收函数作为参数、返回新函数的函数:
PYTHON
def my_decorator(func):
def wrapper():
print("=== 函数执行前 ===")
func()
print("=== 函数执行后 ===")
return wrapper
@my_decorator
def say_hello():
print("你好!")
say_hello()
运行结果:
TEXT
📖 仅展示
=== 函数执行前 ===
你好!
=== 函数执行后 ===
@my_decorator 等同于 say_hello = my_decorator(say_hello)。语法糖让代码看起来更清爽。
▶ 示例:日志装饰器(难度⭐⭐)
PYTHON
def log_call(func):
"""记录函数调用"""
def wrapper(*args, **kwargs):
print(f"[LOG] 调用 {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} 返回 {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def greet(name, greeting="你好"):
return f"{greeting},{name}!"
print(add(3, 5))
print(greet("Alice"))
运行结果:
TEXT
📖 仅展示
[LOG] 调用 add((3, 5), {})
[LOG] add 返回 8
8
[LOG] 调用 greet(('Alice',), {'greeting': '你好'})
[LOG] greet 返回 你好,Alice!
3. functools.wraps:保留原函数信息
装饰器会"替换"原函数,导致函数名和文档丢失。@wraps 修复这个问题:
PYTHON
from functools import wraps
def my_decorator(func):
@wraps(func) # 保留原函数的信息
def wrapper(*args, **kwargs):
"""装饰器内部函数"""
print("函数执行前...")
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello():
"""一个简单的问候函数"""
print("你好!")
print(say_hello.__name__) # say_hello(如果没有 @wraps,会显示 wrapper)
print(say_hello.__doc__) # 一个简单的问候函数(如果没有 @wraps,会显示装饰器内部函数)
💡 写装饰器时永远加
@wraps。不加的话,被装饰后的函数名字和文档就丢了,调试起来很麻烦。从 functools 导入 wraps,在内部 wrapper 函数上加上 @wraps(func)。
4. 带参数的装饰器
有时候装饰器本身也需要参数:
PYTHON
from functools import wraps
def repeat(times=2):
"""重复执行指定次数"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"你好,{name}!")
greet("Alice")
运行结果:
TEXT
📖 仅展示
你好,Alice!
你好,Alice!
你好,Alice!
▶ 示例:计时装饰器(难度⭐⭐⭐)
PYTHON
import time
from functools import wraps
def timer(func):
"""计算函数执行时间"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[计时] {func.__name__} 耗时:{elapsed:.4f}秒")
return result
return wrapper
@timer
def slow_function():
total = 0
for i in range(1000000):
total += i
return total
result = slow_function()
print(f"结果:{result}")
运行结果(时间因电脑而异):
TEXT
📖 仅展示
[计时] slow_function 耗时:0.0452秒
结果:499999500000
▶ 示例:简易计时装饰器(难度⭐)
PYTHON
import time
from functools import wraps
def simple_timer(func):
"""简单的计时装饰器——打印函数执行耗时"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[{func.__name__}] 耗时 {elapsed:.6f} 秒")
return result
return wrapper
@simple_timer
def sum_range(n):
"""计算 1 到 n 的累加和"""
total = 0
for i in range(1, n + 1):
total += i
return total
@simple_timer
def greet(name):
"""简单问候"""
return f"你好,{name}!"
result = sum_range(100000)
print(f"累加结果:{result}")
print(greet("Alice"))
输出:
TEXT
📖 仅展示
[sum_range] 耗时 0.008123 秒
累加结果:5000050000
[greet] 耗时 0.000001 秒
你好,Alice!
5. 多个装饰器组合
一个函数可以叠加多个装饰器,执行顺序从下到上:
PYTHON
from functools import wraps
def bold(func):
@wraps(func)
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def hello():
return "你好"
print(hello()) # <b><i>你好</i></b>
# 执行顺序:先 @italic 再 @bold
# 等价于:hello = bold(italic(hello))
💡 多个装饰器从下往上执行。 想象函数是一层一层"包裹"起来的——最下面的装饰器最靠近原函数,最先执行。理解了这个顺序,多个装饰器组合起来就不会乱了。
6. 常见应用场景
- 日志记录:自动记录每个函数的调用和返回值。
- 性能计时:测量函数执行时间,找出性能瓶颈。
- 权限检查:Web 框架中用
@login_required装饰器检查用户是否登录。 - 缓存:
@functools.lru_cache()自动缓存函数结果。 - 重试:网络请求失败时自动重试指定次数。
❓ 常见问题
Q 装饰器和闭包是什么关系?
A 装饰器用闭包实现。闭包是"函数记住了外部变量",装饰器正是利用闭包让内层wrapper函数能访问到外层传来的func参数。
Q Python 自带了哪些有用的装饰器?
A
@property(属性访问)、@staticmethod(静态方法)、@classmethod(类方法)、@functools.lru_cache()(缓存)、@functools.wraps(保留函数信息)、@dataclass(数据类)。这些都在标准库中。📖 小节
- 闭包:内层函数记住外层函数的变量,即使外层已经执行完毕
- 装饰器:接收函数、返回函数的函数,用
@语法糖调用 @wraps(func)保留原函数的__name__和__doc__,写装饰器一定要加- 带参数装饰器:三层嵌套 → 外层接收参数、中层接收函数、内层处理逻辑
- 多个装饰器从下往上叠加执行
- 装饰器是"函数的函数"——在不修改原代码的前提下增强函数
📝 作业
-
基础题(难度⭐):写一个装饰器
print_call,在函数执行时打印"正在调用 xxx 函数"。 -
进阶题(难度⭐⭐):写一个装饰器
retry(max_attempts=3),当函数抛出异常时自动重试指定次数。如果重试后仍然失败,抛出最后的异常。提示:在 wrapper 中用 for 循环 + try-except。 -
挑战题(难度⭐⭐⭐):写一个带参数的装饰器
cache_result(ttl=60),缓存函数的返回值。在 ttl 秒内调用同一个参数时直接返回缓存结果,超过 ttl 重新计算。