文章目录
- 环境信息
- 前言:你每天都在写 `with open(...)`,但你真的懂它吗
- 一、with 语句的原理:`__enter__` 和 `__exit__`
- 二、contextlib.contextmanager:装饰器 + 生成器
- 三、实战场景1:数据库连接管理
- 四、实战场景2:计时器
- 五、实战场景3:临时目录自动清理
- 六、实战场景4:屏蔽异常
- 七、总结:Python 进阶四连的闭环
环境信息
| 项目 | 版本/说明 |
|---|---|
| Python | 3.10+ |
| 标准库 | contextlib / contextmanager / tempfile |
| 依赖 | 无需第三方库 |
| 场景 | 文件管理 / 数据库连接 / 计时 / 临时目录 |
前言:你每天都在写with open(...),但你真的懂它吗
几乎每个 Python 开发者都写过这行代码:
withopen('data.csv','r')asf:content=f.read()但你有没有想过——为什么with能自动关闭文件?如果你用with包住一个自己写的类,会怎样?答案是:会报错,因为你的类没有实现"上下文管理器协议"。
这篇是我"Python 进阶四连"的第三篇(前两篇是 0814 的装饰器、0815 第一篇的生成器)。写到这里我突然意识到一个特别妙的事——contextmanager装饰器,本质就是"装饰器 + 生成器"的组合。前两篇学的知识点,在这一篇里同时用上了。
收藏提示①:with 语句能自动管理资源,靠的是两个魔法方法
__enter__和__exit__。理解了这两个方法,你就理解了 with 的全部。文末有4个开箱即用的上下文管理器。
一、with 语句的原理:__enter__和__exit__
先看 with 语句到底做了什么。下面两段代码完全等价:
# 写法一:with 语句withopen('data.txt','r')asf:content=f.read()# 写法二:手动等价实现f=open('data.txt','r')try:content=f.read()finally:f.close()# 无论是否异常,都执行关闭with 语句的本质是一个 try/finally 的语法糖——它保证无论代码块内是否抛出异常,finally里的清理逻辑都会执行。
而"清理逻辑"由谁提供?由对象的两个魔法方法:
__enter__:进入 with 块时调用,返回值赋给as后面的变量__exit__:离开 with 块时调用(包括正常结束和异常退出)
看一个自定义的上下文管理器:
classManagedFile:"""一个自定义的上下文管理器,管理文件打开/关闭"""def__init__(self,filepath,mode='r'):self.filepath=filepath self.mode=mode self.file=Nonedef__enter__(self):# 进入 with 块时执行,返回值赋给 as 后面的变量print(f"[enter] 打开文件{self.filepath}")self.file=open(self.filepath,self.mode)returnself.file# 这个返回值就是 `as f` 的 fdef__exit__(self,exc_type,exc_val,exc_tb):# 离开 with 块时执行,负责清理print(f"[exit] 关闭文件{self.filepath}")ifself.file:self.file.close()# 返回 False 表示不吞掉异常(异常继续向外传播)returnFalse# 使用withManagedFile('data.txt','r')asf:content=f.read()# 输出:# [enter] 打开文件 data.txt# [exit] 关闭文件 data.txt__exit__的三个参数分别是异常类型、异常值、异常回溯。如果代码块内抛出了异常,__exit__仍会被调用(这正是它比手动 try/finally 优雅的地方)。如果__exit__返回True,异常会被"吞掉";返回False(或 None),异常继续传播。
收藏提示②:
__exit__返回 True 会吞掉异常,返回 False 会继续抛出。大多数情况应该返回 False——除非你明确知道要处理这个异常(比如某些库用返回 True 来实现"抑制特定异常")。
二、contextlib.contextmanager:装饰器 + 生成器
手写__enter__/__exit__有点繁琐。Python 标准库提供了更优雅的方式——contextlib.contextmanager,它把生成器函数转换成一个上下文管理器。
先看写法:
fromcontextlibimportcontextmanager@contextmanagerdefmanaged_file(filepath,mode='r'):"""用生成器实现的上下文管理器"""print(f"[enter] 打开{filepath}")f=open(filepath,mode)try:yieldf# yield 之前是 __enter__,yield 之后是 __exit__finally:print(f"[exit] 关闭{filepath}")f.close()# 使用方式一模一样withmanaged_file('data.txt','r')asf:content=f.read()看到关键了吗?@contextmanager是一个装饰器,它装饰的是一个生成器函数。生成器函数里:
yield之前的代码 =__enter__yield之后的代码 =__exit__yield出来的值 =as后面的变量
这就是我说的"装饰器 + 生成器"的组合——@contextmanager用装饰器语法(0814 学的),包裹一个生成器函数(0815 第一篇学的),yield天然地把函数切成了"进入"和"退出"两半。
对比一下两种写法的优缺点:
手写__enter__/__exit__ | @contextmanager | |
|---|---|---|
| 代码量 | 较多 | 少,更简洁 |
| 可读性 | 需要理解协议 | 更直观(enter/exit 一目了然) |
| 异常处理 | 手动在__exit__里判断 | 需要try/finally包裹 yield |
| 适用场景 | 复杂的类 | 简单的函数式场景 |
绝大多数情况,@contextmanager更好用。只有需要维护复杂状态时,才考虑手写类。
三、实战场景1:数据库连接管理
上下文管理器最经典的应用——管理数据库连接,保证连接一定被关闭:
importsqlite3fromcontextlibimportcontextmanager@contextmanagerdefdb_connection(db_path):"""自动管理SQLite连接:进入时连接,退出时关闭"""conn=sqlite3.connect(db_path)try:yieldconn conn.commit()# 正常结束才提交exceptException:conn.rollback()# 异常则回滚raisefinally:conn.close()# 无论如何都关闭# 使用withdb_connection('hk_data.db')asconn:cursor=conn.cursor()cursor.execute("SELECT * FROM districts")rows=cursor.fetchall()# 离开 with 块后,连接自动 commit + close这个模式是"事务性资源管理"的范本——正常提交、异常回滚、最终关闭,三步缺一不可,而 with 语句把它们都封装好了。
四、实战场景2:计时器
用上下文管理器做代码块计时,比手动time.time()更优雅:
fromcontextlibimportcontextmanagerimporttime@contextmanagerdeftimer(label):"""计时上下文管理器:统计代码块执行时间"""start=time.perf_counter()try:yield# 这里不需要返回值finally:elapsed=time.perf_counter()-startprint(f"[timer]{label}耗时{elapsed:.3f}秒")# 使用withtimer("数据处理"):time.sleep(1.5)# 模拟数据清洗# 输出: [timer] 数据处理 耗时 1.502秒注意这里yield后面没有值——因为计时器不需要返回任何东西给as,只需要"进入时开始计时、退出时打印耗时"。
五、实战场景3:临时目录自动清理
处理临时文件时,最怕的就是"用完忘了删"。用上下文管理器保证清理:
importtempfileimportosfromcontextlibimportcontextmanager@contextmanagerdeftemp_dir():"""临时目录上下文管理器:用完自动删除"""dirpath=tempfile.mkdtemp()try:yielddirpathfinally:# 递归删除整个临时目录importshutil shutil.rmtree(dirpath,ignore_errors=True)print(f"[cleanup] 已清理临时目录{dirpath}")# 使用withtemp_dir()astmp:# 在临时目录里生成中间文件output_path=os.path.join(tmp,'result.csv')# ... 处理 ...# 离开 with 后,临时目录自动删除六、实战场景4:屏蔽异常
__exit__返回 True 可以吞掉异常。这个特性可以用来实现"忽略特定错误":
fromcontextlibimportcontextmanager,suppress# 方式一:contextmanager 手动吞异常@contextmanagerdefignore_file_not_found():try:yieldexceptFileNotFoundError:pass# 忽略文件不存在的错误# 方式二:标准库自带的 suppress(更简洁)fromcontextlibimportsuppresswithsuppress(FileNotFoundError):os.remove('不存在的文件.txt')# 文件不存在也不会报错收藏提示③:如果想"忽略特定异常",标准库
contextlib.suppress是现成的——with suppress(FileNotFoundError): ...一行搞定,不用自己写 try/except。但注意:不要滥用 suppress,吞掉异常会掩盖真实 bug。只在明确知道"这个错误可以忽略"时才用。
七、总结:Python 进阶四连的闭环
写到这里,我这个"Python 进阶四连"算是闭环了。回顾一下四篇的关系:
- 0814 正则:格式校验(
re+ 校验码算法) - 0814 装饰器:代码复用(
@decorator= 高阶函数 + 闭包) - 0815 生成器:内存优化(
yield= 惰性求值) - 0815 上下文管理器:资源管理(
with=__enter__/__exit__)
而这一篇是前三篇的集大成——@contextmanager用装饰器语法,包裹生成器函数,实现资源管理。你在这四篇里学的每一个知识点,都在这一篇里被串起来了。
四个主题的共同点:都是"写了很久 Python 但可能没真正理解"的基础进阶。它们解决的是四个工程化刚需——校验、复用、内存、资源。
本文为 Python 上下文管理器技术分享。@contextmanager的本质是装饰器与生成器的组合,承接本账号 0814 装饰器、0815 生成器两篇文章。4个实战场景均为工程化刚需。