异常处理

前言

  • 这篇覆盖 Python 错误处理的一整套:try / except / else / finally 的执行规则、raise 与异常链、3.11 的 except*、自定义异常怎么设计、assert 的边界,以及 warnings 和异常的关系。
  • 原笔记里那张「Python 标准异常」表是 Python 2 时代的抄本(StandardError、OverflowWarning 已经不存在,IOError / EnvironmentError / WindowsError 现在只是 OSError 的别名),这里换成按 3.14 实测重画的速查表。
  • 所有示例在本机 Python 3.14.10 上实跑过,输出原样贴在代码块下面;编译期的 SyntaxWarning 也保留在输出里,那本身就是知识点。

异常从哪来:3.14 的继承树

  • 先看根上分了什么

    BaseException 之下不只有 Exception:SystemExit、KeyboardInterrupt、GeneratorExit 是它的兄弟。这条决定了「except Exception 到底接不住什么」。

    1
    2
    3
    4
    5
    6
    7
    8
    import builtins

    print('BaseException 的直接子类:', sorted(c.__name__ for c in BaseException.__subclasses__()))
    print('Exception 的直接子类数:', len(Exception.__subclasses__()))
    print('OSError 的直接子类:', sorted(c.__name__ for c in OSError.__subclasses__()))
    print('ArithmeticError 的直接子类:', sorted(c.__name__ for c in ArithmeticError.__subclasses__()))
    print('LookupError 的直接子类:', sorted(c.__name__ for c in LookupError.__subclasses__()))
    print('Warning 的直接子类:', sorted(c.__name__ for c in Warning.__subclasses__()))

    输出:

    1
    2
    3
    4
    5
    6
    BaseException 的直接子类: ['BaseExceptionGroup', 'Exception', 'GeneratorExit', 'KeyboardInterrupt', 'SystemExit']
    Exception 的直接子类数: 21
    OSError 的直接子类: ['BlockingIOError', 'ChildProcessError', 'ConnectionError', 'FileExistsError', 'FileNotFoundError', 'InterruptedError', 'IsADirectoryError', 'NotADirectoryError', 'PermissionError', 'ProcessLookupError', 'TimeoutError', 'UnsupportedOperation']
    ArithmeticError 的直接子类: ['FloatingPointError', 'OverflowError', 'ZeroDivisionError']
    LookupError 的直接子类: ['CodecRegistryError', 'IndexError', 'KeyError']
    Warning 的直接子类: ['BytesWarning', 'DeprecationWarning', 'EncodingWarning', 'FutureWarning', 'ImportWarning', 'PendingDeprecationWarning', 'ResourceWarning', 'RuntimeWarning', 'SyntaxWarning', 'UnicodeWarning', 'UserWarning']
  • 老表里的名字现在还在吗

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import builtins

    print('IOError is OSError:', IOError is OSError)
    print('builtins.EnvironmentError is OSError:', builtins.EnvironmentError is OSError)
    print('builtins.WindowsError is OSError:', builtins.WindowsError is OSError)
    print('StandardError 还在吗:', hasattr(builtins, 'StandardError'))
    print('OverflowWarning 还在吗:', hasattr(builtins, 'OverflowWarning'))

    try:
    open('C:/no/such/dir/file.txt')
    except EnvironmentError as e: # 别名照样能接住 OSError 家族
    print('接住了:', type(e).__name__, '| errno:', e.errno)

    输出:

    1
    2
    3
    4
    5
    6
    IOError is OSError: True
    builtins.EnvironmentError is OSError: True
    builtins.WindowsError is OSError: True
    StandardError 还在吗: False
    OverflowWarning 还在吗: False
    接住了: FileNotFoundError | errno: 2

    三条结论:

    • StandardError、OverflowWarning 在 Python 3 里根本不存在。
    • IOError、EnvironmentError、WindowsError 三个名字都还在,但它们就是 OSError 本身(is OSError 为真),是留给老代码的兼容别名,新代码一律写 OSError。
    • os.WindowsError 已经没有了。
  • except Exception 接不住什么

    1
    2
    3
    4
    5
    6
    7
    8
    9
    print('KeyboardInterrupt 是 Exception 子类吗:', issubclass(KeyboardInterrupt, Exception))
    print('SystemExit 是 Exception 子类吗:', issubclass(SystemExit, Exception))

    try:
    raise KeyboardInterrupt('用户按了 Ctrl+C')
    except Exception:
    print('被 except Exception 接住了')
    except BaseException as e:
    print('只有 except BaseException 接得住:', type(e).__name__, e)

    输出:

    1
    2
    3
    KeyboardInterrupt 是 Exception 子类吗: False
    SystemExit 是 Exception 子类吗: False
    只有 except BaseException 接得住: KeyboardInterrupt 用户按了 Ctrl+C

    所以「捕获所有异常」要分两种说法:接住除 Ctrl+C / 退出之外的全部,写 except Exception;真要做到一个不落,只能 except BaseException——但那会顺手把 sys.exit() 和用户中断也吞掉,除非是在写顶层 CLI,否则别这么干。

  • 常用异常速查(第三列由解释器核对)

    异常 什么时候抛 直接父类
    BaseException 所有异常的根;只有想连 Ctrl+C 一起接住时才用它 (根)
    Exception 常规错误的基类,业务代码的兜底捕获点 BaseException
    ArithmeticError 数值计算错误的基类 Exception
    ZeroDivisionError 除法 / 取模的第二个参数为 0 ArithmeticError
    OverflowError 运算结果超出可表示范围 ArithmeticError
    AssertionError assert 的条件为假 Exception
    AttributeError 访问对象上不存在的属性或方法 Exception
    EOFError 读输入时提前遇到结束标记 Exception
    ImportError 导入模块失败,或从模块里取不到名字 Exception
    ModuleNotFoundError import 的模块找不到 ImportError
    IndexError 序列下标越界 LookupError
    KeyError 字典里没有这个键 LookupError
    MemoryError 内存不够分配 Exception
    NameError 用到没定义的名字 Exception
    UnboundLocalError 局部变量在赋值前被读取 NameError
    OSError 文件 / 系统 / 网络错误的基类,errno 里有原因 Exception
    FileNotFoundError open 一个不存在的路径 OSError
    PermissionError 权限不足 OSError
    TimeoutError 操作超时 OSError
    ConnectionError 连接建立或传输失败 OSError
    RecursionError 递归层数超过 sys.getrecursionlimit() RuntimeError
    RuntimeError 不属于其它类别的运行时错误 Exception
    NotImplementedError 抽象方法留空时抛它,提示子类必须实现 RuntimeError
    SyntaxError 源码语法不合法(编译期就报,try 接不住) Exception
    IndentationError 缩进不符合要求 SyntaxError
    TabError Tab 与空格混用 IndentationError
    TypeError 对不合适类型做运算,或参数个数 / 关键字不对 Exception
    ValueError 类型对但值不合法,如 int("abc") Exception
    UnicodeError 编解码出错 ValueError
    StopIteration 迭代器耗尽;next(it, 默认值) 可以免抛 Exception
    StopAsyncIteration 异步迭代器耗尽 Exception
    GeneratorExit 关闭生成器时注入的异常 BaseException
    KeyboardInterrupt 用户按 Ctrl+C,不是 Exception 的子类 BaseException
    SystemExit sys.exit() 抛出,同样不是 Exception 的子类 BaseException
    ExceptionGroup 一次抛出多个异常(3.11+,配 except*) BaseExceptionGroup
    Warning 所有警告的基类 Exception

try / except / else / finally

  • 四段各自什么时候跑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try:
    n = int('42') # 这行不会抛异常
    print('转换结果:', n)
    except ValueError as e: # as 后面拿到异常对象本身
    print('捕获到异常:', type(e).__name__, e)
    else:
    print('try 里没抛异常才会走这里')
    finally:
    print('无论有没有异常都会走这里')

    输出:

    1
    2
    3
    转换结果: 42
    try 里没抛异常才会走这里
    无论有没有异常都会走这里

    规则:

    • else 只在 try 没抛异常时执行,位置必须在 except 之后。
    • finally 无论有没有异常、有没有 return 都会执行。
    • try 里抛出却没被任何 except 接住的异常,会在 finally 之后继续往外传。
  • 多个 except 与「分组」

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def parse(text):
    try:
    return int(text)
    except ValueError: # 第一个匹配的 except 生效
    return '不是整数'
    except (TypeError, IndexError): # 多个类型可以并到一个块里
    return '类型或下标有问题'

    for s in ['42', 'abc', None]:
    print(repr(s), '->', parse(s))

    输出:

    1
    2
    3
    '42' -> 42
    'abc' -> 不是整数
    None -> 类型或下标有问题

    一个 except 接多个类型要写成元组 except (TypeError, IndexError):。写成 except TypeError, IndexError: 是 Python 2 语法,Python 3 直接 SyntaxError。

  • 顺序很重要:子类写在父类前面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def catch(value):
    try:
    raise value
    except LookupError: # 父类写在前面,子类就永远轮不到
    return 'LookupError'
    except KeyError:
    return 'KeyError'

    print('父类在前时:', catch(KeyError('k')))

    def catch_fixed(value):
    try:
    raise value
    except KeyError: # 正确顺序:子类在前
    return 'KeyError'
    except LookupError:
    return 'LookupError'

    print('子类在前时:', catch_fixed(KeyError('k')))

    输出:

    1
    2
    父类在前时: LookupError
    子类在前时: KeyError

    except 是从上到下第一个匹配就执行,父类写在前面的话,后面的子类分支就成了死代码。

  • 异常对象上能拿到什么

    1
    2
    3
    4
    5
    6
    7
    try:
    int('abc')
    except ValueError as e:
    print('type:', type(e).__name__)
    print('str(e):', e)
    print('repr(e):', repr(e))
    print('args:', e.args)

    输出:

    1
    2
    3
    4
    type: ValueError
    str(e): invalid literal for int() with base 10: 'abc'
    repr(e): ValueError("invalid literal for int() with base 10: 'abc'")
    args: ("invalid literal for int() with base 10: 'abc'",)

    str(e) 给人看,repr(e) 带类型名、适合写日志,e.args 是构造参数元组。自定义异常带多个字段时,args 里就不止一项。

  • 要完整回溯就借 traceback

    1
    2
    3
    4
    5
    6
    7
    8
    import traceback

    try:
    1 / 0
    except ZeroDivisionError:
    tb = traceback.format_exc() # 拿到完整回溯字符串,适合写日志
    print('回溯行数:', len(tb.strip().splitlines()))
    print('最后一行:', tb.strip().splitlines()[-1])

    输出:

    1
    2
    回溯行数: 5
    最后一行: ZeroDivisionError: division by zero

    traceback.format_exc() 返回字符串,方便塞进日志。只是想在原样抛出的同时记一笔,用裸 raise 更省事(见下一节)。

  • 坑:finally 里的 return 会吞掉异常

    1
    2
    3
    4
    5
    6
    7
    def bad():
    try:
    raise ValueError('很重要的错误')
    finally:
    return 'finally 里的 return' # 3.14 会在这里给一条 SyntaxWarning

    print('调用结果:', bad())

    输出:

    1
    2
    <string>:5: SyntaxWarning: 'return' in a 'finally' block
    调用结果: finally 里的 return

    ValueError 就这么消失了。3.14 已经会对这种写法发 SyntaxWarning(输出的第一行),别写这种代码:finally 里只放清理动作。

  • 处理完还想往上抛:裸 raise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def read(path):
    try:
    return int(path)
    except ValueError:
    print(' 先记一笔日志')
    raise # 裸 raise:原样把当前异常抛出去

    try:
    read('abc')
    except ValueError as e:
    print(' 外层收到:', repr(e))

    输出:

    1
    2
    先记一笔日志
    外层收到: ValueError("invalid literal for int() with base 10: 'abc'")

    在 except 块里写不带参数的 raise,抛出的是原来那个异常对象,回溯链完整保留。

raise 与异常链

  • 显式链:raise X from Y

    在 except 里换成另一种异常往外抛时,用 from 把起因挂上去,读日志的人才看得到链条。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try:
    try:
    int('abc')
    except ValueError as e:
    raise RuntimeError('配置项解析失败') from e # 显式链
    except RuntimeError as out:
    print('__cause__:', type(out.__cause__).__name__)
    print('__context__:', type(out.__context__).__name__)
    print('__suppress_context__:', out.__suppress_context__)

    输出:

    1
    2
    3
    __cause__: ValueError
    __context__: ValueError
    __suppress_context__: True
  • 隐式链与 from None

    不写 from 时,解释器仍会把当时正在处理的异常记到 __context__ 上,只是 __cause__ 为空;from None 是主动掐断展示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    try:
    try:
    int('abc')
    except ValueError:
    raise RuntimeError('配置项解析失败') # 隐式链:不写 from
    except RuntimeError as out:
    print('__cause__:', out.__cause__)
    print('__context__:', type(out.__context__).__name__)

    try:
    try:
    int('abc')
    except ValueError:
    raise RuntimeError('配置项解析失败') from None # 掐断上下文展示
    except RuntimeError as out:
    print('from None 之后 __suppress_context__:', out.__suppress_context__)

    输出:

    1
    2
    3
    __cause__: None
    __context__: ValueError
    from None 之后 __suppress_context__: True

    注意最后一行:from None 之后 __context__ 其实还挂在对象上,只是打印回溯时不再显示。它是「别让无关异常干扰排障」的手段,不是删除。

  • 两种链在报错文本上的区别

    同一份逻辑,一个不写 from、一个写 from e,未捕获时解释器给的提示语不同。不写 from:

    1
    2
    3
    4
    try:
    int('abc')
    except ValueError:
    raise RuntimeError('配置项解析失败')

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Traceback (most recent call last):
    File "<string>", line 2, in <module>
    int('abc')
    ~~~^^^^^^^
    ValueError: invalid literal for int() with base 10: 'abc'

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "<string>", line 4, in <module>
    raise RuntimeError('配置项解析失败')
    RuntimeError: 配置项解析失败

    写 from e:

    1
    2
    3
    4
    try:
    int('abc')
    except ValueError as e:
    raise RuntimeError('配置项解析失败') from e

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Traceback (most recent call last):
    File "<string>", line 2, in <module>
    int('abc')
    ~~~^^^^^^^
    ValueError: invalid literal for int() with base 10: 'abc'

    The above exception was the direct cause of the following exception:

    Traceback (most recent call last):
    File "<string>", line 4, in <module>
    raise RuntimeError('配置项解析失败') from e
    RuntimeError: 配置项解析失败

    During handling of the above exception, another exception occurred 表示「处理 A 的过程中又冒了 B」,通常是意外;The above exception was the direct cause of the following exception 表示这是有意转换。看到前者就该去查是不是漏写了 from。

except*:一次处理多个异常

3.11 引入(PEP 654)。并发场景里可能同时冒出多个异常,ExceptionGroup 把它们打包,except* 按类型分别摘走——注意每个匹配的分支都会执行,不是第一个匹配就结束。

1
2
3
4
5
6
7
8
9
10
try:
try:
raise ExceptionGroup('批量任务有 3 个子任务失败',
[ValueError('第 2 行'), TypeError('第 3 行'), KeyError('第 4 行')])
except* ValueError as eg: # 只摘走 ValueError
print('ValueError 分支:', eg.exceptions)
except* TypeError as eg: # 再摘走 TypeError
print('TypeError 分支:', eg.exceptions)
except* KeyError as eg: # 上面没接住的,外层继续摘
print('漏到外层的 KeyError:', eg.exceptions)

输出:

1
2
3
ValueError 分支: (ValueError('第 2 行'),)
TypeError 分支: (TypeError('第 3 行'),)
漏到外层的 KeyError: (KeyError('第 4 行'),)
  • except* 后面跟类型,拿到的 eg 是一个异常组,.exceptions 是被摘走的那批。
  • 上面没被任何 except* 摘走的成员会打包继续往外抛,外层还能再用 except* 接(示例里的 KeyError 就是这条路径)。
  • except 匹配的是整个组,except* 拆组匹配成员,两者语义不同,别混用着写。
  • asyncio.TaskGroup、concurrent 相关接口抛的多异常就是这个结构,所以 3.11 之后写并发代码基本绕不开它。

自定义异常

最小写法就是继承一下 Exception:

1
2
class MyError(Exception):
"""模块内所有业务异常的基类占位"""

值得自定义的场景是「调用方需要按业务类型分别处理」。推荐一个模块一个基类,业务异常都挂在它下面,再带个错误码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AppError(Exception):
"""本模块所有自定义异常的基类"""

def __init__(self, message, code='E0000'):
super().__init__(message)
self.code = code

def __str__(self):
return '[{}] {}'.format(self.code, super().__str__())

class ConfigError(AppError):
"""配置文件有问题"""

def load(port):
if not isinstance(port, int):
raise ConfigError('port 必须是整数', code='E1001')
return port

try:
load('8080')
except AppError as e: # 捕获基类就能接住所有子类
print('捕获:', e)
print('code:', e.code, '| args:', e.args, '| 类型:', type(e).__name__)

输出:

1
2
捕获: [E1001] port 必须是整数
code: E1001 | args: ('port 必须是整数',) | 类型: ConfigError

几条约定:

  • 类名以 Error 结尾,一眼看出是异常。
  • 直接继承 Exception,别继承 BaseException——那等于躲过了 except Exception 的兜底。
  • 一个模块导出一个基类(示例的 AppError),内部异常继承它:调用方 except AppError 一网打尽,需要细分时再按子类分别接。
  • 额外信息存成属性,同时把消息交给 super().__init__(message)。原因是 pickle / copy 会用 *args 重建实例,args 里只放消息最稳。
  • 别为了「分类」把同一层级的异常拆成十几个类;分类靠错误码或属性,比靠类继承层次更好维护。

断言 assert

  • 基本用法

    「断言」就是把自己的假设写成代码:assert x > 0 读作「我断言 x 一定大于 0」,不成立就当场抛错,好处是让 bug 在假设刚被破坏的地方暴露,而不是跑到远处才炸。语法是 assert condition, message:条件为假时抛 AssertionError,message 成为异常文本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    print('__debug__ =', __debug__)          # 普通运行是 True

    def divide(a, b):
    assert b != 0, '除数不能为零' # 失败时抛 AssertionError
    return a / b

    print('正常调用:', divide(10, 2))
    try:
    divide(10, 0)
    except AssertionError as e:
    print('断言失败:', e)

    输出:

    1
    2
    3
    __debug__ = True
    正常调用: 5.0
    断言失败: 除数不能为零
  • -O 会把断言整句删掉

    断言由 __debug__ 控制:python -O(或 PYTHONOPTIMIZE=1 / 2)时 __debug__ 为 False,编译器直接把 assert 语句优化掉,不是「运行时跳过判断」。同一段代码用 -O 跑:

    1
    2
    3
    4
    5
    6
    7
    print('__debug__ =', __debug__)

    def divide(a, b):
    assert b != 0, '除数不能为零' # -O 下整句被跳过
    return a / b

    print('divide(10, 0) =', divide(10, 0))

    输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    __debug__ = False
    Traceback (most recent call last):
    File "<string>", line 7, in <module>
    print('divide(10, 0) =', divide(10, 0))
    ~~~~~~^^^^^^^
    File "<string>", line 5, in divide
    return a / b
    ~~^~~
    ZeroDivisionError: division by zero

    断言消失了,错误变成 ZeroDivisionError 从更深的地方冒出来。所以不要用 assert 做参数校验或任何带副作用的检查——那是一段「生产环境会蒸发」的代码;校验请写 if ...: raise ValueError(...)。

  • 元组陷阱

    1
    2
    assert (False, '这条消息其实没用上')     # 非空元组恒为真
    print('这行照样执行了:assert 后面跟了个元组,等于永远不触发')

    输出:

    1
    2
    <string>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
    这行照样执行了:assert 后面跟了个元组,等于永远不触发

    assert (cond, msg) 的条件是一个非空元组,恒为真,断言形同虚设。3.14 会直接发 SyntaxWarning: assertion is always true, perhaps remove parentheses?——逗号写错了就该被看见。

  • 什么时候适合用

    内部不变量(「这个分支不可能走到」「这个值由调用方保证非空」)、调试期契约、单元测试。判断标准一句话:线上真的需要这个检查生效,就别用 assert。

警告 warnings

警告不是异常:默认只打印一次、不中断执行,但可以提升成异常。类上 Warning 是 Exception 的子类,所以被提升之后 except Warning 接得住。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import warnings

def old_api():
# stacklevel=2 让警告指向调用方,而不是 warn() 这一行
warnings.warn('old_api 已废弃,请改用 new_api', DeprecationWarning, stacklevel=2)

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always')
old_api()
print('捕获到', len(caught), '条:', caught[0].category.__name__, '-', str(caught[0].message))

try:
with warnings.catch_warnings():
warnings.simplefilter('error', UserWarning) # 把警告升级成异常
warnings.warn('看起来要出问题了', UserWarning)
except UserWarning as e:
print('警告变成了异常:', e)

输出:

1
2
捕获到 1 条: DeprecationWarning - old_api 已废弃,请改用 new_api
警告变成了异常: 看起来要出问题了
  • warnings.warn(msg, Category, stacklevel=2):stacklevel 决定警告指向调用方那一行;写库时必须 +1,否则用户只看到库内部某一行。
  • catch_warnings(record=True) 用来在测试里断言「确实发了警告」。
  • simplefilter('error', UserWarning) 把指定类别升级为异常;等价的命令行用法是 -W error,环境变量是 PYTHONWARNINGS=error。
  • 常见类别:DeprecationWarning(已弃用,默认对普通用户隐藏)、PendingDeprecationWarning(即将弃用)、FutureWarning(行为会变,建议现在就改)、SyntaxWarning(可疑语法)、RuntimeWarning(可疑运行行为)、ResourceWarning(资源没关干净,-W error::ResourceWarning 查泄漏很好用)、EncodingWarning(3.10+,未显式指定编码的文本 IO)。

整体示例:一次调用的完整错误路径

把前面的点串起来:类型转换异常 → 打包成业务异常并保留起因(raise ... from)→ else 里做开发期自检 → finally 里发弃用警告 → 调用方按业务基类捕获、按 __cause__ 打起因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import warnings

class PayError(Exception):
"""支付相关的错误基类"""

class AmountError(PayError):
def __init__(self, message, amount=None):
super().__init__(message)
self.amount = amount

def __str__(self):
base = super().__str__()
return base if self.amount is None else '{}(金额 {})'.format(base, self.amount)

def charge(raw):
"""把外部输入转成分为单位的金额,任何一步不对都抛 AmountError"""
try:
amount = int(raw)
except (ValueError, TypeError) as e:
raise AmountError('金额格式不对', raw) from e # 异常链
else:
assert amount >= 0, '不该出现负数' # 只作开发期自检
finally:
warnings.warn('charge 即将下线', DeprecationWarning, stacklevel=2)
if amount == 0:
raise AmountError('金额不能为零')
return amount * 100

for raw in ['12', 'abc', 0]:
try:
print('charge({!r}) = {}'.format(raw, charge(raw)))
except AmountError as e:
print('charge({!r}) 失败: {}'.format(raw, e))
if e.__cause__:
print(' 起因:', type(e.__cause__).__name__, e.__cause__)
except BaseException as e: # 兜底也接住 SystemExit 之外的
print('charge({!r}) 意外: {}'.format(raw, e))

输出:

1
2
3
4
5
<string>:31: DeprecationWarning: charge 即将下线
charge('12') = 1200
charge('abc') 失败: 金额格式不对(金额 abc)
起因: ValueError invalid literal for int() with base 10: 'abc'
charge(0) 失败: 金额不能为零

四条读法:

  • 'abc' 那条展示了异常链:外层看到 AmountError,__cause__ 里留着真正的 ValueError。
  • finally 里的警告只打印一次(默认过滤器按「位置 + 消息」去重),但清理逻辑每次调用都会执行。
  • 0 那条是主动 raise 的业务异常,没有起因,所以 __cause__ 为空、不打第二行。
  • 循环里用 except BaseException 兜底只是为了演示;真实代码要收窄到具体类型,否则连 Ctrl+C 都停不下来。

参考

本文结束 感谢您的阅读
正在加载今日诗词....