37 KiB
brief, sources
| brief | sources | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 异常处理是 Python 报告、传播、捕获和设计运行时错误语义的机制。 |
|
异常处理
异常处理是 Python 中处理运行时错误、异常状况、资源清理、程序退出、错误测试和诊断记录的核心机制。程序在执行过程中遇到无法正常完成的操作时,会抛出异常;如果异常没有被处理,程序通常会中止并显示 traceback。通过 raise、try-except、finally、with、SystemExit、sys.exit()、assert、测试框架中的异常断言、Python日志记录 中的 logging,以及 debugging 中的调试工具,程序可以报告错误、捕获特定错误、跳过坏数据、释放资源、把错误继续传播给调用者、验证某段代码是否按预期失败,或把诊断信息交给可配置的日志系统。
异常处理既是错误恢复机制,也是接口设计机制。Python 的动态类型特征使很多错误不会由编译器提前发现,而是在程序运行时暴露。因此,异常与 Python测试、debugging、traceback、pdb、repl、print debugging、robust programming、软件测试、concepts/单元测试、Python动态类型 和 程序诊断 密切相关。
随着课程从内置数据类型进入 concepts/类与对象,异常处理也扩展为面向对象设计的一部分。第 4 章“Classes and Objects”的总览明确把“定义新异常”列为类与对象主题之一:异常本身也是对象,自定义异常由 class 语句定义,通常继承自 Exception,并可借助 继承 形成异常层次结构。换言之,异常处理并不只是 try-except 语法;它还依赖 面向对象编程、Python 类与对象、继承与扩展性、Python 特殊方法、动态属性查找 和 Python 对象模型。类机制让库作者可以设计清晰、稳定、可捕获的错误类型,从而构建更可扩展的程序。
异常处理在文件读取、CSV 解析、数据类型转换、命令行脚本、函数设计、资源管理、程序入口设计、类设计、类库 API 设计、日志记录、测试和调试中都很常见。它尤其适合处理来自外部环境的不确定输入,例如缺失字段、空行、格式错误、非法数字、文件不存在、命令行参数数量错误、环境变量缺失等情况。它与 file processing、csv processing、Python容器、资源管理、command line arguments、Python程序入口、命令行工具设计 和 模块化程序设计 密切相关。
学习目标
学习本概念后,应能够:
- 理解异常是什么,以及异常为什么会导致程序中止。
- 读懂 Python 的错误信息和 traceback,知道最后一行通常给出直接原因。
- 根据 traceback 追踪调用栈,定位出错文件、行号和函数调用链。
- 理解 Python 通常不会预先检查函数参数类型,错误多在运行时暴露。
- 使用
raise主动抛出异常。 - 使用
try-except捕获并处理特定异常。 - 理解异常沿调用栈传播到第一个匹配
except的过程。 - 获取异常对象中的错误信息,例如
except ValueError as e。 - 捕获多个异常,并理解过宽捕获的风险。
- 使用裸
raise重新抛出已捕获异常。 - 区分“快速失败”和“优雅恢复”的不同策略。
- 在文件和 CSV 数据处理中跳过或报告坏数据行。
- 判断何时使用
try-except,何时用if预先检查数据。 - 使用
logging替代直接print(),把异常诊断信息交给可配置日志系统。 - 使用
finally和with安全释放文件、锁等资源。 - 使用
SystemExit或sys.exit()终止程序,并理解非零退出码表示错误。 - 理解
assert会在条件不成立时抛出AssertionError。 - 在单元测试中验证代码是否抛出预期异常,例如
unittest.TestCase.assertRaises()或pytest.raises()。 - 理解异常也是对象,并能用
class定义新的异常类型。 - 通过继承构建异常层次结构,例如通用错误父类和具体错误子类。
- 理解“定义新异常”是类与对象学习路径中的重要应用,而不是孤立语法。
前置知识
学习异常处理前,最好已经了解:
- Python 基本语句和表达式,见 summaries/02_Hello_world。
- 变量、字符串、数字类型转换,例如
int()、float()。 - 文件读取与逐行处理,见 summaries/06_Files。
- 函数定义与调用,见 summaries/07_Functions。
- 列表、字典、集合等基本容器,见 summaries/02_Containers。
- 函数参数、默认参数和可选参数,见 summaries/02_More_functions。
- Python 模块可以被导入,也可以作为主程序运行,见 summaries/05_Main_module。
- 类、对象和继承的基础概念,见 concepts/类与对象、继承、面向对象编程 和 summaries/04_Classes_objects__00_Overview。
- 单元测试基础,见 summaries/01_Testing、concepts/单元测试 和 Python unittest。
- 日志记录基础,见 summaries/02_Logging 和 Python日志记录。
- 调试基础,见 summaries/03_Debugging。
核心解释
Python 的运行时错误模型
Python 通常不会在函数调用前严格检查参数类型或取值。函数只要接收到的数据支持函数体中的操作,就会运行;否则错误会在运行时以异常形式出现。
def add(x, y):
return x + y
add(3, 4) # 7
add('Hello', 'World') # 'HelloWorld'
add('3', '4') # '34'
同一个函数既可以做数字加法,也可以做字符串拼接,因为 + 对这些对象都是有效操作。但如果传入不兼容的对象:
add(3, '4')
就会在运行时失败:
TypeError: unsupported operand type(s) for +: 'int' and 'str'
这体现了 Python 的动态类型特征:代码是否正确通常需要通过运行、测试和真实输入来验证。正因为没有编译器提前发现所有错误,summaries/01_Testing 强调测试在 Python 中尤其重要。相关主题可见 Python测试、debugging 和 Python动态类型。
什么是异常
异常是程序运行时发生的错误或异常状况。例如:
int('N/A')
这段代码会失败,因为字符串 'N/A' 不能转换为整数。Python 会抛出 ValueError:
ValueError: invalid literal for int() with base 10: 'N/A'
如果这个异常没有被捕获,程序会终止,并显示 traceback。处理真实数据时,一行空白 CSV、一个缺失字段、一个无法转换的价格、一个不存在的字典键,甚至一个缺失的命令行参数,都可能让程序中止。
row = []
price = float(row[1]) # IndexError
row = ['IBM', '']
price = float(row[1]) # ValueError
traceback 的作用
当异常未被处理时,Python 会打印 traceback。traceback 通常包含:
- 错误发生在哪个文件。
- 错误发生在哪一行。
- 哪些函数调用导致了这个错误。
- 最终的异常类型和错误信息。
阅读 traceback 时,一个实用原则是:最后一行通常说明崩溃的直接原因。例如:
AttributeError: 'int' object has no attribute 'append'
这说明代码试图在整数对象上调用 append() 方法。最后一行给出异常类型和错误消息;上面的 File ... line ... in ... 则显示调用栈,即程序从哪个函数一步步走到出错位置。相关主题:traceback、call stack、debugging。
抛出异常:raise
可以使用 raise 主动报告错误:
if name not in authorized:
raise RuntimeError(f'{name} not authorized')
主动抛出异常适合用于:
- 输入参数组合在语义上不合法。
- 程序状态不符合预期。
- 某个分支理论上不应该发生。
- 函数无法继续履行自己的职责。
- 命令行参数错误,程序无法继续运行。
- 类或对象处于无效状态,无法执行某个方法。
- 库函数收到无法支持的选项或格式名。
例如,一个 CSV 解析函数 parse_csv() 支持通过 select 选择列,但这个功能依赖输入文件有列标题。因此,如果调用者同时传入 select 和 has_headers=False,这是一种无意义的参数组合,应主动抛出异常:
if select and not has_headers:
raise RuntimeError('select argument requires column headers')
如果这个错误属于某个领域或库,也可以定义专门异常:
class CSVFormatError(Exception):
pass
if select and not has_headers:
raise CSVFormatError('select argument requires column headers')
捕获异常:try-except
使用 try-except 可以捕获并处理异常:
try:
shares = int(fields[1])
except ValueError:
print('Could not parse', line)
含义是:先执行 try 代码块;如果没有错误,继续正常执行;如果发生 ValueError,跳到对应的 except 代码块。程序不会因为这个错误立即中止。
也可以把异常对象保存到变量中:
try:
authenticate(username)
except RuntimeError as e:
print(e)
常见异常类型包括:
TypeError:类型不支持某操作。ValueError:值的格式或内容不合法。KeyError:字典中找不到指定键。IndexError:序列索引越界。ImportError:模块导入失败。RuntimeError:一般运行时错误。AssertionError:断言失败。SyntaxError:语法错误。KeyboardInterrupt:用户中断程序。FileNotFoundError:文件不存在。SystemExit:程序请求退出。- 自定义领域错误,例如
PortfolioError、CSVFormatError、FormatError。
实际编程中应优先捕获具体异常,而不是笼统捕获所有错误。
异常传播与重新抛出
异常会沿调用栈向上传播,直到遇到第一个匹配的 except 块。
def grok():
raise RuntimeError('Whoa!')
def spam():
grok()
def bar():
try:
spam()
except RuntimeError as e:
print('caught:', e)
在这个例子中,RuntimeError 在 grok() 中产生,经由 spam() 传播到 bar(),并在 bar() 的 except RuntimeError 中被捕获。由于异常已经被处理,它不会继续传播。
如果需要记录日志、打印诊断信息或执行补救动作,但仍希望调用者知道错误,可以在 except 中使用裸 raise 重新抛出当前异常:
try:
go_do_something()
except Exception as e:
log.error('Operation failed: %s', e)
raise
在把底层异常转换为领域异常时,也可以使用异常链:
try:
price = float(row[1])
except ValueError as e:
raise BadPriceError(f'bad price: {row[1]}') from e
这样既向调用者暴露更贴近业务语义的 BadPriceError,又保留原始 ValueError 的调试线索。
异常处理与日志记录
在处理坏数据时,可以在 except 中直接 print(),但这会把诊断输出固定在代码里;也可以静默 pass,但这会隐藏坏数据和真实错误。因此,summaries/02_Logging 引入了 logging 模块。模块代码可以创建 logger:
import logging
log = logging.getLogger(__name__)
然后在异常处理中使用日志调用:
try:
records.append(split(line, types, names, delimiter))
except ValueError as e:
log.warning('Could not parse : %s', line)
log.debug('Reason : %s', e)
warning 表示用户或操作者应知道的问题,例如某行无法解析;debug 表示开发者调试时才需要的细节,例如底层异常原因。模块只负责发出日志,不决定日志写到哪里、显示哪些级别或采用什么格式。主程序可以统一配置日志输出位置、级别和格式,体现 关注点分离。
不要在通用库模块中随意调用 basicConfig(),否则会破坏使用者对日志系统的控制。
捕获多个异常与捕获所有异常的风险
可以用多个 except 分别处理不同错误:
try:
...
except LookupError as e:
...
except RuntimeError as e:
...
except IOError as e:
...
如果多个异常的处理逻辑相同,可以组合捕获:
try:
...
except (IOError, LookupError, RuntimeError) as e:
...
可以使用 Exception 捕获几乎所有普通异常,但这通常危险,因为它会隐藏真正的错误原因,使调试困难。总体原则是:只捕获你能合理处理的异常。不要捕获无法恢复的错误;如果只是为了记录诊断信息,应在记录后重新抛出。
assert 与 AssertionError
assert 是 Python 提供的内部检查语句。如果表达式不为真,就会抛出 AssertionError。
assert isinstance(10, int), 'Expected int'
assert 的用途主要是检查程序内部假设和不变量,而不是校验用户输入。来自 Web 表单、命令行参数、CSV 文件或环境变量的数据,应使用普通条件判断并显式抛出合适异常,或给出用户友好的错误消息。
相关主题:concepts/断言、程序不变量、契约式编程。
程序退出:SystemExit 与 sys.exit()
Python 的程序退出也通过异常机制实现。可以直接抛出 SystemExit:
raise SystemExit
raise SystemExit(1)
raise SystemExit('Usage: prog.py inputfile outputfile')
也可以使用 sys.exit():
import sys
sys.exit(1)
常见约定:退出码 0 表示成功,非零退出码表示错误。命令行参数检查中经常使用 SystemExit:
import sys
if len(sys.argv) != 3:
raise SystemExit(f'Usage: {sys.argv[0]} portfile pricefile')
这种写法比让程序因为 IndexError 崩溃更友好,因为它明确告诉用户应该如何调用脚本。相关主题见 command line arguments 和 summaries/05_Main_module。
异常处理与 Python 主模块
Python 没有固定的 main() 函数,但有主模块概念。为了避免导入模块时自动执行命令行逻辑,通常使用:
if __name__ == '__main__':
main()
更完整的命令行脚本模板是:
#!/usr/bin/env python3
import logging
log = logging.getLogger(__name__)
def main(argv):
if len(argv) != 3:
raise SystemExit(f'Usage: {argv[0]} portfile pricefile')
portfile = argv[1]
pricefile = argv[2]
...
if __name__ == '__main__':
import sys
logging.basicConfig(level=logging.WARNING)
main(sys.argv)
这种设计把异常处理、参数验证、日志配置和程序入口结合起来:可复用函数负责完成具体工作,并在无法完成时抛出异常;main(argv) 负责解释命令行参数和环境;参数错误可通过 SystemExit 给出友好提示;文件错误、数据错误可根据程序需求选择捕获、记录、报告、跳过或继续上抛。
这与 Python脚本与库的双重用途、Python程序入口、命令行工具设计 和 关注点分离 相关。
finally 与 with:资源管理
finally 用于指定无论是否发生异常都必须执行的代码:
lock = Lock()
lock.acquire()
try:
...
finally:
lock.release()
这常用于释放锁、文件、网络连接和临时资源。即使 try 块中发生异常,finally 中的清理代码仍会执行。
现代 Python 中,很多 try-finally 资源释放逻辑可以用 with 替代:
with open(filename) as f:
...
离开 with 上下文后,资源会自动释放。不过,with 只适用于实现了上下文管理协议的对象。上下文管理协议本身也属于对象协议的一部分,和 concepts/特殊方法、Python 特殊方法 有关。相关主题:资源管理。
自定义异常类型与异常层次结构
当内置异常不能准确表达程序语义时,可以定义新的异常类型。这一点正是“类和对象”章节的重要应用之一:学习 class 不只是为了模拟业务实体,也可以为了创建新的错误类型。用户自定义异常由类定义,并且应继承自 Exception:
class NetworkError(Exception):
pass
简单自定义异常通常是空类,类体中使用 pass 即可。然后可以在适当位置抛出:
raise NetworkError('connection failed')
自定义异常的优点包括:
- 让错误类型更贴近业务语义。
- 允许调用者只捕获自己关心的错误。
- 避免把所有失败都混在
RuntimeError、ValueError中。 - 有助于设计可扩展的库和应用程序。
- 让库使用者区分 Python 常见编程错误和库主动报告的使用错误。
- 把错误接口作为 API 的一部分,使调用者可以依赖稳定的异常类型。
自定义异常也可以组成继承层次:
class DataError(Exception):
pass
class MissingFieldError(DataError):
pass
class BadPriceError(DataError):
pass
调用者可以捕获具体错误,也可以统一捕获所有数据错误:
try:
price = parse_price(row)
except DataError as e:
print('数据错误:', e)
通过继承,可以既保留具体错误类型,又允许调用者用父类统一处理一组相关错误。这是 继承 和 继承与扩展性 在异常处理中的典型应用。相关主题见 库设计、API设计、concepts/类与对象、面向对象编程 和 summaries/04_Defining_exceptions。
异常与测试
异常处理不仅影响程序运行,也影响测试方式。Python 是动态语言,很多错误只有运行代码后才会暴露,因此测试是发现异常行为和验证错误处理的重要手段。相关内容见 summaries/01_Testing、Python测试、concepts/单元测试、Python unittest 和 concepts/pytest。
标准库 unittest 提供异常断言:
with self.assertRaises(TypeError):
s.shares = '100'
pytest 通常使用:
import pytest
with pytest.raises(TypeError):
s.shares = '100'
无论使用 unittest 还是 pytest,测试异常的核心思想都是一样的:错误输入不仅要“失败”,还要以预期的异常类型失败。这样可以避免代码悄悄接受坏数据,或抛出含糊、错误的异常。
try-except 与 if 检查
异常处理不是唯一的防御方式。有些问题可以在操作前用 if 明确判断。
例如,读取价格文件时,空行会产生空列表。如果使用 try-except:
prices = {}
for row in rows:
try:
prices[row[0]] = float(row[1])
except IndexError:
log.warning('跳过空行或缺失字段: %s', row)
except ValueError:
log.warning('跳过价格格式错误的行: %s', row)
也可以用 if 先过滤空行:
prices = {}
for row in rows:
if not row:
continue
prices[row[0]] = float(row[1])
两种方式的选择取决于问题性质:
- 对于“预期中、容易判断”的情况,例如空行、参数数量不足,用
if往往更清晰。 - 对于“转换时才知道是否成功”的情况,例如
float(row[1]),用try-except更自然。 - 对于真实数据处理,常常两者结合使用。
- 如果要保留诊断信息,优先考虑用
logging记录,而不是直接print()或静默忽略。
调试异常
异常处理和调试是互补关系:异常告诉你程序哪里失败;调试帮助你理解失败时程序处于什么状态。
崩溃后进入 REPL
运行脚本时可以加上 -i 选项:
python3 -i blah.py
如果程序崩溃,Python 不会立即退出,而是进入交互式解释器。这样可以在崩溃后继续检查解释器状态,例如查看变量值、调用函数、检查对象类型或复现局部问题。相关主题:repl、runtime state、debugging。
print() 调试与 repr()
print() 调试很常见:
def spam(x):
print('DEBUG:', repr(x))
...
调试输出时应优先使用 repr(),因为 repr() 显示的是对象更精确的开发者表示,而普通 print() 常常显示面向用户的友好形式。相关主题:print debugging、repr、debugging。
使用 Python 调试器
Python 3.7+ 可以在代码中使用 breakpoint() 手动进入调试器:
def some_function():
...
breakpoint()
...
旧版本或旧教程中常见写法是:
import pdb
pdb.set_trace()
也可以在调试器下运行整个程序:
python3 -m pdb someprogram.py
相关主题:pdb、breakpoints、call stack。
典型代码示例
示例 1:捕获数字转换错误
text = 'N/A'
try:
value = int(text)
except ValueError:
print('无法转换为整数:', text)
示例 2:用日志报告 CSV 坏行
import logging
log = logging.getLogger(__name__)
for rowno, row in enumerate(rows, start=1):
try:
converted = [func(val) for func, val in zip(types, row)]
except ValueError as e:
log.warning('Row %d: Could not convert %s', rowno, row)
log.debug('Row %d: Reason %s', rowno, e)
continue
这个例子体现了 summaries/02_Logging 的核心思想:异常处理负责恢复流程,日志记录负责可配置地输出诊断信息。
示例 3:字典查找中的异常与替代方案
try:
price = prices[name]
except KeyError:
price = 0.0
但对于“键可能不存在,而且有默认值”的情况,通常更推荐使用字典的 .get():
price = prices.get(name, 0.0)
这与 Python容器 和 Python数据结构 相关。不是所有不确定性都必须用异常处理;有时容器本身提供了更简洁的安全访问方式。
示例 4:定义并使用自定义异常
class DataError(Exception):
pass
class MissingFieldError(DataError):
pass
class BadPriceError(DataError):
pass
def parse_price(row):
if len(row) < 2:
raise MissingFieldError('missing price field')
try:
return float(row[1])
except ValueError as e:
raise BadPriceError(f'bad price: {row[1]}') from e
示例 5:用 unittest 测试异常
import unittest
import stock
class TestStock(unittest.TestCase):
def test_bad_shares(self):
s = stock.Stock('GOOG', 100, 490.1)
with self.assertRaises(TypeError):
s.shares = '100'
if __name__ == '__main__':
unittest.main()
这个例子验证 Stock 类在收到非法属性赋值时是否按预期抛出 TypeError。它把异常处理和 Python unittest、异常测试、concepts/类与对象 连接起来。
异常处理最佳实践
异常处理最重要的原则是:不要随意捕获异常。让程序快速、明确地失败,即 “fail fast and loud”。只有当你确实能够恢复并继续运行时,才捕获异常。
更具体地说:
- 不要为了“看起来健壮”而吞掉所有异常。
- 捕获异常时,应尽量捕获具体类型。
- 如果捕获所有异常,应提供查看或报告错误原因的机制。
- 捕获异常后,要么恢复,要么记录并重新抛出。
- 对外部输入中的坏数据,可以报告警告并跳过。
- 报告诊断信息时,优先考虑
logging,不要把print()写死在库代码中。 - 不要在库模块中配置全局 logging;日志配置应由主程序决定。
- 对调用者传入的无意义参数组合,应主动抛出异常。
- 对命令行参数错误,应给出用法说明并通过
SystemExit退出。 - 对普通类型错误,不一定要写大量手工检查;让 traceback 暴露问题通常更利于调试。
- 对内部不变量和开发期假设,可以使用
assert。 - 不要用
assert替代用户输入验证。 - 在脚本中把主流程放入
main(argv),便于交互测试和错误处理。 - 当错误属于明确领域语义时,考虑定义自定义异常类。
- 在库代码中,自定义异常可以让调用者更精确地捕获和恢复。
- 自定义异常通常继承自
Exception,简单情况下只需pass。 - 一组相关错误可以定义共同父类,形成异常层次结构。
- 对预期会失败的代码路径,应写测试验证它抛出正确异常。
- 在设计类库时,把异常类型视为 API 的一部分,避免随意更改。
常见错误
1. 捕获了错误类型不匹配的异常
try:
shares = int(fields[1])
except TypeError:
print('bad data')
如果实际发生的是 ValueError,上面的 except TypeError 不会捕获它。
2. 只处理数字错误,却忽略字段缺失
try:
price = float(row[1])
except ValueError:
print('bad price')
如果 row 是空列表 [],实际发生的是 IndexError,而不是 ValueError。
3. 捕获异常后什么也不做
try:
shares = int(fields[1])
except ValueError:
pass
这会隐藏错误,使调试更困难。通常至少应该记录警告,包含出错的行、行号或失败原因。
4. 在库代码中直接 print() 诊断信息
更好的方式是使用 logger:
except ValueError as e:
log.warning('bad row: %s', row)
log.debug('reason: %s', e)
5. 在库模块中调用 logging.basicConfig()
库模块应发出日志,不应配置全局日志行为。basicConfig() 通常应放在主程序启动入口。
6. 把所有异常都笼统吞掉
try:
...
except Exception:
pass
这种写法会掩盖真正的程序错误,不利于调试。
7. 滥用 assert 检查用户输入
assert len(sys.argv) == 3, 'need two filenames'
更好的方式是:
if len(sys.argv) != 3:
raise SystemExit(f'Usage: {sys.argv[0]} portfile pricefile')
8. 只看 traceback 第一行,不看最后一行
traceback 顶部显示调用开始的位置,最后一行通常才是异常类型和直接原因。调试时应先看最后一行,再沿调用栈向上追踪。
9. 自定义异常没有继承 Exception
自定义异常应继承自 Exception 或更具体的异常基类:
class FormatError(Exception):
pass
10. 自定义异常层次过于混乱
如果库中每个错误都随意继承不同内置异常,调用者很难统一捕获。更好的方式是为某个领域定义共同父类,例如 DataError,再定义具体子类。
调试提示
- 先读 traceback 的最后一行,了解异常类型和错误信息。
- 再向上查看 traceback,找到出错的代码行。
- 如果错误来自函数调用,顺着 traceback 查看调用链。
- 使用
python3 -i script.py在崩溃后进入 REPL,检查变量和对象状态。 - 使用
repr()输出调试值,避免被友好显示误导。 - 使用
breakpoint()在可疑位置进入调试器。 - 使用
python3 -m pdb program.py从程序开始处进入调试器。 - 在
pdb中用where查看调用栈,用up/down切换栈帧,用args查看当前函数参数。 - 对数据解析错误,查看原始输入行,例如
line、row或fields。 - 如果看到
IndexError,检查列表是否为空、字段数量是否不足,或命令行参数是否缺失。 - 如果看到
ValueError,检查字符串是否能转换为目标类型。 - 如果看到
KeyError,检查字典中是否存在该键、环境变量是否存在,或考虑使用.get()。 - 如果看到
TypeError,检查参与操作的对象类型是否兼容。 - 如果看到
AttributeError,检查对象真实类型,以及该对象是否具有目标属性或方法。 - 如果看到
AssertionError,检查失败的断言表达式,以及它是否真的是内部不变量。 - 不确定可能发生什么异常时,可以先让程序崩溃一次,观察 traceback,再添加合适的
try-except。 - 捕获异常后如果无法真正恢复,考虑记录信息后重新
raise。 - 对重要异常路径编写单元测试,例如用
assertRaises()或pytest.raises()。
推荐练习
- 在交互式解释器中运行
int('N/A'),观察ValueError和 traceback。 - 运行
add(3, '4'),观察TypeError,理解 Python 的运行时错误模型。 - 尝试
assert isinstance('100', int), 'Expected int',观察AssertionError。 - 编写一个函数,接收字符串并尝试转换为整数;如果失败,打印错误消息。
- 修改投资组合成本计算程序
pcost.py,让它在遇到缺失字段或非法数字时打印警告并继续处理。 - 将文件处理代码封装为
portfolio_cost(filename),然后在交互模式中测试不同输入文件。 - 编写
read_prices(filename),把Data/prices.csv读入字典,并正确处理空行。 - 分别用
if not row: continue和try-except IndexError处理空行,比较哪一种更清晰。 - 尝试访问不存在的字典键,例如
prices['SCOX'],观察KeyError,然后改用prices.get('SCOX', 0.0)。 - 尝试使用
raise RuntimeError('message')主动抛出异常,并观察未捕获异常的输出格式。 - 修改
parse_csv(),当select与has_headers=False同时出现时抛出RuntimeError或自定义异常。 - 将数据解析中的
print()改为log.warning()和log.debug()。 - 用
python3 -i script.py运行一个会崩溃的脚本,崩溃后在 REPL 中检查变量。 - 在可疑代码前加入
print('DEBUG:', repr(value))。 - 在函数中加入
breakpoint(),练习where、up、down、args、step和continue。 - 用
with open(filename) as f:重写文件读取代码,理解上下文管理器如何自动关闭文件。 - 为
report.py添加main(argv),在参数数量错误时使用raise SystemExit('Usage: ...')。 - 定义一个自定义异常类
DataError(Exception),在数据解析失败时抛出它,并在调用者处捕获。 - 定义一个异常继承层次,例如
DataError、MissingFieldError、BadPriceError,比较捕获父类和捕获子类的区别。 - 定义一个
FormatError(Exception),修改create_formatter(name),当用户传入未知格式名如'xls'时抛出。 - 为
Stock类编写unittest测试,验证s.shares = '100'会抛出TypeError。 - 用
pytest.raises()重写同一个异常测试,比较unittest与pytest的风格差异。
关联知识点
- summaries/02_Hello_world:最早接触 Python 程序运行和错误反馈。
- summaries/06_Files:文件读取是异常处理的重要应用场景。
- summaries/07_Functions:介绍函数、标准库、异常捕获和主动抛出异常。
- summaries/02_Containers:在读取价格 CSV 时展示了空行导致崩溃的问题,并讨论用
try-except或if处理坏数据。 - summaries/02_More_functions:函数参数设计与
parse_csv()的可选参数为错误检查提供背景。 - summaries/03_Error_checking:系统讲解异常抛出、捕获、传播、重新抛出、
finally、with以及parse_csv()错误处理练习。 - summaries/05_Main_module:说明主模块、
main(argv)、命令行参数、SystemExit、sys.exit()和脚本入口设计。 - summaries/00_Overview:课程结构中“类和对象”部分引出
class、继承、特殊方法和定义新异常。 - summaries/04_Classes_objects__00_Overview:第 4 章总览,说明类与对象章节将介绍
class、继承、特殊方法、动态属性查找和定义新异常。 - summaries/04_Defining_exceptions:专门说明自定义异常由类定义、通常继承自
Exception、可用pass编写空类,并可形成异常层次结构。 - summaries/01_Testing:介绍
assert、AssertionError、unittest.assertRaises()和用测试验证异常行为。 - summaries/02_Logging:说明如何在异常处理中用
logging替代print()或静默忽略,并通过日志级别控制诊断输出。 - summaries/03_Debugging:说明阅读 traceback、崩溃后进入 REPL、
repr()调试输出、breakpoint()和pdb调试器。 - python functions:函数内部可能产生异常,也可以通过异常报告错误。
- file processing:文件内容不可靠时,需要异常处理增强健壮性。
- csv processing:CSV 解析常与数据格式错误处理结合使用。
- python standard library:标准库模块如
csv、sys、os、unittest、logging、pdb能减少手写解析、测试、诊断和调试成本。 - debugging:traceback、REPL、
print(repr(...))、断点和 debug 日志都是调试异常的重要线索。 - traceback:未捕获异常时显示的调用栈和错误原因。
- pdb:Python 内置交互式调试器。
- repl:交互式解释器可用于崩溃后检查状态。
- print debugging:通过输出变量和执行路径辅助定位错误。
- repr:对象精确表示,适合调试输出。
- breakpoints:控制程序在指定位置暂停,便于检查状态。
- call stack:traceback 和调试器都依赖调用栈定位错误路径。
- robust programming:异常处理是编写健壮程序的重要手段。
- command line arguments:命令行输入可能缺失或非法,也需要错误处理。
- Python容器:列表、字典和集合的访问方式不同,可能触发不同类型的异常。
- Python数据结构:合理的数据结构设计可以减少错误,并让异常处理更有针对性。
- 数据清洗:空行、坏行和非法字段常需要在导入阶段清理或跳过。
- 健壮文件读取:结合条件检查、异常捕获、标准库解析器和日志记录读取不可靠文件。
- 资源管理:
finally、with和上下文管理器用于安全释放资源。 - Python测试:动态语言中通过测试验证程序行为的重要性。
- 软件测试:测试用于发现错误、验证行为并防止回归。
- concepts/单元测试:异常路径也应作为单元测试的一部分。
- Python unittest:
TestCase、assertEqual()、assertRaises()等测试工具。 - concepts/pytest:第三方测试框架,可用简洁断言和
pytest.raises()测试异常。 - 异常测试:验证代码在错误输入下是否抛出预期异常。
- concepts/断言:
assert用于内部检查,失败时抛出AssertionError。 - 契约式编程:通过断言表达函数或类的接口约定。
- 程序不变量:理论上应始终为真的条件,适合用断言检查。
- Python程序入口:
if __name__ == '__main__'与main(argv)决定异常处理、日志配置和程序退出的边界。 - 命令行工具设计:命令行工具需要清晰的参数错误、退出码和用户提示。
- 环境变量:缺失环境变量可能触发
KeyError,可用异常或默认值处理。 - Python进程环境:环境变量和子进程继承会影响脚本运行。
- 面向对象编程:异常是对象,自定义异常依赖类机制。
- concepts/类与对象:异常类和异常实例体现了 Python 的对象模型。
- Python 类与对象:
class语句不仅能定义业务对象,也能定义异常类型。 - 继承:自定义异常通常继承自
Exception或其他异常基类,也可形成异常层次结构。 - 继承与扩展性:异常层次结构让调用者可以在通用错误和具体错误之间选择捕获粒度。
- concepts/特殊方法:异常对象的字符串显示、上下文管理协议等与对象协议相关。
- Python 特殊方法:类通过特殊方法接入 Python 语言机制,资源管理和对象显示均与异常诊断相关。
- 动态属性查找:类属性和实例属性查找机制是理解异常对象行为和对象模型的基础。
- 库设计:库应定义清晰、稳定、可捕获的异常接口,并避免替调用者配置日志。
- API设计:专用异常能让 API 的错误语义更明确。
- Python日志记录:日志为异常处理提供可配置的诊断输出机制。
- 程序诊断:异常、traceback、日志、调试器和测试共同构成程序诊断体系。
- 关注点分离:模块负责发出异常和日志,主程序负责配置策略。
- 模块化程序设计:按模块命名的 logger 让大型程序能够独立控制诊断输出。
对应教材来源
来源:Practical Python Programming, https://github.com/dabeaz-course/practical-python
相关章节:
- summaries/02_Hello_world
- summaries/06_Files
- summaries/07_Functions
- summaries/02_Containers
- summaries/02_More_functions
- summaries/03_Error_checking
- summaries/05_Main_module
- summaries/00_Overview
- summaries/04_Classes_objects__00_Overview
- summaries/04_Defining_exceptions
- summaries/01_Testing
- summaries/02_Logging
- summaries/03_Debugging
See also: summaries/06_Design_discussion
See also: summaries/02_Inheritance
See also: summaries/02_Classes_encapsulation
See also: summaries/01_Iteration_protocol
See also: summaries/01_Variable_arguments
See also: summaries/03_Returning_functions
See also: summaries/02_Third_party