199_Python上下文管理器¶
一句话概述¶
Python上下文管理器通过with语句和__enter__/__exit__协议(或@contextmanager装饰器)自动管理资源的获取和释放,确保文件、数据库连接、锁等资源在使用完毕后被正确清理,即使发生异常也不会泄漏。
核心知识点表格¶
| 知识点 | 说明 |
|---|---|
| with语句 | 上下文管理器的语法入口 |
| enter | 进入上下文时调用,返回值赋给as变量 |
| exit | 退出上下文时调用,处理异常和清理 |
| @contextmanager | 用生成器函数快速创建上下文管理器 |
| contextlib模块 | 提供多种上下文管理器工具 |
| 资源管理 | 文件、锁、数据库连接、临时目录等 |
| 异常处理 | __exit__返回True可以抑制异常 |
| 异步上下文 | async with + aenter/aexit |
步骤详解¶
第一步:基本上下文管理器¶
白话解释:上下文管理器就像一个"管家",你进门时它帮你开灯(enter),离开时帮你关灯(exit),即使你匆忙逃离(异常)也不会忘记关灯。
# 基于类的上下文管理器
class FileManager:
def __init__(self, filename, mode='r'):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
# 返回False(默认):异常继续传播
# 返回True:异常被抑制
return False
with FileManager('data.txt', 'w') as f:
f.write("Hello, Context Manager!")
# 数据库连接管理器
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.conn = None
def __enter__(self):
import sqlite3
self.conn = sqlite3.connect(self.connection_string)
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.conn.rollback() # 异常时回滚
else:
self.conn.commit() # 正常时提交
self.conn.close()
return False
with DatabaseConnection('mydb.sqlite') as conn:
cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS users (name TEXT)")
cursor.execute("INSERT INTO users VALUES ('Alice')")
第二步:使用@contextmanager装饰器¶
白话解释:写一个类太麻烦了。@contextmanager让你用生成器函数(带yield的函数)快速创建上下文管理器。yield之前是__enter__,yield之后是__exit__。
from contextlib import contextmanager
import time
@contextmanager
def timer(label=""):
"""计时上下文管理器"""
start = time.perf_counter()
try:
yield # 这里把控制权交给with块
finally:
elapsed = time.perf_counter() - start
print(f"{label} 耗时: {elapsed:.4f}s")
with timer("数据处理"):
time.sleep(1)
# 数据处理 耗时: 1.0001s
@contextmanager
def temporary_directory():
"""创建临时目录,用完自动删除"""
import tempfile, shutil
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
shutil.rmtree(tmpdir)
with temporary_directory() as tmpdir:
print(f"临时目录: {tmpdir}")
# 使用临时目录...
# 退出后自动删除
@contextmanager
def change_directory(path):
"""临时切换工作目录"""
import os
old_dir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_dir)
@contextmanager
def suppress_output():
"""抑制stdout输出"""
import sys, os
old_stdout = sys.stdout
sys.stdout = open(os.devnull, 'w')
try:
yield
finally:
sys.stdout.close()
sys.stdout = old_stdout
第三步:高级模式¶
from contextlib import ExitStack, suppress, redirect_stdout
# 1. ExitStack:动态管理多个上下文
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in ['a.txt', 'b.txt', 'c.txt']]
# 所有文件在退出时自动关闭
# 2. suppress:忽略特定异常
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove('nonexistent.txt') # 不存在也不会报错
# 3. 可重入上下文管理器
@contextmanager
def managed_resource():
print("获取资源")
yield "resource"
print("释放资源")
# 4. 上下文管理器作为装饰器
class log_execution:
"""同时作为上下文管理器和装饰器"""
def __init__(self, func_or_label=None):
if callable(func_or_label):
self.label = func_or_label.__name__
self.func = func_or_label
else:
self.label = func_or_label or "block"
self.func = None
def __enter__(self):
print(f"[START] {self.label}")
self.start = time.perf_counter()
return self
def __exit__(self, *exc):
elapsed = time.perf_counter() - self.start
print(f"[END] {self.label} ({elapsed:.4f}s)")
return False
def __call__(self, *args, **kwargs):
if self.func:
with self:
return self.func(*args, **kwargs)
# 5. 异步上下文管理器
class AsyncDBConnection:
async def __aenter__(self):
self.conn = await asyncio.connect("db://localhost")
return self.conn
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.conn.close()
# async with AsyncDBConnection() as conn:
# await conn.execute("SELECT 1")
实战命令速查¶
# 文件操作(最常见)
with open('file.txt', 'r') as f:
data = f.read()
# 锁管理
import threading
lock = threading.Lock()
with lock:
# 临界区代码
pass
# 多上下文管理(Python 3.10+)
with (open('a.txt') as a, open('b.txt') as b):
pass
# contextlib实用工具
from contextlib import closing, suppress, redirect_stdout, ExitStack
with closing(urllib.request.urlopen('http://example.com')) as page:
pass
面试常问点¶
Q1: __exit__方法的三个参数是什么?返回True和False有什么区别? A: exc_type是异常类型,exc_val是异常实例,exc_tb是traceback对象。如果没有异常发生,三个参数都是None。返回True表示异常已被处理(抑制),返回False(默认)表示异常继续向上传播。
Q2: @contextmanager和类方式实现各自的优劣? A: @contextmanager更简洁(只需yield),适合简单场景。类方式更灵活,支持继承和状态管理,适合复杂场景。类方式可以同时作为装饰器使用(实现__call__)。
Q3: with语句的执行流程是什么? A: (1)计算with后的表达式得到上下文管理器对象;(2)调用__enter__(),返回值赋给as变量;(3)执行with块中的代码;(4)无论是否发生异常,调用__exit__();(5)如果有异常且__exit__返回False,重新抛出异常。
Q4: ExitStack的使用场景是什么? A: 当需要管理的上下文数量在运行时才能确定时(如打开动态数量的文件)。ExitStack维护一个清理回调栈,按后进先出的顺序执行清理。
Q5: 上下文管理器与try/finally有什么关系? A: 上下文管理器是try/finally的抽象封装。with语句保证__exit__始终执行,等价于try/finally中的finally块。优势是代码更简洁、可复用、不易遗漏清理逻辑。
易错点¶
- @contextmanager中忘记try/finally:yield后的代码在异常时可能不执行
- __exit__中吞掉异常:错误地返回True导致异常被静默忽略
- yield多次:@contextmanager的生成器只能yield一次
- 忘记as关键字:with open('f') as f中的as不能省略(如果需要引用)
- 上下文管理器对象复用:某些上下文管理器不支持重复进入
补充知识¶
contextlib模块常用工具¶
| 工具 | 用途 |
|---|---|
| @contextmanager | 用生成器创建上下文管理器 |
| closing(thing) | 确保调用thing.close() |
| suppress(*exceptions) | 忽略指定异常 |
| redirect_stdout(new) | 重定向标准输出 |
| ExitStack | 动态管理多个上下文 |
| nullcontext(value) | 什么都不做的上下文管理器 |
| @asynccontextmanager | 异步版@contextmanager |