首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python `with` 语句和上下文管理器,到底帮你干了什么?7 个误用场景

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

原创
作者头像
用户6689525
发布于 2026-10-07 13:33:05
发布于 2026-10-07 13:33:05
60
举报

先给结论:with 语句的本质,是在代码块进入时自动调用 __enter__、退出时自动调用 __exit__,它解决的核心问题只有一个——不管中间是正常结束还是抛了异常,资源都能被正确释放。你以为它只是"打开文件更优雅的写法",其实它是一层"保证收尾"的语法糖,等价于 try...finally,但比手写 finally 可靠得多。

下面把 __enter__ / __exit__ 的运作机制拆开,再列 7 个真实误用场景,每个都给正确写法。

一、with 等价于 try...finally,但不会写错

不用 with 时,你要手动关资源:

代码语言:python
复制
f = open("data.txt", "r")
try:
    content = f.read()
finally:
    f.close()   # 异常时也必须关,忘了就文件句柄泄漏

用 with 后,Python 在离开代码块时无条件调用 __exit__,等于帮你写好了那个 finally:

代码语言:python
复制
with open("data.txt", "r") as f:
    content = f.read()

关键点:__exit__ 在异常传播之前被调用,所以即使 f.read() 抛错,文件也会被关掉。这就是 with 比手写 try...finally 更安全的根因——你不可能"忘写 finally"。

二、__exit__ 的三个参数,决定了异常会不会被吞

自定义上下文管理器时,__exit__ 的签名是:

代码语言:python
复制
def __enter__(self):
    return self

def __exit__(self, exc_type, exc_val, exc_tb):
    # exc_type 非 None 表示代码块里抛了异常
    ...

返回值有讲究:

  • 返回假值(None / False / 0):异常继续向外传播,调用方会收到这个错。
  • 返回真值(True):异常被吞掉,调用方以为一切正常。
代码语言:python
复制
class Swallow:
    def __enter__(self): return self
    def __exit__(self, et, ev, tb): return True  # 危险!吞掉所有异常

with Swallow():
    raise ValueError("出错了")
print("居然走到这里")   # 异常被静默吞掉

三、误用 1:在 __exit__ 里返回 True 吞掉异常

这是最隐蔽的 bug。你想"优雅处理",结果把关键错误吃了:

代码语言:python
复制
def __exit__(self, et, ev, tb):
    if et is not None:
        log.error("something failed: %s", ev)
        return True   # ⚠️ 异常没了,调用方毫不知情

正确做法:除非你真的要压制特定异常,否则返回 None(假值)让异常传播。需要压制也要显式判断类型:

代码语言:python
复制
def __exit__(self, et, ev, tb):
    if et is FileNotFoundError:
        return True   # 只压制这一种
    return False      # 其他异常照常抛

四、误用 2:以为 with 退出后还能用文件对象

代码语言:python
复制
with open("data.txt") as f:
    data = f.read()
print(f.read())   # ❌ ValueError: I/O operation on closed file

with 块一结束,上下文管理器已经把资源关了。变量 f 还在,但它是个已关闭的对象。需要后续用,就把使用逻辑放进 with 块里,或显式返回你需要的数据。

五、误用 3:多个 with 的嵌套顺序搞反

同时管理多个资源时,常见两种写法:

代码语言:python
复制
with open("a.txt") as fa, open("b.txt") as fb:
    ...

with open("a.txt") as fa:
    with open("b.txt") as fb:
        ...

要小心的是:先开的资源后关。如果你依赖 A 在 B 关闭前还可用,嵌套顺序就很重要。一般建议用写法 A 保持扁平,可读性更好。

六、误用 4:contextlib.contextmanager 忘了在 yield 后写收尾

用装饰器写管理器最省事,但收尾代码必须放在 yield 之后,且最好包在 try...finally 里:

代码语言:python
复制
from contextlib import contextmanager

@contextmanager
def timer():
    start = time.time()
    try:
        yield            # 这里是 with 块执行的位置
    finally:
        print(f"耗时 {time.time()-start:.2f}s")  # ✅ 收尾放 yield 后

with timer():
    do_something()

坑点:如果 yield 之后的代码不在 finally 里,一旦 with 块抛异常,收尾逻辑就不执行。务必用 try...finally 包住 yield。

七、误用 5:__enter__ 返回的不是你想要的那个对象

with ... as x 里的 x,是 __enter__ 的返回值,不一定是管理器自己:

代码语言:python
复制
class DB:
    def __enter__(self):
        self.conn = connect()
        return self.conn   # as 拿到的是连接,不是 DB 实例
    def __exit__(self, *a):
        self.conn.close()

with DB() as conn:
    conn.execute(...)   # 这里用的是连接,不是 DB

很多人误以为 as 拿到的是 DB() 实例,结果调用了不存在的方法。记住:as 绑定的是 __enter__ 的返回值。

八、一张表:手动 try...finally vs with

维度

手写 try...finally

with 上下文管理器

资源释放可靠性

依赖你记得写 finally

语言保证退出必调用 __exit__

异常时释放

写了才释放,漏写就泄漏

无条件释放

可读性

样板代码多,嵌套深

扁平清晰

多资源管理

易漏关其中一个

逗号并联一次管好几个

自定义成本

无

实现 __enter__/__exit__ 或用 @contextmanager

异常传播控制

你自己决定

由 __exit__ 返回值决定

九、记住这三条就够用了

  • 凡是涉及资源(文件、连接、锁、事务)的获取与释放,优先用 with,别手写 try...finally。
  • 自定义管理器时,__exit__ 默认返回 None 让异常传播,别随意 return True 吞异常。
  • yield 之后的收尾务必包在 try...finally 里,否则 with 块抛错时收尾不执行。

说到底,with 帮你兜底的不是"打开",而是"关闭"。它最值钱的地方,是在你最容易忘记收尾的异常路径上,替你把那扇门稳稳关上。

你平时用 with 时,有没有遇到过"明明关了文件却还是报句柄泄漏"的情况?评论区聊聊你踩过的上下文管理器坑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、with 等价于 try...finally,但不会写错
  • 二、__exit__ 的三个参数,决定了异常会不会被吞
  • 三、误用 1:在 __exit__ 里返回 True 吞掉异常
  • 四、误用 2:以为 with 退出后还能用文件对象
  • 五、误用 3:多个 with 的嵌套顺序搞反
  • 六、误用 4:contextlib.contextmanager 忘了在 yield 后写收尾
  • 七、误用 5:__enter__ 返回的不是你想要的那个对象
  • 八、一张表:手动 try...finally vs with
  • 九、记住这三条就够用了
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档