7 minutes
异常处理与调试
程序总会有出错的时候——用户输入了无效数据、网络连接中断、磁盘空间不足……优秀的程序员不是写出永不报错的代码(这是不可能的),而是能优雅地处理错误,让程序在异常情况下也能行为可预测、可排查。
本章将系统学习 Python 的异常处理机制——从 try/except 到自定义异常,再到使用 pdb 和 logging 进行调试,让你的代码从"动不动就崩溃"变成"稳如磐石"。
Python 的异常哲学
Python 推崇 EAFP 原则——“Easier to Ask for Forgiveness than Permission”(请求原谅比获得许可更容易)。也就是:先尝试执行,如果出错了再处理,而不是在动手之前做各种检查。
# LBYL 风格(Look Before You Leap)——非 Python 方式
def safe_divide_lbyl(a, b):
if b == 0:
return None
if not isinstance(a, (int, float)):
return None
return a / b
# EAFP 风格——Python 方式
def safe_divide_eafp(a, b):
try:
return a / b
except (ZeroDivisionError, TypeError):
return None
try/except/else/finally
Python 的异常处理结构由四个子句组成:
try:
# 可能出错的代码
result = risky_operation()
except SomeError:
# 出错时执行的代码
handle_error()
else:
# 没出错时执行的代码
process_result(result)
finally:
# 无论是否出错,都执行的代码
cleanup()
基础用法
try:
number = int(input("请输入一个数字: "))
result = 100 / number
print(f"结果: {result}")
except ValueError:
print("输入的不是有效数字!")
except ZeroDivisionError:
print("不能除以零!")
else 子句
else 在 try 块没有抛出异常时执行。它把"正常逻辑"和"错误处理"清晰分开:
try:
data = load_file("config.json")
except FileNotFoundError:
print("配置文件不存在,使用默认配置")
data = DEFAULT_CONFIG
except json.JSONDecodeError:
print("配置文件格式错误")
data = DEFAULT_CONFIG
else:
print(f"成功加载配置,共 {len(data)} 项") # 只有没异常时才执行
finally 子句
finally 无论是否发生异常都会执行,通常用于资源清理:
def read_file_safe(path):
file = None
try:
file = open(path, "r", encoding="utf-8")
return file.read()
except FileNotFoundError:
print(f"文件未找到: {path}")
return ""
finally:
# 无论成败,都确保文件被关闭
if file is not None:
file.close()
print("文件已关闭")
finally 的一个特殊行为——即使 try 中有 return,finally 也会执行:
def demo():
try:
print("try 中")
return "返回值"
finally:
print("finally —— 即使有 return 也会执行")
print(demo())
# try 中
# finally —— 即使有 return 也会执行
# 返回值
捕获特定异常
永远不要使用裸 except(不带异常类型):
# 极其不推荐——会捕获所有异常,包括 SystemExit、KeyboardInterrupt
try:
result = risky_operation()
except: # 裸 except
print("出错了")
result = None
# 正确——只捕获预期的异常
try:
result = risky_operation()
except (ValueError, TypeError) as e:
print(f"数据处理错误: {e}")
result = None
获取异常信息
使用 as 关键字获取异常对象:
try:
data = {"name": "Alice"}
value = data["age"]
except KeyError as e:
print(f"缺少键: {e}") # 'age'
print(f"异常类型: {type(e).__name__}") # KeyError
print(f"异常详情: {e.args}") # ('age',)
多个异常的统一处理
# 方式 1:元组形式
try:
process(data)
except (ValueError, TypeError, KeyError) as e:
print(f"数据处理错误: {e}")
# 方式 2:链式 except(按优先级排列)
try:
process(data)
except ValueError as e:
print(f"值错误: {e}")
except TypeError as e:
print(f"类型错误: {e}")
# 更具体的异常要放在前面!
异常层次结构
Python 的所有异常都继承自 BaseException。常见的继承链:
BaseException
├── SystemExit # sys.exit() 触发
├── KeyboardInterrupt # Ctrl+C 触发
└── Exception # 所有常规异常的基类
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ ├── KeyError
│ └── ...
├── ValueError
├── TypeError
├── FileNotFoundError
├── AttributeError
├── ImportError
│ └── ModuleNotFoundError
└── ...
这意味着: 捕获 Exception 会捕获它所有的子类。捕获 ArithmeticError 会同时捕获 ZeroDivisionError 和 OverflowError。
try:
result = 1 / 0
except ArithmeticError as e:
# 也会捕获 ZeroDivisionError(它是 ArithmeticError 的子类)
print(f"算术错误: {e}")
常见的内置异常
| 异常 | 含义 | 常见触发场景 |
|---|---|---|
ValueError |
值不符合预期 | int("abc")、remove("不存在的元素") |
TypeError |
类型不匹配 | "hello" + 42、len(123) |
IndexError |
索引超出范围 | [1,2,3][5] |
KeyError |
字典键不存在 | {"a": 1}["b"] |
FileNotFoundError |
文件不存在 | open("不存在的文件.txt") |
ZeroDivisionError |
除以零 | 1 / 0 |
AttributeError |
属性不存在 | "hello".nonexist() |
ImportError |
导入失败 | import nonexistent_module |
ModuleNotFoundError |
模块未找到 | ImportError 的子类 |
StopIteration |
迭代结束 | 生成器耗尽时 |
AssertionError |
assert 失败 |
assert False |
TimeoutError |
操作超时 | 网络请求超时 |
raise —— 主动抛出异常
你可以在自己的代码中主动抛出异常,使用 raise 关键字:
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("取款金额必须为正数")
if amount > balance:
raise ValueError("余额不足")
return balance - amount
# 调用者负责处理
try:
new_balance = withdraw(100, 200)
except ValueError as e:
print(f"取款失败: {e}")
重新抛出异常
在捕获异常后,可以决定不处理而是继续向上抛出:
def process_order(order):
try:
save_to_database(order)
send_confirmation_email(order)
except DatabaseError:
# 记录日志后重新抛出
logger.error(f"数据库错误,订单 {order.id} 未保存")
raise # 重新抛出原始异常
注意: 使用裸 raise 会保留原始的 traceback。如果写成 raise e 则会丢失调用栈信息。
异常链(raise ... from ...)
def load_config(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError as e:
raise ConfigError(f"配置文件缺失: {path}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"配置文件格式错误: {path}") from e
from e 会将原始异常附加到新的异常上,调试时可以看到完整的异常链条。
自定义异常类
对于大型项目,定义自己的异常层次结构是非常好的实践:
class AppError(Exception):
"""应用程序的基类异常"""
pass
class ValidationError(AppError):
"""输入验证失败"""
pass
class NotFoundError(AppError):
"""资源未找到"""
pass
class AuthError(AppError):
"""认证或授权失败"""
pass
class DatabaseError(AppError):
"""数据库操作失败"""
pass
# 使用示例
def get_user(user_id):
user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
if not user:
raise NotFoundError(f"用户 {user_id} 不存在")
return user
def transfer(sender_id, receiver_id, amount):
if amount <= 0:
raise ValidationError("转账金额必须为正数")
try:
sender = get_user(sender_id)
receiver = get_user(receiver_id)
except NotFoundError as e:
raise ValidationError("转账用户不存在") from e
自定义异常的命名通常以 Error 结尾,并继承自 Exception(而非 BaseException)。
assert 断言
assert 是 Python 的调试辅助工具,在开发阶段检查不应发生的条件:
def divide(a, b):
assert b != 0, "除数不能为零"
return a / b
# 等价于:
# if not (b != 0):
# raise AssertionError("除数不能为零")
def calculate_average(numbers):
assert len(numbers) > 0, "列表不能为空"
assert all(isinstance(n, (int, float)) for n in numbers), "所有元素必须是数字"
return sum(numbers) / len(numbers)
重要: assert 在 python -O(优化模式)下会被跳过。所以不要用 assert 来做数据验证或安全检查——这些应该用 if + 正常的异常处理。
# 不要这样做
def login(username, password):
assert username, "用户名不能为空" # 可能会被 -O 跳过!
assert password, "密码不能为空"
# 应该这样做
def login(username, password):
if not username:
raise ValidationError("用户名不能为空")
if not password:
raise ValidationError("密码不能为空")
logging —— 替代 print 的调试方式
在调试时随手写 print() 很常见,但更好的方式是使用 logging 模块:
import logging
# 基本配置
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# 获取日志器
logger = logging.getLogger(__name__)
# 不同级别的日志
logger.debug("调试信息——开发时使用")
logger.info("普通信息——确认程序运行正常")
logger.warning("警告——可能有问题的状况")
logger.error("错误——功能无法正常工作")
logger.critical("严重错误——程序可能崩溃")
配置日志输出到文件
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("app.log", encoding="utf-8"), # 写入文件
logging.StreamHandler(), # 同时输出到控制台
],
)
logger = logging.getLogger(__name__)
logger.info("程序启动")
logging vs print
| 对比项 | print |
logging |
|---|---|---|
| 级别控制 | 没有 | DEBUG/INFO/WARNING/ERROR/CRITICAL |
| 输出目标 | 只能控制台 | 文件、控制台、网络等 |
| 生产环境 | 需手动删除 | 调整 level 即可关闭调试日志 |
| 性能 | 始终执行 | 低于 level 的日志不会处理 |
建议: 从项目一开始就使用 logging,而不是 print。
traceback 模块
traceback 模块可以获取和格式化异常的调用栈信息:
import traceback
try:
1 / 0
except ZeroDivisionError:
# 打印完整的调用栈
traceback.print_exc() # 等价于 print(traceback.format_exc())
# 获取栈信息的字符串
tb_str = traceback.format_exc()
# 可以记录到日志或文件中
with open("error.log", "a") as f:
f.write(tb_str)
import traceback
def deep_func(n):
if n <= 0:
raise ValueError("递归终止")
return deep_func(n - 1)
def handler():
try:
deep_func(5)
except ValueError:
# 获取精简的栈摘要
stack_summary = traceback.extract_stack()
for frame in stack_summary:
print(f" 文件 {frame.filename}, 第 {frame.lineno} 行, 在 {frame.name}")
pdb —— Python 调试器
当 print 和 logging 不够用时,可以用 pdb(Python Debugger)进行交互式调试。
设置断点(Python 3.7+)
def calculate(x, y):
result = x + y
breakpoint() # 在此处暂停,进入调试器
result *= 2
return result
print(calculate(3, 5))
breakpoint() 在 3.7+ 中是内置函数,等价于 import pdb; pdb.set_trace()。
pdb 常用命令
| 命令 | 简写 | 作用 |
|---|---|---|
list |
l |
显示当前行附近的代码 |
next |
n |
执行下一行(不进入函数) |
step |
s |
进入当前行调用的函数 |
continue |
c |
继续执行至下一个断点 |
print expr |
p |
打印表达式值 |
pp expr |
漂亮打印表达式 | |
args |
a |
打印当前函数的参数 |
return |
r |
执行到当前函数返回 |
quit |
q |
退出调试器 |
pdb 使用示例
# 示例:查找 bug
def process_data(data):
total = 0
for i, item in enumerate(data):
breakpoint() # 断点
total += item["value"] * item["count"]
return total
records = [
{"value": 10, "count": 2},
{"value": "5", "count": 3}, # 这里有问题!
{"value": 20, "count": 1},
]
result = process_data(records)
print(result)
运行时的交互:
(Pdb) l # 查看代码
(Pdb) p item # 打印当前元素
(Pdb) p type(item["value"]) # 查看类型
(Pdb) n # 下一步
(Pdb) c # 继续执行
异常处理最佳实践
1. 精确捕获
# 不推荐——过于宽泛
try:
result = process(data)
except Exception as e:
print(f"出错了: {e}")
# 推荐——精确到具体异常
try:
result = process(data)
except (ValueError, TypeError) as e:
print(f"数据错误: {e}")
except ConnectionError as e:
print(f"网络错误: {e}")
retry()
2. 不要吞没异常
# 反模式——无声无息地吞没异常
try:
result = risky_operation()
except Exception:
pass # 最糟糕的做法!
# 如果确实需要忽略,至少要记录日志
try:
result = risky_operation()
except Exception as e:
logger.warning(f"操作失败(可忽略): {e}")
result = None
3. 在合适的层级处理
# 不好的设计——到处都在处理异常
def get_user_email(user_id):
try:
user = db.fetch_user(user_id)
return user.email
except Exception: # 不应该在这里处理
return None
# 更好的设计——让异常向上传播到合适的层级处理
def get_user_email(user_id):
user = db.fetch_user(user_id)
return user.email
# 在调用链的顶层集中处理
def handle_request(user_id):
try:
email = get_user_email(user_id)
return {"email": email}
except NotFoundError:
return {"error": "用户不存在"}, 404
except DatabaseError:
return {"error": "服务暂时不可用"}, 500
4. 自定义异常的命名
class PaymentError(Exception):
"""支付相关的错误基类"""
pass
class InsufficientFundsError(PaymentError):
"""余额不足"""
pass
class CardDeclinedError(PaymentError):
"""卡片被拒绝"""
pass
class PaymentTimeoutError(PaymentError):
"""支付超时"""
pass
5. 异常处理 vs 条件检查
# 用异常处理比条件检查更 Pythonic
try:
value = my_dict[key]
except KeyError:
value = default
# 但有些场景条件检查更清晰
if not os.path.exists(path):
create_directory(path)
完整示例:健壮的文件处理程序
综合运用本章的全部知识:
import json
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
class ConfigError(Exception):
"""配置相关错误"""
pass
class DataProcessError(Exception):
"""数据处理错误"""
pass
def load_config(path: Path) -> dict:
"""加载配置文件,处理常见的异常"""
try:
if not path.exists():
raise FileNotFoundError(f"配置文件 {path} 不存在")
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise ConfigError("配置文件必须是一个 JSON 对象")
logger.info(f"成功加载配置: {path}")
return data
except FileNotFoundError as e:
logger.warning(str(e))
logger.info("使用默认配置")
return {"theme": "light", "language": "zh"}
except json.JSONDecodeError as e:
raise ConfigError(f"JSON 解析错误: {e}") from e
except PermissionError as e:
raise ConfigError(f"权限不足: {e}") from e
def process_user_data(raw_data: str) -> dict:
"""处理用户数据,精确捕获每种异常"""
if not raw_data or not raw_data.strip():
raise DataProcessError("输入数据为空")
try:
user = json.loads(raw_data)
except json.JSONDecodeError as e:
raise DataProcessError(f"无效的 JSON 格式: {e}") from e
required_fields = ["name", "age", "email"]
missing = [f for f in required_fields if f not in user]
if missing:
raise DataProcessError(f"缺少必要字段: {', '.join(missing)}")
try:
user["age"] = int(user["age"])
except (ValueError, TypeError) as e:
raise DataProcessError(f"年龄必须是数字: {e}") from e
if not isinstance(user["email"], str) or "@" not in user["email"]:
raise DataProcessError("邮箱格式无效")
return user
def main():
"""主函数——集中处理所有异常"""
try:
config = load_config(Path("config.json"))
logger.info(f"配置: {config}")
raw = '{"name": "Alice", "age": "28", "email": "alice@example.com"}'
user = process_user_data(raw)
logger.info(f"处理成功: {user['name']}, {user['age']}岁")
except ConfigError as e:
logger.critical(f"配置错误,程序无法启动: {e}")
except DataProcessError as e:
logger.error(f"数据处理失败: {e}")
except Exception as e:
logger.critical(f"未预期的错误: {e}", exc_info=True)
if __name__ == "__main__":
main()
小结
本章我们学习了 Python 的异常处理和调试技术:
try/except/else/finally:完整的异常处理结构- EAFP 原则:“请求原谅比获得许可更容易”
- 精确捕获异常:永远不要使用裸
except - 异常的层次结构:所有异常继承自
BaseException raise与异常链:raise ... from e保留错误上下文- 自定义异常:为项目创建自己的异常层次
assert:调试辅助,但不要用于数据验证logging:用日志替代print,可分级、可持久化traceback:获取和记录调用栈信息pdb:交互式调试器,breakpoint()设置断点- 最佳实践:精确捕获、不吞没异常、在合适层级处理
下一步: 函数与模块篇到这里就结束了。接下来我们将进入面向对象篇,学习类与对象——Python 编程的核心范式。
Summary: 异常处理、logging、pdb 调试与自定义异常。