ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python `with` 语句和上下文管理器,到底帮你干了什么?7 个误用场景

Python `with` 语句和上下文管理器,到底帮你干了什么?7 个误用场景 Pythonwith语句和上下文管理器到底帮你干了什么7 个误用场景先给结论with语句的本质是在代码块进入时自动调用__enter__、退出时自动调用__exit__它解决的核心问题只有一个——不管中间是正常结束还是抛了异常资源都能被正确释放。你以为它只是打开文件更优雅的写法其实它是一层保证收尾的语法糖等价于try...finally但比手写finally可靠得多。下面把__enter__/__exit__的运作机制拆开再列 7 个真实误用场景每个都给正确写法。一、with等价于try...finally但不会写错不用with时你要手动关资源fopen(data.txt,r)try:contentf.read()finally:f.close()# 异常时也必须关忘了就文件句柄泄漏用with后Python 在离开代码块时无条件调用__exit__等于帮你写好了那个finallywithopen(data.txt,r)asf:contentf.read()# 到此 f 已自动关闭哪怕上面抛了异常关键点__exit__在异常传播之前被调用所以即使f.read()抛错文件也会被关掉。这就是with比手写try...finally更安全的根因——你不可能忘写 finally。二、__exit__的三个参数决定了异常会不会被吞自定义上下文管理器时__exit__的签名是def__enter__(self):returnselfdef__exit__(self,exc_type,exc_val,exc_tb):# exc_type 非 None 表示代码块里抛了异常...返回值有讲究返回假值None/False/0异常继续向外传播调用方会收到这个错。返回真值True异常被吞掉调用方以为一切正常。classSwallow:def__enter__(self):returnselfdef__exit__(self,et,ev,tb):returnTrue# 危险吞掉所有异常withSwallow():raiseValueError(出错了)print(居然走到这里)# 异常被静默吞掉三、误用 1在__exit__里返回True吞掉异常这是最隐蔽的 bug。你想优雅处理结果把关键错误吃了def__exit__(self,et,ev,tb):ifetisnotNone:log.error(something failed: %s,ev)returnTrue# ⚠️ 异常没了调用方毫不知情正确做法除非你真的要压制特定异常否则返回None假值让异常传播。需要压制也要显式判断类型def__exit__(self,et,ev,tb):ifetisFileNotFoundError:returnTrue# 只压制这一种returnFalse# 其他异常照常抛四、误用 2以为with退出后还能用文件对象withopen(data.txt)asf:dataf.read()print(f.read())# ❌ ValueError: I/O operation on closed filewith块一结束上下文管理器已经把资源关了。变量f还在但它是个已关闭的对象。需要后续用就把使用逻辑放进with块里或显式返回你需要的数据。五、误用 3多个with的嵌套顺序搞反同时管理多个资源时常见两种写法# 写法 A逗号并联同层级Python 2.7/3.1 支持withopen(a.txt)asfa,open(b.txt)asfb:...# 写法 B嵌套进入顺序外→内退出顺序内→外withopen(a.txt)asfa:withopen(b.txt)asfb:...要小心的是先开的资源后关。如果你依赖 A 在 B 关闭前还可用嵌套顺序就很重要。一般建议用写法 A 保持扁平可读性更好。六、误用 4contextlib.contextmanager忘了在yield后写收尾用装饰器写管理器最省事但收尾代码必须放在yield之后且最好包在try...finally里fromcontextlibimportcontextmanagercontextmanagerdeftimer():starttime.time()try:yield# 这里是 with 块执行的位置finally:print(f耗时{time.time()-start:.2f}s)# ✅ 收尾放 yield 后withtimer():do_something()坑点如果yield之后的代码不在finally里一旦with块抛异常收尾逻辑就不执行。务必用try...finally包住yield。七、误用 5__enter__返回的不是你想要的那个对象with ... as x里的x是__enter__的返回值不一定是管理器自己classDB:def__enter__(self):self.connconnect()returnself.conn# as 拿到的是连接不是 DB 实例def__exit__(self,*a):self.conn.close()withDB()asconn:conn.execute(...)# 这里用的是连接不是 DB很多人误以为as拿到的是DB()实例结果调用了不存在的方法。记住as绑定的是__enter__的返回值。八、一张表手动try...finallyvswith维度手写try...finallywith上下文管理器资源释放可靠性依赖你记得写finally语言保证退出必调用__exit__异常时释放写了才释放漏写就泄漏无条件释放可读性样板代码多嵌套深扁平清晰多资源管理易漏关其中一个逗号并联一次管好几个自定义成本无实现__enter__/__exit__或用contextmanager异常传播控制你自己决定由__exit__返回值决定九、记住这三条就够用了凡是涉及资源文件、连接、锁、事务的获取与释放优先用with别手写try...finally。自定义管理器时__exit__默认返回None让异常传播别随意return True吞异常。yield之后的收尾务必包在try...finally里否则with块抛错时收尾不执行。说到底with帮你兜底的不是打开而是关闭。它最值钱的地方是在你最容易忘记收尾的异常路径上替你把那扇门稳稳关上。你平时用with时有没有遇到过明明关了文件却还是报句柄泄漏的情况评论区聊聊你踩过的上下文管理器坑。
返回列表