跳到主要内容

异常基础

异常(exception)是程序运行过程中发生的错误信号。当 Python 无法继续执行某段代码时,就会抛出(raise)一个异常对象;如果不处理这个异常,程序就会终止并打印错误信息。理解异常的工作机制是编写健壮代码的第一步——它让你能够预见错误、优雅地恢复,并给出有意义的反馈,而不是让程序崩溃。

什么是异常

异常本质上是一种特殊的对象,它描述了程序运行中遇到的问题。当错误发生时,Python 会创建一个异常对象并中断当前执行流,沿着调用栈向上传递,直到被某个 try 语句捕获处理;如果一直没被捕获,程序就会终止并打印 Traceback。

# 一个未捕获的异常会让程序崩溃
print(10 / 0)
# ZeroDivisionError: division by zero

执行上面的代码,会看到类似这样的错误输出:

Traceback (most recent call last):
File "example.py", line 1, in <module>
print(10 / 0)
~~~^^^
ZeroDivisionError: division by zero

:::info 异常不是"坏东西" 异常不是设计缺陷,而是 Python 与你交流错误的方式。一个好的程序应当主动抛出异常来标识异常情况,并合理处理那些预期可能发生的异常。完全消除异常既不可能也不必要。 :::

异常 vs 错误返回码

一些语言(如 C、Go)用返回值表示错误,调用者必须检查每个返回值。Python 选择用异常来表达错误,这样做的好处是:

  • 正常逻辑与错误处理分离:核心代码不会被错误检查淹没
  • 错误不会"被忽略":忘记处理异常会导致程序崩溃,而不是悄悄返回错误结果
  • 错误向上传递:异常会自动沿着调用栈向上传播,不需要逐层手动转发
# 返回码风格(Go/C 风格,不 Pythonic)
def divide_code(a, b):
if b == 0:
return None, "division by zero"
return a / b, None

result, err = divide_code(10, 0)
if err is not None:
print(f"错误:{err}")
else:
print(result)

# 异常风格(Pythonic)
def divide(a, b):
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b

try:
print(divide(10, 0))
except ZeroDivisionError as e:
print(f"错误:{e}")

异常层次结构

Python 的所有异常都从一个根类 BaseException 派生。绝大多数用户遇到的异常都继承自 Exception,这是"普通错误"的基类。

BaseException
├── SystemExit # sys.exit() 触发
├── KeyboardInterrupt # Ctrl+C 中断
├── GeneratorExit # 生成器被关闭
└── Exception # 所有普通异常的基类
├── StopIteration # 迭代器耗尽
├── StopAsyncIteration # 异步迭代器耗尽
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── AttributeError
├── OSError
│ ├── FileNotFoundError
│ ├── FileExistsError
│ ├── PermissionError
│ ├── IsADirectoryError
│ └── TimeoutError
├── TypeError
├── ValueError
│ └── UnicodeError
├── NameError
├── RuntimeError
│ └── NotImplementedError
├── Warning # 警告的基类
│ ├── DeprecationWarning
│ ├── UserWarning
│ └── ...
└── ... 还有更多

BaseException 与 Exception

# 异常本身也是对象,可以查看其继承关系
print(ZeroDivisionError.__mro__)
# 输出:
# (<class 'ZeroDivisionError'>, <class 'ArithmeticError'>,
# <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

print(isinstance(ZeroDivisionError(), Exception)) # 输出:True
print(isinstance(ZeroDivisionError(), BaseException)) # 输出:True
print(isinstance(Exception(), BaseException)) # 输出:True

:::warning BaseException 不要直接捕获 BaseException 包含了 KeyboardInterrupt(用户按 Ctrl+C)和 SystemExit(程序主动退出)等"非错误"信号。直接捕获 BaseException 会把这些信号也吞掉,导致无法中断或正常退出程序。日常开发中只捕获 Exception 及其子类,让 Ctrl+C 等信号能正常穿透。 :::

异常的派生关系用于分组

异常的层次结构让你既能精确捕获某个具体异常,也能捕获整组异常:

def safe_get(mapping, key, default=None):
try:
return mapping[key]
except KeyError: # 精确捕获 KeyError
return default
except LookupError: # 兜底捕获所有查找类错误(更宽泛)
return default

print(safe_get({"a": 1}, "b", "默认值")) # 输出:默认值

常见内置异常

Python 内置了大量异常类型,下面是最常用的几个。

ValueError

值类型正确,但内容不合法时抛出。

# 把字符串转成整数,但内容不是数字
n = int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'

# 从列表中移除不存在的元素
nums = [1, 2, 3]
nums.remove(5)
# ValueError: list.remove(x): x not in list

TypeError

类型不匹配,对不支持该操作的对象执行了某种运算。

# 数字与字符串相加
result = 1 + "2"
# TypeError: unsupported operand type(s) for +: 'int' and 'str'

# 对不可迭代对象做迭代
for x in 42:
pass
# TypeError: 'int' object is not iterable

KeyError

字典中找不到指定键。

user = {"name": "Alice", "age": 30}
print(user["email"])
# KeyError: 'email'

# 用 .get() 或 in 检查可避免
print(user.get("email", "未填写")) # 输出:未填写

IndexError

索引超出序列范围。

nums = [10, 20, 30]
print(nums[5])
# IndexError: list index out of range

FileNotFoundError

文件不存在(属于 OSError 的子类,Python 3.3+)。

with open("不存在的文件.txt") as f:
content = f.read()
# FileNotFoundError: [Errno 2] No such file or directory: '不存在的文件.txt'

ZeroDivisionError

除以零或对零取模。

print(5 / 0) # ZeroDivisionError: division by zero
print(5 % 0) # ZeroDivisionError: integer division or modulo by zero

AttributeError

访问对象不存在的属性或方法。

name = "Alice"
print(name.uppercase())
# AttributeError: 'str' object has no attribute 'uppercase'
# Did you mean: 'upper'?

:::tip Python 3.12+ 的建议功能 从 Python 3.10 开始,AttributeErrorNameError 会自动给出"你是不是想用……"的建议,这对快速定位拼写错误非常友好。例如上面提示 Did you mean: 'upper'?。 :::

NameError

访问未定义的变量名。

print(undefined_var)
# NameError: name 'undefined_var' is not defined. Did you mean: 'undefined'?

RuntimeError

运行时出现的一般性错误,通常不属于其他更具体的类别时使用。其子类 NotImplementedError 表示某功能尚未实现。

class Animal:
def speak(self):
raise NotImplementedError("子类必须实现 speak()")

Animal().speak()
# NotImplementedError: 子类必须实现 speak()

一览表

异常名触发场景
ValueError值类型正确但内容不合法
TypeError类型不匹配
KeyError字典键不存在
IndexError索引越界
FileNotFoundError文件不存在
PermissionError权限不足
ZeroDivisionError除零
AttributeError属性不存在
NameError变量名未定义
ImportError模块导入失败
StopIteration迭代器耗尽
RuntimeError一般运行时错误
NotImplementedError功能未实现
OSError系统相关错误(基类)

阅读 Traceback

Traceback(回溯)是程序崩溃时打印的错误堆栈。学会阅读它,是排查问题的核心技能。

Traceback 的结构

Traceback (most recent call last): ← 1. 错误类型标记
File "main.py", line 10, in <module> ← 2. 调用栈(最外层→最内层)
result = process(data)
~~~~~~~^^^^^^^ ← 3. 出错的代码位置(3.11+ 高亮)
File "main.py", line 6, in process ← 4. 更深一层的调用
return data["value"] / 0
~~~~^^^^^^^^~~
ZeroDivisionError: division by zero ← 5. 最终异常类型与信息

阅读顺序建议:

  1. 从最后一行看起:异常类型 + 错误信息,立刻知道发生了什么
  2. 从底部往上读调用栈:每一段都告诉你"哪个文件、哪一行、哪个函数"出错
  3. 关注最顶部的调用:那是离错误源头最近的位置

:::tip Python 3.11+ 更清晰的错误信息 Python 3.11 起大幅改进了错误信息:会用 ^ 符号精确高亮出错的表达式片段,而不是整行。例如:

File "x.py", line 2
x['a']['b'] = 1
^^^^^^^^
TypeError: 'int' object is not subscriptable

这让你一眼就能定位到是 x['a'] 这部分出了问题。 :::

一个完整的示例

def load_config(path):
with open(path) as f: # ① 这里可能抛 FileNotFoundError
lines = f.readlines()
return dict(line.split("=", 1) for line in lines)

def main():
config = load_config("missing.conf") # ② 这里调用
print(config["debug"]) # ③ 可能抛 KeyError

main()

如果 missing.conf 不存在,运行后会看到:

Traceback (most recent call last):
File "main.py", line 9, in <module>
main()
File "main.py", line 6, in main
config = load_config("missing.conf")
~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "main.py", line 2, in load_config
with open(path) as f:
~~~~^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'missing.conf'

从下往上读:最后一行告诉你"找不到文件";再往上找到出错代码位置在 load_config 第 2 行的 open(path);再往上看,调用来源是 main 函数第 6 行。

异常即对象

异常在 Python 中就是普通的类实例对象。这意味着你可以:

  • 检查异常的属性:错误信息、错误码等
  • 捕获后再次抛出:保留原始上下文
  • 创建自定义异常类:继承 Exception 表达业务错误
  • 用 isinstance 判断异常类型

异常的属性和方法

exc = ValueError("无效的输入:负数")
print(type(exc)) # 输出:<class 'ValueError'>
print(str(exc)) # 输出:无效的输入:负数
print(exc.args) # 输出:('无效的输入:负数',)
print(repr(exc)) # 输出:ValueError('无效的输入:负数')

# 异常对象支持解包
msg, = exc.args
print(msg) # 输出:无效的输入:负数

OSError 的额外属性

OSError 及其子类(FileNotFoundErrorPermissionError 等)额外携带 errnostrerror 属性:

import errno

try:
with open("/root/protected.txt") as f:
f.read()
except OSError as e:
print(f"错误码 errno: {e.errno}") # 13 (EACCES)
print(f"错误信息 strerror: {e.strerror}") # Permission denied
print(f"文件名 filename: {e.filename}") # /root/protected.txt

# 对照常见错误码
if e.errno == errno.ENOENT:
print("文件不存在")
elif e.errno == errno.EACCES:
print("权限不足")

捕获后保留异常对象

使用 as 把异常对象绑定到变量,便于进一步检查或记录日志:

def parse_age(text: str) -> int:
try:
age = int(text)
if age < 0 or age > 150:
raise ValueError(f"年龄 {age} 不在合法范围内")
return age
except ValueError as exc:
# exc 是异常对象,可以记录或重新抛出
print(f"解析失败:{exc}(参数:{exc.args})")
return -1

print(parse_age("25")) # 输出:25
print(parse_age("abc")) # 输出:解析失败:invalid literal...(参数:...) / -1
print(parse_age("200")) # 输出:解析失败:年龄 200 不在合法范围内 / -1

:::info 异常对象的 notes Python 3.11+ 给异常增加了 add_note() 方法,可以在异常对象上附加额外说明信息,便于调试时记录更多上下文:

try:
data = {"x": 1}
value = data["y"]
except KeyError as exc:
exc.add_note(f"当前字典内容:{data}")
exc.add_note("检查键名是否拼写正确")
raise # 重新抛出,note 会一并打印

打印时这些 note 会出现在 Traceback 之后,对于复杂场景的调试极其有用。 :::

异常的抛出

使用 raise 语句主动抛出异常。可以抛出内置异常,也可以抛出自定义异常类。

def withdraw(balance: float, amount: float) -> float:
if amount <= 0:
raise ValueError("取款金额必须为正数")
if amount > balance:
raise ValueError(f"余额不足:当前 {balance},需取 {amount}")
return balance - amount

# 抛出时可以只给类名,也可以给实例
raise ValueError("错误描述")
raise ValueError # 等价于 raise ValueError(),但通常应附带说明

:::warning raise 一定要带信息 不要写裸的 raise ValueError()——没有任何描述信息会让排查变得困难。永远提供清晰、可操作的错误信息,说明发生了什么以及期望是什么。 :::

重新抛出当前异常

except 块中可以使用裸 raise 重新抛出当前捕获的异常,保留完整的原始上下文:

def process(data: dict) -> int:
try:
return int(data["value"])
except KeyError:
print("警告:缺少 value 字段")
raise # 重新抛出 KeyError,让上层处理

try:
process({})
except KeyError:
print("上层捕获到 KeyError")
# 输出:
# 警告:缺少 value 字段
# 上层捕获到 KeyError

实战:识别常见异常

下面这段代码故意触发了多种常见异常,请尝试阅读 Traceback 并识别每种异常的类型与原因:

def buggy_pipeline(data: dict) -> str:
"""演示多种异常的混合处理"""
# 1. KeyError:缺少键
try:
name = data["name"]
except KeyError:
name = "匿名"

# 2. ValueError:类型转换失败
try:
age = int(data.get("age", "未知"))
except (ValueError, TypeError) as exc:
print(f" 年龄解析失败:{exc}")
age = 0

# 3. TypeError:类型不匹配的运算
try:
score = data.get("score", 0)
normalized = score / 100 # 若 score 是 str 会 TypeError
except TypeError as exc:
print(f" 分数计算失败:{exc}")
normalized = 0.0

# 4. IndexError:索引越界
try:
first_tag = data["tags"][0]
except (KeyError, IndexError) as exc:
print(f" 获取标签失败:{exc}")
first_tag = None

return f"{name}{age} 岁),归一化分数:{normalized:.2f},标签:{first_tag}"


# 故意传入不规范的数据触发各种异常
samples = [
{"name": "Alice", "age": "30", "score": 85, "tags": ["vip"]},
{"age": "abc", "score": "九十分"}, # 缺 name、age 非数字、score 是字符串
{}, # 全空字典
{"name": "Bob", "age": 25, "score": 90, "tags": []}, # tags 为空列表
]

for i, sample in enumerate(samples, 1):
print(f"\n样本 {i}{sample}")
result = buggy_pipeline(sample)
print(f" → {result}")

输出示例:

样本 1:{'name': 'Alice', 'age': '30', 'score': 85, 'tags': ['vip']}
→ Alice(30 岁),归一化分数:0.85,标签:vip

样本 2:{'age': 'abc', 'score': '九十分'}
分数计算失败:unsupported operand type(s) for /: 'str' and 'int'
获取标签失败:'tags'
→ 匿名(0 岁),归一化分数:0.00,标签:None

样本 3:{}
年龄解析失败:invalid literal for int() with base 10: '未知'
分数计算失败:unsupported operand type(s) for /: 'NoneType' and 'int'
获取标签失败:'tags'
→ 匿名(0 岁),归一化分数:0.00,标签:None

样本 4:{'name': 'Bob', 'age': 25, 'score': 90, 'tags': []}
获取标签失败:list index out of range
→ Bob(25 岁),归一化分数:0.90,标签:None

:::tip 学会"防御性编程" 上面的代码体现了防御性编程的思路:在每个可能出错的位置包一层 try,让程序即便遇到异常数据也能继续处理后续逻辑,而不是整体崩溃。下一节我们将系统学习 try-except 语句的所有用法。 :::

小结

  • 异常是 Python 表达错误的标准方式,它让错误更难被忽略
  • 所有异常派生自 BaseException,普通错误都继承 Exception——日常只捕获 Exception
  • 异常层次结构允许按"分组"捕获:捕获 LookupError 能同时兜住 KeyErrorIndexError
  • 常见内置异常:ValueErrorTypeErrorKeyErrorIndexErrorFileNotFoundErrorZeroDivisionErrorAttributeErrorNameError
  • 阅读 Traceback 的顺序:先看最后一行的异常类型与信息,再从底部往上追调用栈
  • 异常即对象,可以检查属性、附加 note、重新抛出
  • raise 主动抛异常时,务必附带清晰的信息

下一节我们将学习 try-except 语句,掌握捕获、传递、链式抛出以及 Python 3.11+ 的 ExceptionGroup 等进阶用法。