Skip to content

06 异常处理

程序运行时总会出错:文件不存在、网络断开、用户输入了非法数据。如果不处理,程序直接崩溃。try/except让你捕获并处理异常,raise让你主动抛出异常。

一、try/except

1.1 基本用法

python
>>> while True:
...     try:
...         x = int(input("请输入一个数字: "))
...         break
...     except ValueError:
...         print("无效数字,请重试!")

try里的代码如果发生ValueError,就跳到except执行,不会崩溃。

1.2 捕获多种异常

python
>>> try:
...     # 可能出错的代码
...     result = 1 / 0
... except ZeroDivisionError:
...     print("除以零")
... except (ValueError, TypeError):
...     print("值错误或类型错误")

一个try可以跟多个except,分别处理不同类型的异常。也可以用元组捕获多种异常。

1.3 捕获异常信息

python
>>> try:
...     result = 1 / 0
... except ZeroDivisionError as e:
...     print(f"错误: {e}")
...
错误: division by zero

二、else子句

elsetry没有发生异常时执行:

python
>>> try:
...     f = open('workfile', encoding='utf-8')
... except OSError:
...     print("无法打开文件")
... else:
...     content = f.read()
...     f.close()
...     print(f"文件有{len(content)}个字符")

else的好处是:只把可能出错的代码放在try里,正常逻辑放在else里,避免意外捕获不需要的异常。

三、finally子句

finally不管有没有异常都会执行,通常用于清理资源:

python
>>> try:
...     result = 1 / 0
... except ZeroDivisionError:
...     print("除以零")
... finally:
...     print("这段总是会执行")

完整的try语句:tryexceptelsefinally

四、raise抛出异常

4.1 基本用法

python
>>> def divide(a, b):
...     if b == 0:
...         raise ValueError("除数不能为零")
...     return a / b
...
>>> divide(1, 0)
ValueError: 除数不能为零

4.2 重新抛出

python
>>> try:
...     result = 1 / 0
... except ZeroDivisionError:
...     print("记录错误日志")
...     raise   # 重新抛出当前异常

raise不带参数会重新抛出当前正在处理的异常。

4.3 异常链

python
>>> def func():
...     try:
...         result = 1 / 0
...     except ZeroDivisionError as e:
...         raise RuntimeError("计算失败") from e
...
>>> func()
Traceback:
  ...
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
  ...
RuntimeError: 计算失败

from指定原始异常,形成异常链,方便调试。

五、自定义异常

5.1 定义异常类

python
>>> class MyError(Exception):
...     pass
...
>>> raise MyError("出错了")
MyError: 出错了

异常类通常继承Exception,可以添加属性:

python
>>> class ValidationError(Exception):
...     def __init__(self, field, message):
...         self.field = field
...         self.message = message
...         super().__init__(f"{field}: {message}")
...
>>> raise ValidationError("email", "格式不正确")
ValidationError: email: 格式不正确

5.2 异常层次

BaseException
 +-- KeyboardInterrupt
 +-- SystemExit
 +-- Exception
      +-- ValueError
      +-- TypeError
      +-- RuntimeError
      +-- OSError
      |    +-- FileNotFoundError
      |    +-- PermissionError
      +-- 自定义异常

自定义异常应该继承Exception,不要继承BaseException

六、ExceptionGroup(3.11+)

6.1 基本概念

ExceptionGroup可以同时抛出多个异常:

python
>>> def collect_errors():
...     errors = []
...     try:
...         int("abc")
...     except ValueError as e:
...         errors.append(e)
...     try:
...         1 / 0
...     except ZeroDivisionError as e:
...         errors.append(e)
...     if errors:
...         raise ExceptionGroup("多个错误", errors)
...
>>> collect_errors()
  + Exception Group Traceback (most recent call last):
  |   ...
  | ExceptionGroup: 多个错误 (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | ValueError: invalid literal for int() with base 10: 'abc'
    +---------------- 2 ----------------
    | ZeroDivisionError: division by zero
    +------------------------------------

6.2 except*匹配

except*匹配ExceptionGroup中的特定异常:

python
>>> try:
...     raise ExceptionGroup("错误", [
...         ValueError("无效值"),
...         TypeError("类型错误"),
...         FileNotFoundError("文件不存在"),
...     ])
... except* ValueError as eg:
...     print(f"值错误: {eg.exceptions}")
... except* (TypeError, FileNotFoundError) as eg:
...     print(f"其他错误: {eg.exceptions}")

except*可以匹配多次,每次处理一个子组。

七、实用模式

7.1 忽略异常

python
>>> try:
...     value = config["key"]
... except KeyError:
...     value = default_value

>>> # 或者用dict.get(),但try/except更通用

7.2 清理资源

python
>>> f = open('workfile', 'w')
>>> try:
...     f.write('data')
...     # ... 更多操作
... finally:
...     f.close()   # 无论是否出错都关闭

或者更推荐用with

python
>>> with open('workfile', 'w') as f:
...     f.write('data')

7.3 断言(assert)

python
>>> def calculate_average(numbers):
...     assert len(numbers) > 0, "列表不能为空"
...     return sum(numbers) / len(numbers)

assert用于开发时检查不变量,不要用于处理用户输入。-O参数会禁用assert。

八、总结

语句用途
try/except捕获异常
try/except/else没异常时执行else
try/finally总是执行清理代码
raise抛出异常
raise ... from ...异常链
ExceptionGroup同时抛出多个异常(3.11+)
except*匹配异常组(3.11+)
assert开发时检查条件

with语句是处理文件等资源的最佳方式,比try/finally更简洁。自定义异常继承Exception,不要继承BaseException