Python教程27:上下文管理器与with语句
“始有终,开必合。”
我们已经多次使用with语句打开文件,今天深入学习它的原理——上下文管理器(Context Manager)。这是Python资源管理的利器,让代码更安全、更优雅。
1. 什么是上下文管理器
问题场景
资源(文件、网络连接、数据库连接、锁)需要正确管理:
1# 问题代码:可能忘记关闭
2file = open("data.txt")
3content = file.read()
4file.close() # 忘记关闭?异常时未关闭?
5
6# try-finally保证关闭
7file = open("data.txt")
8try:
9 content = file.read()
10finally:
11 file.close() # 总是执行
12
13# 但每次都这样写很繁琐...
上下文管理器(Context Manager):
- 自动管理资源的生命周期
- 确保资源的正确获取和释放
- 使用
with语句 - 即使发生异常也能正确清理
1# with语句:简洁且安全
2with open("data.txt") as file:
3 content = file.read()
4# 自动关闭文件,即使有异常也会关闭
with语句的优势
- 自动清理:离开with块自动释放资源
- 异常安全:即使发生异常也会执行清理
- 代码简洁:不需要显式的try-finally
- 可读性强:明确表达资源管理意图
2. 上下文管理器协议
上下文管理器需要实现两个魔术方法:
enter()
进入with块时调用,返回资源对象:
1def __enter__(self):
2 """
3 进入上下文时调用
4 - 获取资源
5 - 返回的对象赋给as后的变量
6 """
7 return self # 或其他资源对象
exit()
离开with块时调用,执行清理操作:
1def __exit__(self, exc_type, exc_value, traceback):
2 """
3 退出上下文时调用
4
5 Args:
6 exc_type: 异常类型(无异常时为None)
7 exc_value: 异常值
8 traceback: 异常追踪信息
9
10 Returns:
11 True: 抑制异常(异常不会传播)
12 False/None: 异常正常传播
13 """
14 # 执行清理操作
15 self.cleanup()
16
17 # 返回True会抑制异常
18 return False
3. 创建上下文管理器
方法1:类实现
1class FileManager:
2 """
3 自定义文件管理器
4 - 展示上下文管理器的基本实现
5 - __enter__打开文件
6 - __exit__关闭文件
7 """
8 def __init__(self, filename, mode):
9 self.filename = filename
10 self.mode = mode
11 self.file = None
12
13 def __enter__(self):
14 """打开文件"""
15 print(f"打开文件:{self.filename}")
16 self.file = open(self.filename, self.mode)
17 return self.file
18
19 def __exit__(self, exc_type, exc_value, traceback):
20 """关闭文件"""
21 if self.file:
22 print(f"关闭文件:{self.filename}")
23 self.file.close()
24
25 if exc_type:
26 print(f"发生异常:{exc_type.__name__}: {exc_value}")
27
28 # 返回False,让异常正常传播
29 return False
30
31# 使用
32with FileManager("test.txt", "w") as f:
33 f.write("Hello, World!")
34 # raise ValueError("测试异常") # 取消注释测试
35# 输出:
36# 打开文件:test.txt
37# 关闭文件:test.txt
方法2:contextlib.contextmanager装饰器
contextlib是Python标准库,提供上下文管理器工具:
1from contextlib import contextmanager
2
3@contextmanager
4def file_manager(filename, mode):
5 """
6 使用装饰器创建上下文管理器
7 - yield之前是__enter__
8 - yield的值是as后的变量
9 - yield之后是__exit__
10 """
11 print(f"打开文件:{filename}")
12 f = open(filename, mode)
13
14 try:
15 yield f # yield的值赋给as变量
16 finally:
17 print(f"关闭文件:{filename}")
18 f.close()
19
20# 使用
21with file_manager("test.txt", "w") as f:
22 f.write("使用装饰器版本")
@contextmanager的工作原理:
yield之前:相当于__enter__,获取资源yield value:value赋给as后的变量yield之后(finally块):相当于__exit__,清理资源
4. 实用上下文管理器示例
示例1:数据库连接
1import sqlite3
2from contextlib import contextmanager
3
4@contextmanager
5def database_connection(db_name):
6 """
7 数据库连接上下文管理器
8 - 自动管理数据库连接的打开和关闭
9 - 确保连接总是被关闭,避免资源泄漏
10 """
11 conn = sqlite3.connect(db_name)
12 print(f"连接数据库:{db_name}")
13
14 try:
15 yield conn
16 finally:
17 conn.close()
18 print("关闭数据库连接")
19
20# 使用
21with database_connection("test.db") as conn:
22 cursor = conn.cursor()
23 cursor.execute("SELECT * FROM users")
24# 自动关闭连接
示例2:计时器
1import time
2from contextlib import contextmanager
3
4@contextmanager
5def timer(name):
6 """
7 代码块计时器
8 - 测量with块内代码的执行时间
9 - 常用于性能分析
10 """
11 start = time.time()
12 print(f"开始计时:{name}")
13
14 yield # 不需要返回值
15
16 end = time.time()
17 print(f"{name}执行时间:{end - start:.4f}秒")
18
19# 使用
20with timer("数据处理"):
21 # 模拟耗时操作
22 time.sleep(1)
23 result = sum(range(1000000))
24# 输出:数据处理执行时间:1.0234秒
示例3:临时目录
1import os
2import shutil
3import tempfile
4from contextlib import contextmanager
5
6@contextmanager
7def temporary_directory():
8 """
9 临时目录上下文管理器
10 - 创建临时目录
11 - 使用后自动删除
12 """
13 tmpdir = tempfile.mkdtemp()
14 print(f"创建临时目录:{tmpdir}")
15
16 try:
17 yield tmpdir
18 finally:
19 shutil.rmtree(tmpdir)
20 print("删除临时目录")
21
22# 使用
23with temporary_directory() as tmpdir:
24 # 在临时目录中工作
25 file_path = os.path.join(tmpdir, "temp.txt")
26 with open(file_path, "w") as f:
27 f.write("临时文件")
28# 临时目录自动删除
示例4:修改工作目录
1import os
2from contextlib import contextmanager
3
4@contextmanager
5def change_directory(path):
6 """
7 临时切换工作目录
8 - 进入with时切换到指定目录
9 - 退出with时恢复原目录
10 """
11 old_dir = os.getcwd()
12 os.chdir(path)
13 print(f"切换到:{path}")
14
15 try:
16 yield
17 finally:
18 os.chdir(old_dir)
19 print(f"恢复到:{old_dir}")
20
21# 使用
22with change_directory("/tmp"):
23 print(f"当前目录:{os.getcwd()}")
24 # 在/tmp中工作
25print(f"恢复后目录:{os.getcwd()}")
5. contextlib模块
contextlib提供了更多上下文管理器工具:
suppress - 抑制异常
1from contextlib import suppress
2
3# suppress用于临时忽略特定异常
4# 来自contextlib标准库
5
6# 不使用suppress
7try:
8 os.remove("nonexistent.txt")
9except FileNotFoundError:
10 pass # 忽略文件不存在的错误
11
12# 使用suppress(更清晰)
13with suppress(FileNotFoundError):
14 os.remove("nonexistent.txt")
redirect_stdout/stderr - 重定向输出
1from contextlib import redirect_stdout
2import io
3
4# 重定向标准输出
5# 用于捕获print输出
6output = io.StringIO()
7with redirect_stdout(output):
8 print("这段文字被捕获了")
9 print("不会显示在终端")
10
11captured = output.getvalue()
12print(f"捕获的内容:{captured}")
closing - 确保close()被调用
1from contextlib import closing
2from urllib.request import urlopen
3
4# closing确保对象的close()方法被调用
5# 适用于有close()方法但不是上下文管理器的对象
6with closing(urlopen("http://example.com")) as page:
7 content = page.read()
8# 自动调用page.close()
ExitStack - 动态管理多个上下文
1from contextlib import ExitStack
2
3# ExitStack允许动态地进入/退出多个上下文
4# 适用于数量不固定的资源管理
5files = ["file1.txt", "file2.txt", "file3.txt"]
6
7with ExitStack() as stack:
8 # 动态打开多个文件
9 file_objects = [
10 stack.enter_context(open(fname))
11 for fname in files
12 ]
13
14 # 使用所有文件
15 for f in file_objects:
16 content = f.read()
17# 所有文件自动关闭
6. 嵌套with语句
多个with
1# Python 2.6之前
2with open("input.txt") as infile:
3 with open("output.txt", "w") as outfile:
4 outfile.write(infile.read())
5
6# Python 2.7+:一行多个with
7with open("input.txt") as infile, open("output.txt", "w") as outfile:
8 outfile.write(infile.read())
7. 异常处理
__exit__的返回值控制异常传播:
1class ExceptionHandler:
2 def __enter__(self):
3 return self
4
5 def __exit__(self, exc_type, exc_value, traceback):
6 if exc_type is ValueError:
7 print(f"捕获ValueError:{exc_value}")
8 return True # 抑制ValueError
9
10 # 其他异常正常传播
11 return False
12
13# 使用
14with ExceptionHandler():
15 raise ValueError("这个异常会被抑制")
16 print("这行不会执行")
17print("继续执行") # 会执行
18
19with ExceptionHandler():
20 raise TypeError("这个异常会传播") # TypeError不会被抑制
21print("不会执行")
8. 最佳实践
1. 优先使用with
1# 不好
2f = open("data.txt")
3content = f.read()
4f.close()
5
6# 好
7with open("data.txt") as f:
8 content = f.read()
2. 对所有需要清理的资源使用上下文管理器
1# 文件
2with open(file) as f: pass
3
4# 锁
5with lock: pass
6
7# 数据库连接
8with get_connection() as conn: pass
9
10# 线程池
11with ThreadPoolExecutor() as executor: pass
3. 自定义资源类时实现上下文管理器
1class Resource:
2 def __init__(self):
3 self.resource = None
4
5 def __enter__(self):
6 self.resource = acquire_resource()
7 return self.resource
8
9 def __exit__(self, *args):
10 release_resource(self.resource)
9. 小结
今天我们学习了上下文管理器:
- 定义:自动管理资源生命周期的对象
- 协议:
__enter__和__exit__方法 - with语句:自动调用协议方法
- 创建方式:
- 类实现(实现__enter__/exit)
- @contextmanager装饰器(更简洁)
- contextlib模块:suppress、redirect、closing、ExitStack
- 应用场景:文件、数据库、锁、计时、临时资源
- 最佳实践:优先使用with、为资源类实现协议
上下文管理器是Python资源管理的标准做法,让代码更安全、更Pythonic。
练习题:
- 创建一个上下文管理器,在进入时打印开始时间,退出时打印结束时间
- 实现一个上下文管理器来管理数据库事务(commit/rollback)
- 使用@contextmanager创建一个临时修改全局变量的上下文管理器
本文代码示例:
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
