十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python上下文管理器实战:with语句的原理 + contextmanager装饰器

Python上下文管理器实战:with语句的原理 + contextmanager装饰器 文章目录环境信息前言你每天都在写 with open(...)但你真的懂它吗一、with 语句的原理__enter__ 和 __exit__二、contextlib.contextmanager装饰器 生成器三、实战场景1数据库连接管理四、实战场景2计时器五、实战场景3临时目录自动清理六、实战场景4屏蔽异常七、总结Python 进阶四连的闭环环境信息项目版本/说明Python3.10标准库contextlib / contextmanager / tempfile依赖无需第三方库场景文件管理 / 数据库连接 / 计时 / 临时目录前言你每天都在写with open(...)但你真的懂它吗几乎每个 Python 开发者都写过这行代码withopen(data.csv,r)asf:contentf.read()但你有没有想过——为什么with能自动关闭文件如果你用with包住一个自己写的类会怎样答案是会报错因为你的类没有实现上下文管理器协议。这篇是我Python 进阶四连的第三篇前两篇是 0814 的装饰器、0815 第一篇的生成器。写到这里我突然意识到一个特别妙的事——contextmanager装饰器本质就是装饰器 生成器的组合。前两篇学的知识点在这一篇里同时用上了。收藏提示①with 语句能自动管理资源靠的是两个魔法方法__enter__和__exit__。理解了这两个方法你就理解了 with 的全部。文末有4个开箱即用的上下文管理器。一、with 语句的原理__enter__和__exit__先看 with 语句到底做了什么。下面两段代码完全等价# 写法一with 语句withopen(data.txt,r)asf:contentf.read()# 写法二手动等价实现fopen(data.txt,r)try:contentf.read()finally:f.close()# 无论是否异常都执行关闭with 语句的本质是一个 try/finally 的语法糖——它保证无论代码块内是否抛出异常finally里的清理逻辑都会执行。而清理逻辑由谁提供由对象的两个魔法方法__enter__进入 with 块时调用返回值赋给as后面的变量__exit__离开 with 块时调用包括正常结束和异常退出看一个自定义的上下文管理器classManagedFile:一个自定义的上下文管理器管理文件打开/关闭def__init__(self,filepath,moder):self.filepathfilepath self.modemode self.fileNonedef__enter__(self):# 进入 with 块时执行返回值赋给 as 后面的变量print(f[enter] 打开文件{self.filepath})self.fileopen(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:contentf.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它把生成器函数转换成一个上下文管理器。先看写法fromcontextlibimportcontextmanagercontextmanagerdefmanaged_file(filepath,moder):用生成器实现的上下文管理器print(f[enter] 打开{filepath})fopen(filepath,mode)try:yieldf# yield 之前是 __enter__yield 之后是 __exit__finally:print(f[exit] 关闭{filepath})f.close()# 使用方式一模一样withmanaged_file(data.txt,r)asf:contentf.read()看到关键了吗contextmanager是一个装饰器它装饰的是一个生成器函数。生成器函数里yield之前的代码 __enter__yield之后的代码 __exit__yield出来的值 as后面的变量这就是我说的装饰器 生成器的组合——contextmanager用装饰器语法0814 学的包裹一个生成器函数0815 第一篇学的yield天然地把函数切成了进入和退出两半。对比一下两种写法的优缺点手写__enter__/__exit__contextmanager代码量较多少更简洁可读性需要理解协议更直观enter/exit 一目了然异常处理手动在__exit__里判断需要try/finally包裹 yield适用场景复杂的类简单的函数式场景绝大多数情况contextmanager更好用。只有需要维护复杂状态时才考虑手写类。三、实战场景1数据库连接管理上下文管理器最经典的应用——管理数据库连接保证连接一定被关闭importsqlite3fromcontextlibimportcontextmanagercontextmanagerdefdb_connection(db_path):自动管理SQLite连接进入时连接退出时关闭connsqlite3.connect(db_path)try:yieldconn conn.commit()# 正常结束才提交exceptException:conn.rollback()# 异常则回滚raisefinally:conn.close()# 无论如何都关闭# 使用withdb_connection(hk_data.db)asconn:cursorconn.cursor()cursor.execute(SELECT * FROM districts)rowscursor.fetchall()# 离开 with 块后连接自动 commit close这个模式是事务性资源管理的范本——正常提交、异常回滚、最终关闭三步缺一不可而 with 语句把它们都封装好了。四、实战场景2计时器用上下文管理器做代码块计时比手动time.time()更优雅fromcontextlibimportcontextmanagerimporttimecontextmanagerdeftimer(label):计时上下文管理器统计代码块执行时间starttime.perf_counter()try:yield# 这里不需要返回值finally:elapsedtime.perf_counter()-startprint(f[timer]{label}耗时{elapsed:.3f}秒)# 使用withtimer(数据处理):time.sleep(1.5)# 模拟数据清洗# 输出: [timer] 数据处理 耗时 1.502秒注意这里yield后面没有值——因为计时器不需要返回任何东西给as只需要进入时开始计时、退出时打印耗时。五、实战场景3临时目录自动清理处理临时文件时最怕的就是用完忘了删。用上下文管理器保证清理importtempfileimportosfromcontextlibimportcontextmanagercontextmanagerdeftemp_dir():临时目录上下文管理器用完自动删除dirpathtempfile.mkdtemp()try:yielddirpathfinally:# 递归删除整个临时目录importshutil shutil.rmtree(dirpath,ignore_errorsTrue)print(f[cleanup] 已清理临时目录{dirpath})# 使用withtemp_dir()astmp:# 在临时目录里生成中间文件output_pathos.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个实战场景均为工程化刚需。
返回列表