Files

9.3 KiB
Raw Permalink Blame History

doc_type, full_text
doc_type full_text
short sources/03_Error_checking.md

03_Error_checking 总结

本文补充说明 Python 中的错误检查与异常处理机制,重点包括:Python 的运行时错误模型、异常的抛出与捕获、异常传播、捕获范围控制、重新抛出、finally 与 with 的资源管理,以及在 parse_csv() 中处理脏数据的实践练习。相关主题可归入 Python异常处理、错误处理最佳实践、资源管理 与 CSV解析。

核心观点

Python 通常不会在函数调用前检查参数类型或取值。函数只要接收到的数据能支持函数体中的操作,就会运行;否则错误会在运行时以异常形式出现。

例如:

def add(x, y):
    return x + y

add(3, 4)               # 7
add('Hello', 'World')   # 'HelloWorld'
add('3', '4')           # '34'
add(3, '4')             # TypeError

这体现了 Python 的动态类型特征:代码是否正确通常通过运行和测试来验证。因此,本文强调测试在 Python 程序可靠性中的重要性,相关内容可连接到 Python测试。

异常的基本用法

异常用于表示程序中的错误或非正常情况。

抛出异常

使用 raise 主动抛出异常:

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

捕获异常

使用 try-except 捕获异常:

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

except RuntimeError as e 中的 e 是异常实例,保存了具体错误信息。虽然它是对象,但打印时通常表现得像字符串。

异常传播机制

异常会沿调用栈向上传播,直到遇到第一个匹配的 except 块。

如果某个函数中抛出了 RuntimeError,调用它的函数没有处理,异常会继续向上传递;一旦被某层 except RuntimeError 捕获,传播就停止,不会继续传给更外层调用者。

这说明异常处理具有“最近匹配处理者优先”的特性。捕获后,程序会从整个 try-except 结构之后的第一条语句继续执行。

内置异常

Python 提供了多种内置异常类型,异常名称通常暗示了错误原因。例如:

  • TypeError:类型不支持某操作
  • ValueError:值的格式或内容不合法
  • KeyError:字典中找不到指定键
  • IndexError:序列索引越界
  • ImportError:模块导入失败
  • RuntimeError:一般运行时错误
  • SyntaxError:语法错误
  • KeyboardInterrupt:用户中断程序

本文列举的异常包括:

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

完整列表应参考 Python 官方文档。

捕获多个异常

可以用多个 except 分别处理不同错误:

try:
    ...
except LookupError as e:
    ...
except RuntimeError as e:
    ...
except IOError as e:
    ...
except KeyboardInterrupt as e:
    ...

如果多个异常的处理逻辑相同,可以将它们组合:

try:
    ...
except (IOError, LookupError, RuntimeError) as e:
    ...

这属于 Python异常处理 中的异常分类处理策略。

捕获所有异常的风险

可以使用 Exception 捕获几乎所有普通异常:

try:
    ...
except Exception:
    print('An error occurred')

但这通常是危险做法,因为它会隐藏真正的错误原因,使调试困难。例如:

try:
    go_do_something()
except Exception:
    print('Computer says no')

这种写法会吞掉所有异常,包括意料之外的问题,例如依赖模块未安装、代码逻辑错误等。

更好的做法是至少打印异常原因:

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

但总体原则是:只捕获你能合理处理的异常。不要捕获无法恢复的错误。相关主题可归入 错误处理最佳实践。

重新抛出异常

如果需要记录日志或执行某些补救动作,但仍希望调用者知道错误,可以在 except 中使用裸 raise 重新抛出当前异常:

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

这种模式适合“记录后继续上抛”,避免错误被静默吞掉。

异常处理最佳实践

本文给出的核心建议是:

  • 不要随意捕获异常。
  • 让程序快速、明确地失败,即 “fail fast and loud”。
  • 只有当你确实能够恢复并继续运行时,才捕获异常。
  • 如果捕获所有异常,应提供查看或报告错误原因的机制。
  • 对于“不应该发生”的无意义状态,可以主动检查并抛出异常。

这一区分很重要:

  • 不需要检查所有参数类型,让错误自然暴露即可。
  • 但如果参数组合本身在语义上无效,应主动报错。

例如,parse_csv() 中如果指定 select,就必须有列标题;因此当 select 与 has_headers=False 同时出现时,应抛出异常。

finally:保证执行的清理逻辑

finally 用于指定无论是否发生异常都必须执行的代码:

lock = Lock()
lock.acquire()
try:
    ...
finally:
    lock.release()

它常用于释放资源,例如:

  • 锁
  • 文件
  • 网络连接
  • 临时资源

这属于 资源管理 的基础模式。

with:现代资源管理方式

现代 Python 中,很多 try-finally 资源释放逻辑可以用 with 替代:

lock = Lock()
with lock:
    ...

离开 with 上下文后,资源会自动释放。

文件操作也是典型例子:

with open(filename) as f:
    ...

with 定义了资源的使用上下文。当执行离开该上下文时,资源会被清理。不过,with 只适用于实现了上下文管理协议的对象。

练习 3.8:在 parse_csv() 中主动抛出异常

此前的 parse_csv() 支持用户通过 select 参数选择列,但该功能依赖 CSV 文件具有列标题。

因此,如果同时传入:

select=['name', 'price']
has_headers=False

就应抛出异常:

raise RuntimeError("select argument requires column headers")

示例:

parse_csv('Data/prices.csv', select=['name','price'], has_headers=False)

应得到:

RuntimeError: select argument requires column headers

该练习强调:不必检查所有输入类型,例如文件名是否为字符串、types 是否为列表等;这些错误可以让程序自然失败。但对于语义上自相矛盾的参数组合,应主动检查并报错。

练习 3.9:捕获脏数据导致的转换错误

现实中的 CSV 文件可能包含缺失、损坏或格式不正确的数据。例如 Data/missing.csv 中某些行的 shares 字段为空,转换为 int 时会抛出:

ValueError: invalid literal for int() with base 10: ''

要求修改 parse_csv():

  • 在记录创建期间捕获 ValueError
  • 对无法转换的行打印警告
  • 警告包含行号
  • 警告包含失败原因
  • 跳过错误行,继续处理后续数据

示例输出:

Row 4: Couldn't convert ['MSFT', '', '51.23']
Row 4: Reason invalid literal for int() with base 10: ''
Row 7: Couldn't convert ['IBM', '', '70.44']
Row 7: Reason invalid literal for int() with base 10: ''

最终返回的 portfolio 中只包含成功转换的记录。

该练习展示了异常处理的合理用途:输入数据不可靠,但程序可以跳过坏记录并继续工作。

练习 3.10:允许用户静默错误

继续修改 parse_csv(),增加类似 silence_errors=True 的参数,使用户可以主动关闭错误提示:

portfolio = parse_csv(
    'Data/missing.csv',
    types=[str, int, float],
    silence_errors=True
)

此时坏记录仍会被跳过,但不打印警告信息。

本文强调:一般不应该静默忽略错误。更好的默认行为是报告问题,并允许用户显式选择是否静默。

关键收获

  • Python 不会预先验证函数参数类型,错误通常在运行时出现。
  • 异常通过 raise 抛出,通过 try-except 捕获。
  • 异常会传播到第一个匹配的 except。
  • 捕获过宽的异常会隐藏问题,降低可调试性。
  • 最好只捕获能够实际处理和恢复的异常。
  • 使用裸 raise 可以在记录错误后重新抛出异常。
  • finally 保证清理代码总会运行。
  • with 是现代 Python 中管理文件、锁等资源的推荐方式。
  • 对无意义的参数组合应主动抛出异常。
  • 对现实输入中的脏数据,可以捕获特定异常、报告问题并继续处理。

相关页面建议

  • Python异常处理:异常抛出、捕获、传播、多异常处理与重新抛出。
  • 错误处理最佳实践:何时捕获异常、何时快速失败、如何避免吞掉错误。
  • 资源管理:finally、with、上下文管理器与资源释放。
  • CSV解析:parse_csv() 的参数设计、类型转换、列选择与错误处理。
  • Python测试:动态语言中通过测试验证程序行为的重要性。