系列文章

Python 从入门到精通

第 18 / 36 篇

从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。

  1. 01
    Python 怎么装、怎么用
  2. 02
    变量、数字和字符串
  3. 03
    列表:把一组数据放在一起
  4. 04
    元组、集合和字典
  5. 05
    条件判断:让程序做选择
  6. 06
    循环:重复的事交给程序
  7. 07
    函数:把代码整理成可复用的块
  8. 08
    模块与包:拆分你的程序
  9. 09
    输入、输出与字符串格式化
  10. 10
    文件读写:保存程序的数据
  11. 11
    异常处理:程序出错时怎么办
  12. 12
    基础阶段练习:命令行记账本
  13. 13
    类和对象:面向对象入门
  14. 14
    继承、组合与特殊方法
  15. 15
    迭代器与生成器
  16. 16
    列表推导式与生成器表达式
  17. 17
    装饰器:给函数增加能力
  18. 18
    上下文管理器与 with正在阅读
  19. 19
    类型标注与 dataclass
  20. 20
    正则表达式:从文本中找规律
  21. 21
    日期、时间与时区
  22. 22
    日志与调试
  23. 23
    虚拟环境与依赖管理
  24. 24
    测试:让修改不再提心吊胆
  25. 25
    网络请求:用 Python 调用 API
  26. 26
    网页解析与合规采集
  27. 27
    操作 Excel、CSV 与批量文件
  28. 28
    SQLite:给程序加一个数据库
  29. 29
    数据分析入门:NumPy 与 Pandas
  30. 30
    画图:把数据变得直观
  31. 31
    Flask 入门:做一个小网站
  32. 32
    异步编程:同时处理多项任务
  33. 33
    线程、进程与并发选择
  34. 34
    性能分析与优化
  35. 35
    项目结构、配置与发布
  36. 36
    综合项目:从需求到上线

查看整个系列 →

文件那篇我们说过:打开文件要记得关闭,用 with open(...) 最省心。但 with 不止能管文件——数据库连接、网络连接、线程锁,凡是”用完必须清理”的资源,都能用 with 统一管理。这一篇讲透上下文管理器(context manager)

with 到底做了什么

先看文件的标准写法:

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
# 出了 with 块,文件自动关闭

它等价于(但更安全):

f = open("data.txt", "r", encoding="utf-8")
try:
    content = f.read()
finally:
    f.close()      # 无论如何都关闭

with 的核心价值:进入时准备资源,退出时自动清理,即使中间出了异常也保证清理。忘了 close 的文件会一直占着句柄,Windows 上还会导致”文件被占用”删不掉——with 帮你彻底消灭这类问题。

原理:__enter__ 和 __exit__

任何实现了这两个魔术方法的对象,都能用 with:

class ManagedFile:
    def __init__(self, name):
        self.name = name

    def __enter__(self):            # 进入 with 时调用
        print(f"打开 {self.name}")
        self.file = open(self.name, "w", encoding="utf-8")
        return self.file            # as 后面的变量拿到这个返回值

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 退出 with 时调用(无论正常还是异常)
        # exc_type/exc_val/exc_tb:异常信息,没异常时都是 None
        print(f"关闭 {self.name}")
        self.file.close()
        return False                # False = 异常继续往外抛;True = 吞掉

with ManagedFile("hello.txt") as f:
    f.write("南山小站n")
# 打开 hello.txt
# 关闭 hello.txt

三个要点:

  • __enter__ 的返回值给 as 变量
  • __exit__ 无论异常与否都会执行,参数里带着异常信息
  • __exit__ 返回 True 表示”异常我处理了,别抛了”;返回 False(默认)异常继续冒泡

更简单的写法:contextlib.contextmanager

手写类有点重,Python 提供了装饰器版——用生成器写上下文管理器,yield 前面是进入逻辑,yield 后面是清理逻辑

from contextlib import contextmanager

@contextmanager
def managed_file(name):
    print(f"打开 {name}")
    f = open(name, "w", encoding="utf-8")
    try:
        yield f                    # yield 的值给 as 变量
    finally:
        print(f"关闭 {name}")
        f.close()                  # 无论异常与否都执行

with managed_file("hello.txt") as f:
    f.write("南山小站n")

比类写法直观多了。记口诀:yield 前面”入场”,yield 后面”退场”,退场逻辑放 finally 里保证执行。

实战:临时改配置、计时

from contextlib import contextmanager
import time

# 1. 计时器:with 块花了多久
@contextmanager
def timer(name):
    start = time.time()
    try:
        yield
    finally:
        print(f"{name} 耗时 {time.time() - start:.3f} 秒")

with timer("批量导入"):
    time.sleep(0.3)
    print("导入完成")
# 导入完成
# 批量导入 耗时 0.300 秒

# 2. 临时改全局设置,用完还原
import sys

@contextmanager
def quiet():
    old = sys.stdout
    import io
    sys.stdout = io.StringIO()      # 静音
    try:
        yield
    finally:
        sys.stdout = old            # 还原

with quiet():
    print("这句话不会显示")
print("这句话正常显示")

“临时改一个东西,用完还原”是上下文管理器的经典场景——数据库事务、改环境变量、改当前目录,都这么干。

标准库里的现成货

很多标准库对象天生支持 with:

# 文件
with open(...) as f: ...

# 线程锁(并发那篇会细讲)
import threading
lock = threading.Lock()
with lock:                    # 自动 acquire / release
    ...

# 临时目录(用完自动删除)
import tempfile
with tempfile.TemporaryDirectory() as tmp:
    print(tmp)                # /tmp/tmpXXXX
# 目录已自动清理

# 忽略指定异常(contextlib 提供)
from contextlib import suppress
with suppress(FileNotFoundError):
    # 文件不存在也不报错,静默跳过
    open("不存在的文件.txt", "r")

suppress 特别实用:想忽略某个异常又不想写 try/except 空块,一行搞定。

新手坑

坑 1:contextmanager 装饰的函数里忘写 try/finally。yield 后面直接写清理代码的话,中途抛异常清理就不执行了。一定要 finally。

坑 2:__exit__ 返回 True 吞异常。除非你明确知道在干什么,否则别返回 True,把异常留给上层处理。

坑 3:with 多个资源。可以写 with A() as a, B() as b:,但别写太长,可读性差;嵌套 with 更清晰。

小结

  • with = 进入准备资源,退出自动清理,异常也保证清理
  • 类写法:__enter__(返回值给 as)+ __exit__(清理 + 异常处理)
  • 推荐 @contextmanager:yield 前入场、yield 后(finally 里)退场
  • 经典场景:文件、锁、事务、临时改配置、计时
  • contextlib.suppress 优雅忽略指定异常

练习

  1. @contextmanager 写一个 chdir(path):进入 with 时切换目录,退出时切回原目录(提示:os.getcwd / os.chdir)
  2. 写一个 @contextmanager 的数据库连接模拟:进入时打印”连接数据库”,退出时打印”断开连接”
  3. 思考:如果 with 块里抛了异常,__exit__ 的三个异常参数分别是什么?返回 True 和不返回(None/False)有什么区别?