装饰器与上下文管理
学习目标
- 理解函数是对象以及装饰器的闭包结构。
- 用
functools.wraps保留被装饰函数元数据。 - 用
with和contextlib管理资源生命周期。
核心知识点
装饰器接收一个可调用对象并返回新的可调用对象,常用于日志、计时、权限等横切逻辑。上下文管理器实现 __enter__/__exit__,保证资源在离开 with 块时清理。
常用用法
最小装饰器
python
def announce(function):
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return f"result={result}"
return wrapper
@announce
def add(left: int, right: int) -> int:
return left + right
print(add(2, 3))
# 输出:result=5装饰器语法等价于 add = announce(add),定义顺序很重要。
functools.wraps
python
from functools import wraps
def trace(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
@trace
def greet(name: str) -> str:
"""返回问候语。"""
return f"Hi, {name}"
print(greet.__name__, greet.__doc__)
# 输出:greet 返回问候语。wraps 复制 __name__、__doc__ 等元数据,便于调试、文档和类型工具识别原函数。
带参数的装饰器
python
from functools import wraps
def repeat(times: int):
def decorate(function):
@wraps(function)
def wrapper(*args, **kwargs):
return [function(*args, **kwargs) for _ in range(times)]
return wrapper
return decorate
@repeat(2)
def identify(value: str) -> str:
return value
print(identify("ok"))
# 输出:['ok', 'ok']三层结构分别负责配置、接收函数、接收调用参数;装饰器配置应在定义时校验。
类作为装饰器
python
class CallCounter:
def __init__(self, function):
self.function = function
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.function(*args, **kwargs)
@CallCounter
def double(value: int) -> int:
return value * 2
double(2)
print(double(3), double.count)
# 输出:6 2类装饰器适合需要保存状态的场景,但要考虑如何暴露被装饰函数的元数据。
with 使用上下文管理器
python
from io import StringIO
buffer = StringIO()
with buffer as stream:
stream.write("hello")
content = stream.getvalue()
print(buffer.closed, content)
# 输出:True hellowith 把获取和释放资源绑定起来;文件、锁和临时目录都应优先使用此形式。
contextlib.contextmanager
python
from contextlib import contextmanager
@contextmanager
def labeled(label: str):
print(f"enter:{label}")
# 输出:enter:demo
try:
yield label.upper()
finally:
print(f"exit:{label}")
# 输出:exit:demo
with labeled("demo") as value:
result = value
print(result)
# 输出:DEMOyield 前是进入逻辑,之后是退出逻辑;finally 保证异常时也清理。
不常用但需要知道
functools.lru_cache
python
from functools import lru_cache
@lru_cache(maxsize=2)
def square(value: int) -> int:
return value * value
print(square(4), square.cache_info().hits)
# 输出:16 0缓存要求参数可哈希,且函数应是无副作用的;数据变化后要考虑 cache_clear()。
contextlib.suppress
python
from contextlib import suppress
values = {"name": "Ada"}
with suppress(KeyError):
del values["missing"]
print(values)
# 输出:{'name': 'Ada'}只对明确可忽略的异常使用,避免把错误隐藏在宽泛的 suppress 中。
contextlib.ExitStack
python
from contextlib import ExitStack
from io import StringIO
with ExitStack() as stack:
streams = [stack.enter_context(StringIO()) for _ in range(2)]
streams[0].write("a")
print(len(streams), streams[0].closed)
# 输出:2 True资源数量动态变化时,ExitStack 能统一管理退出顺序。
易混点
- 装饰器在定义时执行,包装函数在调用时执行。
- 不用
wraps会丢失函数名和文档,某些框架也会因此无法识别签名。 with不会自动捕获所有异常,只负责进入和退出协议。- 装饰器顺序从下到上应用,调用时从上到下包裹。
- 缓存只适合可重复、参数可哈希且结果稳定的函数。
课后小问
@trace语法的等价写法是什么? 答案:函数定义后执行function = trace(function)。 解析:装饰器替换的是绑定到函数名的对象,调用者之后看到的是包装函数。为什么上下文管理器要把清理写在
finally? 答案:这样正常离开和异常离开都会执行清理。 解析:资源释放不能依赖业务主体是否成功,否则异常路径会泄漏资源。
本节小结
装饰器封装调用前后行为,上下文管理器封装资源进出;二者都应保持边界小、异常语义明确、元数据可追踪。
快速回顾
- 函数包装:闭包、
@decorator - 元数据:
functools.wraps - 参数装饰器:三层函数结构
- 资源管理:
with、__enter__、__exit__ - 工具:
contextmanager、ExitStack、lru_cache
阿源的知识库