三亩地 三亩地SAN MU DI · CODE DIARY
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: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 进阶四连"算是闭环了。回顾一下四篇的关系:

  1. 0814 正则:格式校验(re+ 校验码算法)
  2. 0814 装饰器:代码复用(@decorator= 高阶函数 + 闭包)
  3. 0815 生成器:内存优化(yield= 惰性求值)
  4. 0815 上下文管理器:资源管理(with=__enter__/__exit__

而这一篇是前三篇的集大成——@contextmanager用装饰器语法,包裹生成器函数,实现资源管理。你在这四篇里学的每一个知识点,都在这一篇里被串起来了。

四个主题的共同点:都是"写了很久 Python 但可能没真正理解"的基础进阶。它们解决的是四个工程化刚需——校验、复用、内存、资源。


本文为 Python 上下文管理器技术分享。@contextmanager的本质是装饰器与生成器的组合,承接本账号 0814 装饰器、0815 生成器两篇文章。4个实战场景均为工程化刚需。

← 返回列表