Python 错误与异常详解
搞懂 Bug 的两种面孔,写出真正健壮的代码
每个程序员都逃不掉和 Bug 打交道。不管是刚写完的新模块还是维护了五年的老系统,Bug 总会在你最不想它出现的时候冒出来。Python 把运行时的问题分为「错误」和「异常」两大类,理解它们的区别和各自的处理方式,是写出可靠代码的第一步。
Python 中的「错误」主要分两种:语法错误是代码写法不对,解释器在执行前就能发现;逻辑错误是代码写法没问题,但业务逻辑有缺陷,只有跑起来才会暴露。
比如循环语句漏了冒号,或者括号没闭合,这类问题在 PyCharm、VS Code 等编辑器里会直接标红提示,基本不会带到运行阶段。
for i in range(5)
print(i)
SyntaxError: expected ':'
逻辑错误是真正让人头疼的部分——语法完全正确,程序也能正常跑完,但输出的结果就是不对。比如计算平均分时忘了处理空列表的情况:
def avg_score(scores):
return sum(scores) / len(scores)
# 正常调用
print(avg_score([85, 90, 78])) # 84.33
# 空列表调用 -> 程序崩溃
print(avg_score([]))
ZeroDivisionError: division by zero
编辑器不会提醒你 len(scores) 可能为 0,这类问题只能靠编码经验和对业务场景的理解来预防——或者用异常处理来兜底。
💡 小贴士
语法错误是「写错了」,逻辑错误是「想错了」。前者靠工具帮你兜底,后者只能靠扎实的编码习惯和对边界条件的敏感度。测试工程师在设计用例时,应重点关注意后者——尤其空值、零值、越界等边界场景。
语法正确不代表万事大吉。程序在执行过程中,可能会遇到各种意料之外的情况:读取一个不存在的文件、访问列表中超出范围的索引、网络连接突然中断……这些运行期才暴露的问题,就是「异常」。
如果代码里没有对异常做处理,Python 解释器会直接打印一长串错误信息(Traceback)并终止程序。这在线上环境中显然是不可接受的——我们需要主动捕获异常,让程序在遇到问题时优雅地降级或恢复。
03Python 异常继承体系BaseException 家族Python 的所有异常都继承自 BaseException,它有四个直接子类,其中 Exception 是我们日常打交道最多的——几乎所有业务异常都继承自它。
BaseException ← 所有异常的根
├── SystemExit ← sys.exit() 触发
├── KeyboardInterrupt ← Ctrl+C 中断
├── GeneratorExit ← 生成器被关闭
└── Exception ← 业务异常的根(重点关注)
├── ArithmeticError ← 算术类
│ ├── ZeroDivisionError │ 除零
│ └── OverflowError │ 数值溢出
├── LookupError ← 查找类
│ ├── IndexError │ 索引越界
│ └── KeyError │ 字典键不存在
├── OSError ← 系统操作类
│ ├── FileNotFoundError │ 文件不存在
│ ├── PermissionError │ 权限不足
│ └── TimeoutError │ 超时
├── TypeError ← 类型不匹配
├── ValueError ← 值不合法
├── NameError ← 变量未定义
├── AttributeError ← 属性不存在
├── ImportError ← 导入失败
├── RuntimeError ← 运行时错误
│ └── RecursionError │ 递归过深
├── StopIteration ← 迭代器耗尽
└── Warning ← 警告类
├── DeprecationWarning │ 弃用警告
└── UserWarning │ 用户自定义警告
这个继承关系非常实用:捕获父类异常时,所有子类异常都会被一并捕获。比如 except OSError 可以同时处理 FileNotFoundError 和 PermissionError。
日常开发中最常遇到的异常,整理如下:
💡 小贴士
继承关系意味着你可以用父类异常做「兜底捕获」,但不建议滥用。比如 except Exception 会捕获所有业务异常,可能把本该暴露的问题也吞掉。精确捕获具体异常类型,才是最佳实践。
先看一个不处理异常的例子——解析用户输入的数字,如果输入的不是数字,程序直接崩掉:
user_input = "abc"
number = int(user_input)
print(f"你输入的数字是: {number}")
ValueError: invalid literal for int() with base 10: 'abc'
user_input = "abc"
try:
number = int(user_input)
print(f"你输入的数字是: {number}")
except ValueError:
print("输入的不是有效数字,请重试")
🔑 try/except 执行流程
① 先执行 try 块中的代码
② 如果没出异常,跳过所有 except,继续往下走
③ 如果出了异常,从上往下依次匹配 except 后面的异常类型
④ 匹配成功就执行对应 except 块,然后继续后续代码
⑤ 没匹配到任何 except,异常会向上层调用者传递
⑥ 如果最终无人处理,程序终止并打印 Traceback
实际开发中,一段代码可能抛出多种异常,可以写多个 except 分支分别处理:
def read_config(filepath):
try:
with open(filepath, 'r') as f:
data = f.read()
return int(data.strip())
except FileNotFoundError:
print("配置文件不存在,使用默认值")
return 8080
except ValueError:
print("配置文件内容不是数字,使用默认值")
return 8080
except PermissionError:
print("没有权限读取配置文件")
return 8080
💡 小贴士
用 except ValueError as e 可以拿到异常对象,通过 e 访问错误详情,方便记录日志做问题排查。
除了 try 和 except,Python 还提供了两个可选子句:else 和 finally,它们各自承担不同职责:
Python - 完整 try/except/else/finallydef safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("except --> 除数不能为0")
except TypeError:
print("except --> 参数必须是数字")
else:
print(f"else --> 计算成功: {result}")
finally:
print("finally --> 计算结束\n")
# 测试三种情况
safe_divide(10, 2) # 正常
safe_divide(10, 0) # 除零异常
safe_divide(10, "x") # 类型异常
① safe_divide(10, 2) — 正常流程
else --> 计算成功: 5.0
finally --> 计算结束
② safe_divide(10, 0) — 除零异常
except --> 除数不能为0
finally --> 计算结束
③ safe_divide(10, "x") — 类型异常
except --> 参数必须是数字
finally --> 计算结束
🔑 关键要点
① else:try 块没有抛出异常时才执行,适合放「成功后的后续操作」
② finally:无论是否异常都会执行,适合放资源清理(关文件、断连接)
③ 顺序必须是:try → except → else → finally,不能调换
💡 小贴士
为什么用 else 而不直接把后续代码写在 try 里?因为如果把后续代码也放进 try,它抛出的异常也会被 except 捕获,可能导致误捕获。把成功路径放到 else 里,职责更清晰。
有时候程序本身没出错,但业务逻辑不允许继续执行——比如用户传入了负数的年龄、注册时邮箱格式不对。这时可以用 raise 主动抛出异常,强制中断执行并通知调用者:
def set_age(age):
if not isinstance(age, int):
raise TypeError("年龄必须是整数")
if age < 0 or age > 150:
raise ValueError(f"年龄 {age} 不在合理范围内(0-150)")
print(f"年龄设置成功: {age}")
# 正常调用
set_age(25) # 年龄设置成功: 25
# 类型不对
set_age("25") # TypeError: 年龄必须是整数
# 值不合理
set_age(-1) # ValueError: 年龄 -1 不在合理范围内(0-150)
raise 的参数必须是一个异常实例或异常类。在实际项目中,合理使用 raise 可以把问题尽早暴露在调用链的上游,避免错误数据一路传递下去造成更难排查的问题。
内置异常覆盖了通用场景,但在中大型项目中,我们往往需要更精准的错误分类。比如一个测试平台,接口调用失败、用例数据格式错误、断言未通过,这些最好用不同的异常类型区分,方便定位问题。做法很简单——继承 Exception 即可:
# 自定义异常基类
class TestPlatformError(Exception):
"""测试平台异常基类"""
def __init__(self, message, detail=None):
self.message = message
self.detail = detail
super().__init__(self.message)
# 接口调用失败异常
class ApiCallError(TestPlatformError):
"""API 调用异常"""
pass
# 用例数据格式异常
class CaseDataError(TestPlatformError):
"""测试用例数据异常"""
pass
# 使用示例
def run_test_case(case):
if not isinstance(case, dict):
raise CaseDataError(
"用例数据必须是字典类型",
detail=f"实际类型: {type(case).__name__}"
)
if "url" not in case:
raise CaseDataError("用例缺少必填字段: url")
print(f"执行用例: {case.get('url')}")
# 测试
try:
run_test_case("not a dict")
except CaseDataError as e:
print(f"用例数据错误: {e.message}")
if e.detail:
print(f"详情: {e.detail}")
except ApiCallError as e:
print(f"接口调用失败: {e.message}")
用例数据错误: 用例数据必须是字典类型
详情: 实际类型: str
这个例子展示了实际项目中的典型做法:定义一个异常基类承载通用信息(message + detail),再派生子类区分具体错误类型。调用方可以精确捕获某个子类异常,也可以用基类做兜底捕获。
⚠️ 踩坑经验
千万别用 except: pass 或 except Exception: pass 来「静默」异常——这会让问题被隐藏,排查时完全找不到线索。正确做法是:捕获具体异常 → 记录日志 → 返回合理的默认值或重新抛出。
| | |
|---|
| | |
| | |
| | |
| | |
| | |
| | |
| class XxxError(Exception) | |
💡 给测试工程师的建议
1. 异常场景是测试用例的重要维度:空值、越界、类型不匹配、网络中断……正常流程通过不代表异常流程也稳
2. 自动化脚本必须做异常处理:一个接口超时不应该导致整条用例线全挂
3. 别用裸 except 吞异常:except Exception as e 搭配日志记录,才能定位问题
4. finally 做资源释放:文件句柄、数据库连接、浏览器驱动,确保无论是否异常都能关闭
异常处理不是「可选技能」,而是写出可靠代码的基本功。记住核心原则:精确捕获、妥善处理、资源必释放。把这些习惯融入日常编码,你的程序会从一个「能跑的程序」变成一个「跑得稳的程序」。
Python 学习笔记系列 | 第十三篇