Python 从入门到精通
第 18 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with正在阅读 -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
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优雅忽略指定异常
练习
- 用
@contextmanager写一个chdir(path):进入 with 时切换目录,退出时切回原目录(提示:os.getcwd / os.chdir) - 写一个
@contextmanager的数据库连接模拟:进入时打印”连接数据库”,退出时打印”断开连接” - 思考:如果 with 块里抛了异常,
__exit__的三个异常参数分别是什么?返回 True 和不返回(None/False)有什么区别?