Appearance
异常与上下文管理
程序运行时会遇到各种意外——文件不存在、数据格式不对、网络超时、计算溢出。异常是 Python 表示"出错了"的方式,try/except 捕获异常、决定出错后怎么处理。完全不处理时,程序直接中断,剩下一堆堆栈信息;到处 except Exception 又会把真正的错误吞掉,排查时找不到线索。
上下文管理器用 with 管理资源的获取和释放——文件打开后自动关闭、锁获取后自动释放。with 比 try/finally 更简洁,自定义类也能支持。
一、异常的基本结构
python
try:
# 可能出错的代码
value = int("abc")
except ValueError as exc:
# 出错时的处理
print(f"转换失败:{exc}")try 里放可能出错的代码,except 捕获特定类型的异常。ValueError 表示"值不对",int("abc") 字符串无法转整数就会抛这个异常。
常见异常类型:
| 类型 | 触发场景 |
|---|---|
ValueError | 值不合法(int("abc")) |
TypeError | 类型不对(len(123)) |
KeyError | 字典键不存在(d["missing"]) |
IndexError | 列表下标越界(lst[100]) |
FileNotFoundError | 文件不存在 |
ZeroDivisionError | 除零(1 / 0) |
AttributeError | 属性不存在(obj.missing_attr) |
二、多个 except 和 else
可以捕获多种异常,分别处理:
python
from pathlib import Path
def read_article(file_path):
"""读取文章内容,返回第一行。"""
try:
content = Path(file_path).read_text(encoding="utf-8")
first_line = content.splitlines()[0]
return first_line
except FileNotFoundError:
print(f"文件不存在:{file_path}")
return None
except IndexError:
print("文件是空的,没有第一行")
return None
else:
# 没有异常时执行
print("读取成功")
return first_line
result = read_article("notes/article.txt")else 在 try 没抛异常时执行。else 比"把成功逻辑写在 try 里"更清晰——一眼能看出哪些代码可能出错、哪些是成功后的处理。
三、finally 子句
finally 里的代码一定会执行,常用于清理资源:
python
file = open("notes/article.txt", encoding="utf-8")
try:
content = file.read()
print(content[:100])
finally:
file.close() # 不管前面有没有异常,都关闭文件即使 try 里出了异常、没被 except 捕获,finally 还是会执行——程序中断前先跑完 finally。
四、raise 抛出异常
函数里遇到不该继续的情况,可以主动抛异常:
python
def validate_email(email):
"""校验邮箱格式,不合格时抛异常。"""
if "@" not in email:
raise ValueError(f"邮箱格式不合法:{email}")
validate_email("张三") # 抛出 ValueErrorraise 抛异常,调用方要么捕获、要么让异常继续往上抛。不要用异常做正常的流程控制——if 才是判断分支用的,异常是"出错了"才用的。
五、自定义异常
内置异常类型不够用时,可以自定义:
python
class ArticleNotFoundError(Exception):
"""文章不存在。"""
pass
class ArticleAlreadyPublished(Exception):
"""文章已经发布过了。"""
pass
def publish_article(article):
"""发布文章,异常情况抛自定义异常。"""
if article.status == "已发布":
raise ArticleAlreadyPublished(f"《{article.title}》已经发布")
if not article.content:
raise ArticleNotFoundError(f"《{article.title}》内容为空,无法发布")
article.status = "已发布"
# 调用方捕获自定义异常
try:
publish_article(article)
except ArticleAlreadyPublished as exc:
print(f"重复发布:{exc}")
except ArticleNotFoundError as exc:
print(f"内容缺失:{exc}")自定义异常继承 Exception,名字以 Error 或 Exception 结尾。好处是调用方能按异常类型分别处理——ArticleAlreadyPublished 和 ArticleNotFoundError 是两种不同的错误,处理方式可能不同。
六、with 语句
文件、锁、数据库连接这类资源,用完要释放。with 能保证资源自动清理:
python
from pathlib import Path
# 传统写法
file = open("notes/article.txt", encoding="utf-8")
try:
content = file.read()
finally:
file.close()
# with 写法(更简洁)
with open("notes/article.txt", encoding="utf-8") as file:
content = file.read()
# 出了 with 块,文件自动关闭with 等价于 try/finally,但不用显式写 close。出了 with 块,资源一定被释放——不管有没有异常。
pathlib.Path 的 read_text 和 write_text 内部已经用 with,更省事:
python
from pathlib import Path
content = Path("notes/article.txt").read_text(encoding="utf-8")
Path("notes/article_backup.txt").write_text(content, encoding="utf-8")一行搞定读写,不用手动 open 和 close。
七、自定义上下文管理器
想让自定义类支持 with,要实现 __enter__ 和 __exit__ 两个方法:
python
class Timer:
"""计时器——进入 with 时记录开始时间,退出时打印耗时。"""
def __enter__(self):
import time
self.start = time.time()
return self # 返回的对象赋给 as 后面的变量
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.time() - self.start
print(f"耗时 {elapsed:.2f} 秒")
# 返回 True 表示异常被处理,不再往上抛
# 返回 False 或不返回,异常继续往上抛
return False
with Timer() as timer:
# 执行一些耗时操作
total = sum(range(1000000))
print("计算完成")输出:
text
耗时 0.05 秒
计算完成__enter__ 在进入 with 块时执行,返回值赋给 as 后面的变量;__exit__ 在退出 with 块时执行,能拿到异常信息(如果有)。
__exit__ 返回 True 会吞掉异常——如果 with 块里出了异常,__exit__ 返回 True 后异常就不往上抛了。多数情况返回 False,让异常继续传播。
八、异常处理的常见误区
| 误区 | 正确做法 |
|---|---|
except Exception: 捕获所有异常 | 指定具体类型,或只捕获能处理的几种 |
except: pass 吞掉异常不处理 | 至少打印异常信息或记录日志 |
| 用异常做正常流程控制 | 用 if 判断,异常只在"出错了"时用 |
| 每层都捕获再抛新异常 | 让异常自然往上抛,在最外层集中处理 |
不写 finally 清理资源 | 用 with 或 try/finally 保证清理 |
异常处理的黄金原则:在能处理的地方捕获,在能恢复的地方处理,在其他地方让异常自然往上抛。不要到处 except Exception 然后什么都不做——排查时看到的是"程序正常退出但结果不对",完全不知道哪里出了问题。