当前位置: 首页 > news >正文

Python 上下文管理器:contextlib 与 with 的正确姿势

Python 上下文管理器:contextlib 与 with 的正确姿势

写 Python 的人几乎每天都在用with open(...) as f,但一问「with 到底帮你做了什么」「怎么给自己的类加上 with 支持」「try/finally 和 with 有啥区别」,很多人就说不清了。这篇把上下文管理器从原理到实战讲透:什么时候该用、怎么用contextlib少写一半代码、以及那些异常处理的坑。

从一段「看着没错」的代码说起

假设你要操作一个数据库连接,朴素写法是这样:

conn=get_connection()cursor=conn.cursor()cursor.execute("INSERT INTO logs VALUES (?)",(msg,))conn.commit()conn.close()# 问题:上面任何一行抛异常,这里就执行不到,连接泄漏

只要executecommit抛异常,conn.close()永远不会执行,连接就泄漏了。数据库连接池被占满后,整个服务就卡死。

有人会说:那我用 try/finally 兜住:

conn=get_connection()try:cursor=conn.cursor()cursor.execute("INSERT INTO logs VALUES (?)",(msg,))conn.commit()finally:conn.close()# 无论是否异常都会执行

这样确实对了。但如果你每次操作连接都要写这一坨 try/finally,重复且容易漏。上下文管理器就是把这套「进入时准备、退出时收尾」的模式封装起来,让调用方一行with搞定。

with 背后的协议:enterexit

with能作用于任何实现了__enter____exit__两个方法的对象。我们给数据库连接包一层:

classManagedConnection:def__enter__(self):self.conn=get_connection()returnself.conn# 这个返回值就是 as 后面拿到的东西def__exit__(self,exc_type,exc_val,exc_tb):# 无论正常结束还是抛异常,这里一定会被调用self.conn.close()# 返回 False(或不返回)表示不吞掉异常,让它继续往上抛returnFalse

用起来:

withManagedConnection()asconn:conn.cursor().execute("INSERT INTO logs VALUES (?)",(msg,))conn.commit()# 出了 with 块,conn 已经自动 close,无论中间是否抛异常

__exit__的三个参数是关键:

  • 如果 with 块内没有异常,三个参数都是None
  • 如果抛了异常,它们分别是异常类型、异常实例、traceback。
  • __exit__返回值决定异常是否被吞掉:返回True表示「我处理了,异常到此为止」;返回FalseNone表示「我只做清理,异常继续往上抛」。

⚠️ 一个高频坑:不要在__exit__里无脑return True。那样会把 with 块内的所有异常都吞掉,调用方以为一切正常,实际上数据根本没写进去。除非你明确要「屏蔽某类异常」,否则让它默认往上抛。

用 contextlib.contextmanager 少写一半代码

为一个简单场景写整个类(__enter__/__exit__)太重。contextlib提供了装饰器写法,用yield一分为二:

fromcontextlibimportcontextmanager@contextmanagerdefmanaged_connection():conn=get_connection()try:yieldconn# yield 之前是 __enter__,yield 出去的值就是 as 的目标finally:conn.close()# yield 之后是 __exit__,finally 保证异常时也执行

用法完全一样:

withmanaged_connection()asconn:conn.cursor().execute("...")conn.commit()

这里的心智模型很清晰:

  • yield之前的代码 = 进入时执行(相当于__enter__)。
  • yield出去的值=as绑定的对象。
  • yield之后的代码 = 退出时执行(相当于__exit__)。

必须用 try/finally 包住 yield:如果 with 块内抛异常,异常会在yield这一行被重新抛出,只有 finally 才能保证清理代码执行。这是这种写法最容易漏的地方。

实战:一个计时上下文管理器

工作中常需要给某段代码计时。上下文管理器写起来特别干净:

importtimefromcontextlibimportcontextmanager@contextmanagerdeftimer(label):start=time.perf_counter()# perf_counter 比 time() 精度高,适合测耗时try:yieldfinally:cost=time.perf_counter()-startprint(f"[{label}] 耗时{cost*1000:.1f}ms")# 使用withtimer("批量导入"):do_heavy_import()# 输出: [批量导入] 耗时 1234.5ms

注意这里yield后面没有值,因为计时不需要给调用方返回什么,with timer("x"):后面不用写as

contextlib 的几个实用工具

1.suppress:优雅地忽略特定异常

fromcontextlibimportsuppress# 想删一个可能不存在的文件,不想为 FileNotFoundError 写 try/exceptwithsuppress(FileNotFoundError):os.remove("tmp.lock")# 文件不存在就静默跳过,存在就删掉;其他异常照常抛

try: ... except FileNotFoundError: pass更短更直观。但别滥用,suppress范围要精确到具体异常类型,不要suppress(Exception)把什么都吞了。

2.closing:给只有 close() 没有 with 支持的对象补上

fromcontextlibimportclosingfromurllib.requestimporturlopen# urlopen 返回的对象有 close() 但老版本不支持 with,用 closing 包一下withclosing(urlopen("http://example.com"))aspage:data=page.read()# 退出时自动调用 page.close()

3.ExitStack:管理数量不定的上下文

当你要同时打开一批文件(数量运行时才知道),没法写死 N 个嵌套 with,用ExitStack:

fromcontextlibimportExitStackdefmerge_files(paths,out):withExitStack()asstack:files=[stack.enter_context(open(p))forpinpaths]# 所有文件都被登记到 stack,退出时统一按逆序关闭withopen(out,"w")asfout:forfinfiles:fout.write(f.read())# 无论中间哪个文件出错,已打开的都会被正确关闭

ExitStack帮你把「动态数量的资源」的清理管起来,避免手写一堆嵌套 with 或者漏关。

小结

  • with的本质是__enter__/__exit__协议:进入时准备资源、退出时保证清理,即使中途抛异常也一定收尾。
  • __exit__返回True会吞掉异常,别无脑这么写,默认让异常往上抛。
  • 简单场景用@contextmanager+yield,记住「yield 前是进入、yield 后是退出、必须 try/finally 包住 yield」。
  • contextlib三件套:suppress(精确忽略异常)、closing(给裸 close 对象补 with)、ExitStack(管理动态数量的资源)

一句话记忆点:凡是「用完必须收尾」的资源——文件、连接、锁、临时状态——都该交给上下文管理器,而不是靠自己记得写 close。

http://www.jsqmd.com/news/1173583/

相关文章:

  • 熔保炉天然气节能技术 - 中媒介
  • C++从零实现神经网络:手写数字识别与反向传播算法详解
  • 无缝插卡矩阵赋能商业地产全域智慧运营升级
  • 2026微博去水印教程:手机端图片视频免费去除方法
  • ChatGPT内容日历终极壁垒:如何让AI理解你的品牌语调、受众情绪周期与渠道衰减曲线(独家「声纹校准」提示工程)
  • 高效解密网易云音乐NCM文件:专业图形界面工具实战指南
  • 自动驾驶三大认知范式:端到端、VLA与世界模型演进路径
  • LV3296与PIC18F87K22在嵌入式数据采集系统中的应用
  • 遗传算法工程化实战:选择压力、适应度缩放与收敛诊断
  • 水刀增压器泛塞封 - 中媒介
  • 蓝牙5.4 LE Audio开发实战:IDC777-1模块与STM32方案解析
  • AI舞蹈视频生成实战:即梦AI Seedance2.5从提示词到参数配置全解析
  • 2026抖音去水印免费工具实测,手机电脑实用方法教程
  • 世界上第一个计算机算法:1843年洛芙莱斯注释G全解析
  • 全景视野无框切边眼镜选购 - 中媒介
  • 2026年广州留学日语哪家机构好?资深学员推荐这3家
  • 光学衍射神经网络实战指南:如何构建下一代全光计算系统
  • 武汉官方直招的正规中专新能源专业学校全面大盘点 - 湖北找学校
  • DirectX兼容层技术解决Windows 11游戏闪退问题 - WarcraftHelper架构详解
  • 2026 浙江宁波市全区域彩钢瓦修缮公司 TOP4 权威推荐|彩钢瓦翻新 / 防水 / 补漏 / 除锈喷漆优选指南 + 避坑攻略 - 本地便民网
  • 五年一线观察:贴标机定制背后的行业适配逻辑
  • Momenta携“物理AI”上市市值破700亿港元,蘑菇车联、毫末智行等境遇为何不同?
  • 使用CodeBuddy高效获取长沙市暑假中小学开放信息(第一篇)
  • 遗传算法第二部分:选择压力、模式重组与多样性维护
  • Typora 1.8.10 跨平台安装与激活:Windows/macOS/Linux 3系统实测对比
  • 武汉智能体产品对比:3个选型关键指标
  • 深度掌控AMD Ryzen:SMUDebugTool硬件调试终极指南
  • 遗传算法工程实践:从原理到工业级GA框架搭建
  • 电力项目投标文件合规性审查:7个关键章节与ISO 9001质量体系衔接
  • STM32F745VG与MCP3551高精度数据采集系统设计