清理操作
程序在操作外部资源(文件、网络连接、数据库事务、锁)时,必须确保无论操作成功还是失败,资源都能被正确释放。Python 提供了 finally 子句和 with 语句两种机制来保证清理操作的执行。
finally 子句
finally 子句是 try 语句的可选组成部分,定义了在所有情况下都必须执行的清理代码。它是 try 语句结束前执行的最后一项任务。
try:
f = open("data.txt", "r")
data = f.read()
print(data.upper())
except FileNotFoundError:
print("文件不存在")
except UnicodeDecodeError:
print("编码错误")
finally:
print("关闭文件")
f.close()
无论 try 中发生什么,finally 都会执行:
- 如果
try正常完成,finally执行后整个语句结束。 - 如果
try触发异常并被except捕获,finally在异常处理完成后执行。 - 如果
try触发异常但没有匹配的except,finally先执行,然后异常继续向上传播。 - 如果
except或else子句执行期间又触发新异常,finally执行后新异常被重新抛出。
def demo(x, y):
try:
result = x / y
except ZeroDivisionError:
print("除零错误")
return "error"
else:
print(f"结果:{result}")
return result
finally:
print("finally 执行")
print(demo(10, 2)) # 正常路径,finally 执行,返回 5.0
print(demo(10, 0)) # 捕获异常,finally 执行,返回 "error"
finally 的一个特殊行为是:如果其中包含 return、break 或 continue,未被处理的异常将不会被重新引发。同时,如果 try 和 finally 都有 return,返回值来自 finally。
def tricky():
try:
return "try"
finally:
return "finally"
print(tricky()) # finally
这种写法容易让人困惑,实际编码中应避免在 finally 中使用 return。
with 语句与上下文管理器
with 语句提供了一种更优雅的资源管理方式。它确保在代码块结束时调用对象的清理方法,即使代码块中发生了异常。
with open("data.txt", "r") as f:
for line in f:
print(line.strip())
# 离开 with 块时,f.close() 自动被调用
with 语句的工作原理依赖于上下文管理器协议:对象必须实现 __enter__() 和 __exit__() 两个方法。__enter__() 在进入 with 块时执行,其返回值绑定到 as 后的变量;__exit__() 在离开 with 块时执行,负责清理工作。
class ManagedResource:
"""模拟一个需要手动释放的资源。"""
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"[{self.name}] 资源初始化")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"[{self.name}] 资源清理")
# 返回 False 表示不吞掉异常,让它继续传播
return False
def do_work(self):
print(f"[{self.name}] 执行操作")
with ManagedResource("数据库连接") as res:
res.do_work()
# 输出:
# [数据库连接] 资源初始化
# [数据库连接] 执行操作
# [数据库连接] 资源清理
__exit__() 接收三个参数:异常类型、异常值和 Traceback。如果 with 块正常结束,这三个参数都是 None。如果发生了异常,它们描述该异常;__exit__() 可以选择处理异常并返回 True(异常被吞掉不再传播),或返回 False(异常继续传播)。
class SuppressZeroDivision:
"""一个会吞掉 ZeroDivisionError 的上下文管理器。"""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ZeroDivisionError:
print(f"已抑制:{exc_val}")
return True # 吞掉异常
return False
with SuppressZeroDivision():
print(1 / 0)
print("这行不会执行")
print("with 块之后继续执行")
contextlib 模块
contextlib 提供了便捷工具,让创建上下文管理器更简单,无需编写完整的类。
contextlib.contextmanager
用装饰器将生成器函数转换为上下文管理器。yield 之前的代码相当于 __enter__(),yield 之后的代码相当于 __exit__()。
from contextlib import contextmanager
@contextmanager
def managed_file(path, mode):
f = open(path, mode)
try:
yield f
finally:
f.close()
with managed_file("test.txt", "w") as f:
f.write("hello")
如果 yield 和 finally 之间的代码抛出异常,异常会在 yield 处被注入到生成器中,然后执行 finally 块。这与类方式的 __exit__() 行为一致。
from contextlib import contextmanager
@contextmanager
def temp_database():
print("创建临时数据库")
db = {"status": "ready"}
try:
yield db
except Exception as e:
print(f"操作失败,回滚:{e}")
db["status"] = "rolled_back"
raise
finally:
print(f"清理临时数据库,最终状态:{db['status']}")
with temp_database() as db:
print(f"使用数据库:{db}")
# raise ValueError("模拟错误")
contextlib.closing
对于本身不是上下文管理器但具有 close() 方法的对象(如某些第三方库的资源),closing() 可以将其包装为上下文管理器。
from contextlib import closing
import urllib.request
with closing(urllib.request.urlopen("https://example.com")) as response:
data = response.read()
contextlib.suppress
suppress() 创建一个上下文管理器,自动忽略指定的异常类型,让代码更简洁。
from contextlib import suppress
import os
# 传统写法
try:
os.remove("maybe_not_exist.txt")
except FileNotFoundError:
pass
# 简洁写法
with suppress(FileNotFoundError):
os.remove("maybe_not_exist.txt")
contextlib.ExitStack
当需要同时管理多个上下文管理器,且数量在运行时才能确定时,ExitStack 提供了动态进入和退出的能力。
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [stack.enter_context(open(f)) for f in files]
# 所有文件在此处同时打开
contents = [f.read() for f in handles]
# 离开 with 块时,所有文件按相反顺序自动关闭
ExitStack 还会正确处理多个上下文管理器中的异常:如果某个 __exit__() 抛出异常,后续上下文管理器仍会被清理。
资源管理实战模式
模式一:嵌套 with 语句
当需要管理多种资源时,可以嵌套使用 with 语句,或者利用 Python 3.1+ 支持的多项 with 语法:
# 嵌套写法
with open("input.txt") as src:
with open("output.txt", "w") as dst:
dst.write(src.read().upper())
# 并行写法(推荐)
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
模式二:可重入锁
from threading import RLock
lock = RLock()
with lock:
print("持有锁")
with lock: # RLock 支持同线程重入
print("再次持有锁")
print("释放内层锁")
print("释放外层锁")
模式三:数据库事务
from contextlib import contextmanager
@contextmanager
def transaction(conn):
cursor = conn.cursor()
try:
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
with transaction(db_conn) as cur:
cur.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
cur.execute("INSERT INTO logs (action) VALUES (?)", ("create_user",))
# 成功时自动 commit,异常时自动 rollback