基础

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

更新于: · Skillaria.Top

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 "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

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

如何阅读 traceback

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

Traceback (most recent call last):
  File "tracker.py", line 9, in <module>
    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 中的一行错误数据而崩溃,并用自定义异常表示预算超支。

常见问题

错误(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。

通过视频学习本主题

课程“Python 3.15: от новичка до профи”结合实践详细讲解了这一主题。

前往课程