# Python 异常：try、except、else、finally、raise 以及自定义异常

URL: https://skillaria.top/zh/guides/python/isklyucheniya-python

Python 中的异常是表示错误的对象，它会中断正常的执行流程，直到被 try/except 块捕获。else 在没有出现错误时执行，finally 总是执行。用 raise 手动抛出异常，例如 raise ValueError("金额为负数")；自定义异常则是继承自 Exception 的类。

Python 中的异常是一个表示错误的对象，它会中断程序的正常执行。如果没有任何代码捕获它，Python 会打印 traceback 并结束运行。异常用 `try/except` 结构捕获，用 `raise` 语句主动抛出。下面讲解它的工作原理、最常见的错误类型，以及如何避免把错误处理变成隐藏错误。

## Python 中的异常是什么

当某个操作无法完成时——除以零、访问不存在的键、把文字转换为数字——Python 会创建一个异常对象并“抛出”。当前函数的执行随即停止，异常沿调用链向上传播，直到遇到合适的处理器。如果没有找到，程序就会崩溃。

```
>>> 10 / 0
Traceback (most recent call last):
  File "", line 1, in
ZeroDivisionError: division by zero
```

顺便回答一个常见问题：在 Python 中除以零会引发 `ZeroDivisionError` 异常。无论是 `/`、`//` 还是 `%`，都是如此。

## 如何阅读 traceback

Traceback 是程序一步步走到错误处的报告。从下往上阅读更方便。最后一行最重要：异常类型和错误信息。再往上是出错的代码行，再往上是调用它的代码，直到入口点。

```
Traceback (most recent call last):
  File "tracker.py", line 9, in
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: '一百'
```

从 Python 3.11 开始，代码行下方会画出 `~~~^^^` 标记（PEP 657），指出该行中具体出错的表达式。当一行中连续调用了多个函数时，这尤其有用。

### “Did you mean” 提示

从 Python 3.10 起，名字拼写错误时，错误信息会建议一个相近的名字。在 Python 3.15 中，这些提示变得更加智能：解释器能识别其他语言中的习惯写法。调用 `list.push` 会提示 `append`，调用 `"咖啡".toUpperCase()` 会提示 `upper` 方法。提示还会深入到嵌套的属性中：如果字典保存在对象的 `totals` 属性里，而你直接对对象调用 `items()`，提示会建议 `.totals.items`。`delattr()` 也会建议相近的名字。

## Python 中的异常类型

内置异常有数十种，但刚开始时最常见的是下面这些：

| 异常 | 何时出现 | 示例 |
| --- | --- | --- |

| `NameError` | 名字未定义，通常是拼写错误 | `print(totl)` |

| `TypeError` | 操作不适用于该类型 | `"5" + 3` |

| `ValueError` | 类型正确，但值不正确 | `int("一百")` |

| `KeyError` | 字典中没有这个键 | `d["missing"]` |

| `IndexError` | 索引超出列表范围 | `[1, 2][5]` |

| `AttributeError` | 对象没有该方法或字段 | `[].push(1)` |

| `ZeroDivisionError` | 除以零 | `1 / 0` |

| `FileNotFoundError` | 文件未找到 | `open("nope.txt")` |

需要单独说明的是 `SyntaxError`（及其特例 `IndentationError`）：它在解析文件时就会出现，早于代码运行，因此无法在同一文件中用 `try` 捕获。

### 异常的层次结构

异常是类，它们组成一个层次结构。顶层是 `BaseException`。`Exception`（几乎所有“普通”错误）以及若干系统级异常都继承自它：`KeyboardInterrupt`、`SystemExit`、`GeneratorExit`。

```
BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError
```

层次结构在捕获时很重要：`except LookupError` 会同时捕获 `KeyError` 和 `IndexError`。而 `KeyboardInterrupt` 并不是代码中的错误：它表示用户按下了 Ctrl+C。它特意不继承自 `Exception`，这样 `except Exception` 就不会阻止程序被中断。

## 异常处理：try/except/else/finally

可能出错的代码放在 `try` 块中。在 `except` 块中指定错误类型，以及如何处理它。正是 `except` 块在 Python 中负责处理异常。

```
def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"跳过该行：{e}")
        return None
    else:
        return value          # 仅在没有错误时执行
    finally:
        print("解析完成")      # 总是执行

print(parse_amount("250"))
```

这里的输出顺序是：先是“解析完成”，然后是 `250.0`。`else` 块中的 `return` 会先记住返回值，然后 Python 一定会执行 `finally`，之后才真正返回结果。因此 `finally` 非常适合用于清理工作：关闭文件，释放连接。

如果在 `finally` 中写了自己的 `return`，它会覆盖之前的返回值，甚至“吞掉”异常。从 Python 3.14 起，这种代码会发出 `SyntaxWarning`（PEP 765）——不要在 `finally` 中写 `return`。

### 同时处理多个异常

可以为不同类型各写一个 `except` 块，也可以把多个类型放在一个元组中：

```
try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("金额格式错误：", e)
    amount = 0
```

各块从上到下依次检查，第一个匹配的生效，因此更具体的类型应放在通用类型之前。从 Python 3.14 起（PEP 758），多个类型可以不加括号直接列出——`except ValueError, TypeError:`——但如果使用了 `as`，括号仍然必须保留。

### 如何打印错误信息并获取其类型

通过 `as e` 可以访问异常对象。`str(e)` 得到错误信息，`type(e).__name__` 得到类型名：

```
try:
    int("一百")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: '一百'
```

## traceback 模块：完整的错误文本

有时需要捕获错误，但同时保留完整的 traceback——例如写入日志或报告。这时使用 `traceback` 模块：

```
import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # 将 traceback 打印到 stderr
    text = traceback.format_exc()     # 同样的文本，以字符串形式返回
```

如果使用 `logging` 模块写日志，在 `except` 中直接调用 `logging.exception("无法计算")` 更简单——traceback 会自动附加到消息中。

## Python raise：如何抛出异常

`raise` 语句手动抛出异常。函数借此告知调用者：它无法处理这样的数据：

```
def add_expense(amount):
    if amount < 0:
        raise ValueError(f"金额不能为负数：{amount}")
    ...

add_expense(-5)
# ValueError: 金额不能为负数：-5
```

Python 中的 raise 是语句而不是函数，因此 `raise` 后面不需要括号：括号里传递的是异常类构造函数的参数。根据含义选择类型：值不对用 `ValueError`，类型不对用 `TypeError`。

### 不带参数的 raise 与 raise from

在 `except` 块中，单独的 `raise` 会原样把当前异常继续抛出。当你想记录一些信息，但把处理交给调用者时，就需要这样做。

如果要用另一个异常替换原来的异常，使用 `raise ... from e`——原始错误会保留在异常链中，traceback 中两者都会显示：

```
def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"找不到配置文件 {path}") from e
```

### add_note：为错误补充上下文

从 Python 3.11 开始（PEP 678），可以用 `add_note()` 方法为异常添加备注，然后继续抛出。异常类型不变，备注会显示在错误信息下方：

```
def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"文件 expenses.csv，第 {line_no} 行")
        raise

parse("一百", 17)
# ValueError: could not convert string to float: '一百'
# 文件 expenses.csv，第 17 行
```

## 如何创建自定义异常

自定义异常适用于业务领域的错误：预算超支、订单已支付、用户被封禁。只需继承 `Exception` 即可：

```
class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}：限额 {limit}，已花费 {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("餐饮", 10000, 12500)
except BudgetExceeded as e:
    print("警告：", e)   # 警告： 餐饮：限额 10000，已花费 12500
```

现在调用代码能够专门捕获预算超支的情况，而不会把它与拼写错误混淆。在大型项目中，通常会定义一个公共基类（`class TrackerError(Exception)`），让所有自定义异常都继承自它——这样就可以用一个 `except TrackerError` 捕获全部。应继承自 `Exception`，而不是 `BaseException`。

## 异常组与 except*

有时会同时出现多个错误——例如检查整个文件，或在 `asyncio.TaskGroup` 的并行任务中。从 Python 3.11 起，可以把它们收集到 `ExceptionGroup` 中，并用 `except*` 按类型分别处理：

```
def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("文件中的错误", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"错误的行数：{len(eg.exceptions)}")   # 错误的行数：2
```

## Python 中 try except 捕获所有错误：为什么不应这样做

最有害的习惯是捕获一切：`except: pass`。程序看似“不再崩溃”，但实际上不再报告任何问题。裸 `except` 甚至会捕获 `KeyboardInterrupt` 和 `SystemExit`，导致无法用 Ctrl+C 中断程序。`except Exception: pass` 稍好一些，但仍然会悄无声息地吞掉变量名的拼写错误和逻辑错误。

在 Python 中正确处理异常的方法：

- 只捕获你能够处理的类型，例如 `except ValueError`，而不是全部；
- 让 `try` 块尽量简短——只包含可能以这种方式出错的那几行；
- 如果在程序顶层使用宽泛的捕获（`except Exception`），一定要记录 traceback；
- 在简单情况下，用检查代替异常控制流程：用 `dict.get()` 代替 `try/except KeyError`；
- 处理资源时优先使用 `with`——即使出错，它也会关闭文件。

## 小结

Python 中的异常是正常的错误报告机制，而不是隐藏错误的理由。从下往上阅读 traceback，在 `try/except` 中捕获具体的类型，用 `else` 处理“一切正常”的代码，用 `finally` 做清理。Python raise 让你主动报告错误，`raise from` 和 `add_note` 保留上下文，而自定义异常应继承自 `Exception`。课程中这一主题同样结合实践讲解：支出记录器不会因为 CSV 中的一行错误数据而崩溃，并用自定义异常表示预算超支。

## FAQ

**错误（error）和异常（exception）在 Python 中有什么区别？**

口语中两者是同义词。严格来说，SyntaxError 在解析代码时就会出现，而其他错误是在运行时以可捕获的异常对象形式出现的。

**异常之后能否从出错的那一行继续执行？**

不能。捕获异常后，执行会从 try/except 块之后继续，而不是从出错的那一行继续。如果要逐行处理文件，应把 try 放在循环内部。

**如果在 except 块中又出现了异常，会怎样？**

新异常会继续向上传播，原始异常保存在它的 __context__ 属性中。traceback 中会出现 “During handling of the above exception, another exception occurred” 这样的提示。

**如何在测试中检查函数是否抛出了异常？**

在 pytest 中使用 with pytest.raises(ValueError):，在 unittest 中使用 self.assertRaises(ValueError)。只有当代码块中抛出了指定类型的异常时，测试才会通过。

**assert 是什么，它与 raise 有什么区别？**

assert 条件, "信息" 在条件为假时抛出 AssertionError，它是开发阶段的检查工具。使用 -O 参数运行时，assert 会被禁用，因此对用户输入的检查应该使用 if 加 raise。


---
Source: Skillaria.Top (https://skillaria.top)
