拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用好 Python 上下文管理器:contextlib 让你少写一半 with 代码

用好 Python 上下文管理器contextlib 让你少写一半 with 代码with语句几乎每个 Python 程序员每天都在用打开文件、加锁、连接数据库。但很多人只停留在with open(...)这个层面一旦需要自己封装资源或复用一段进入/退出逻辑就手写__enter__和__exit__甚至干脆放弃用with、裸写try/finally。这篇文章讲清contextlib标准库里的几个实用工具让你把资源管理代码写得更短、更安全。一、先明确一个事实with 不等于 openwith语句本质上是协议不是文件专用语法。一个对象只要实现了__enter__和__exit__或者被contextlib包装就能用with管理。理解这一点后很多场景立刻开阔起来withopen(data.txt,r,encodingutf-8)asf:textf.read()上面这行代码解释器实际执行的是fopen(data.txt,r,encodingutf-8)try:textf.read()finally:f.close()with的价值在于无论with块里正常结束还是抛异常退出逻辑close都会被保证执行。手写try/finally也能做到但容易漏、容易忘with让这件事变成强制。二、场景 1同一个进入/退出逻辑要复用假设你在多个地方需要临时修改环境变量用完再改回来。最朴素的写法是importos oldos.environ.get(MY_VAR)os.environ[MY_VAR]temporarytry:do_something()finally:ifoldisNone:os.environ.pop(MY_VAR,None)else:os.environ[MY_VAR]old这段代码一旦要在两三个函数里重复就很烦。用contextmanager装饰器可以把它封装成可复用的上下文管理器fromcontextlibimportcontextmanagerimportoscontextmanagerdeftemp_env_var(name:str,value:str):临时设置环境变量退出时恢复原值。oldos.environ.get(name)os.environ[name]valuetry:yieldfinally:ifoldisNone:os.environ.pop(name,None)else:os.environ[name]old用法withtemp_env_var(MY_VAR,temporary):print(os.environ[MY_VAR])# temporaryprint(os.environ.get(MY_VAR))# 恢复到原来的值或不存在关键点有两个yield前面是进入逻辑后面是退出逻辑。finally保证退出逻辑一定执行——即使with块里抛了异常yield之后的finally也会跑。陷阱提醒contextmanager装饰的函数里try一定要把yield包住否则with块里的异常不会触发你的清理逻辑。三、场景 2临时换工作目录chdir是全局生效的容易让后续代码不知道自己身在哪个目录。用上下文管理器把目录切换限制在一个作用域内是常见且实用的做法fromcontextlibimportcontextmanagerimportoscontextmanagerdefworking_directory(path:str):临时切换到指定目录退出时切回。oldos.getcwd()os.chdir(path)try:yieldfinally:os.chdir(old)这样切目录这件事就变成了和文件一样清晰的作用域withworking_directory(/tmp):print(当前目录,os.getcwd())print(回到,os.getcwd())这个模式在构建脚本、测试隔离里特别常用。四、场景 3多个上下文管理器一起用有些资源是嵌套的比如同时打开两个文件做合并或者同时持有两把锁。可以并列写withopen(a.txt)asfa,open(b.txt)asfb:text_afa.read()text_bfb.read()也可以配合ExitStack动态地管理运行时才知道数量的上下文管理器数组。ExitStack是contextlib里的一个进阶工具能把你 push 进去的上下文管理器统一退出fromcontextlibimportExitStack files[a.txt,b.txt,c.txt]withExitStack()asstack:handles[stack.enter_context(open(f))forfinfiles]forhinhandles:print(h.readline().rstrip())# 退出 with 后所有文件句柄都会被自动 closeExitStack尤其适合上下文管理器的数量和内容在运行时才确定的场景比如动态加载一批插件、批量建立数据库连接等。五、场景 4关闭一个本不是上下文管理器的对象closing能把一个只提供close()方法、却没有实现上下文管理器协议的对象包装成可用with的对象fromcontextlibimportclosingimportsqlite3 connsqlite3.connect(:memory:)withclosing(conn):cursorconn.cursor()cursor.execute(CREATE TABLE t (id INTEGER))# 退出后 conn 已经 close注意sqlite3.Connection本身也支持with但它的with并不会自动关闭连接只是提交/回滚事务。所以如果你确实想用完就关closing是更明确的表达。这一点容易踩坑值得单独记住。六、什么时候不该用 contextmanagercontextmanager适合做一次性的进入/退出。如果你需要一个可复用、可迭代、带状态的上下文对象比如可以多次进入、内部维护复杂状态那还是老老实实写类、实现__enter__和__exit__更清晰。contextmanager是轻量便捷不是万能替代。小结with是通用协议不是open专用核心价值是保证退出逻辑执行。contextmanager把进入/退出封装成可复用函数yield前后分别对应两者try必须包住yield。ExitStack适合运行时动态管理的多上下文closing适合包装只有close()的对象。需要复杂状态管理时回到类 __enter__/__exit__。延伸阅读官方文档 contextlib — Utilities for with-statement contexts以及 PEP 343 了解with语句的设计背景。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门