Skip to content

异常与上下文管理

程序运行时会遇到各种意外——文件不存在、数据格式不对、网络超时、计算溢出。异常是 Python 表示"出错了"的方式try/except 捕获异常、决定出错后怎么处理。完全不处理时,程序直接中断,剩下一堆堆栈信息;到处 except Exception 又会把真正的错误吞掉,排查时找不到线索。

上下文管理器用 with 管理资源的获取和释放——文件打开后自动关闭、锁获取后自动释放。withtry/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")

elsetry 没抛异常时执行。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("张三")  # 抛出 ValueError

raise 抛异常,调用方要么捕获、要么让异常继续往上抛。不要用异常做正常的流程控制——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,名字以 ErrorException 结尾。好处是调用方能按异常类型分别处理——ArticleAlreadyPublishedArticleNotFoundError 是两种不同的错误,处理方式可能不同。

六、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.Pathread_textwrite_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")

一行搞定读写,不用手动 openclose

七、自定义上下文管理器

想让自定义类支持 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 清理资源withtry/finally 保证清理

异常处理的黄金原则:在能处理的地方捕获,在能恢复的地方处理,在其他地方让异常自然往上抛。不要到处 except Exception 然后什么都不做——排查时看到的是"程序正常退出但结果不对",完全不知道哪里出了问题。