
在 Python 3.11 之前,异常处理(如 try-except 语句)依赖于一个运行时维护的“块栈”(block stack)。当进入 try 块时,解释器会通过特定的字节码指令(例如 SETUP_FINALLY)将一个异常处理块推入栈中;当离开 try 块时,则通过 POP_BLOCK 等指令将其弹出。这种机制虽然功能完善,但在正常执行路径(即没有异常发生)下,仍然会产生额外的字节码执行开销。
为了优化这一过程,Python 3.11 引入了“零成本”(zero-cost)异常处理机制。其核心思想是,在没有异常发生时,异常处理机制的开销应尽可能接近于零。这通过将异常处理逻辑从字节码指令流中分离出来,存储在一个独立的 ExceptionTable 中实现。当程序正常执行时,解释器无需执行任何与异常处理相关的额外指令;只有当实际发生异常时,解释器才会查询 ExceptionTable 来确定跳转目标。
当使用 dis 模块反汇编 Python 3.11 及更高版本的代码时,你可能会在输出的末尾看到一个 ExceptionTable 部分。这个表格记录了异常发生时程序应跳转到的目标位置。
考虑以下列表推导式在 Python 3.13 中的 dis 输出:
立即学习“Python免费学习笔记(深入)”;
>>> import dis
>>> dis.dis('[i for i in range(10)]')
0 RESUME 0
1 LOAD_NAME 0 (range)
PUSH_NULL
LOAD_CONST 0 (10)
CALL 1
GET_ITER
LOAD_FAST_AND_CLEAR 0 (i)
SWAP 2
L1: BUILD_LIST 0
SWAP 2
L2: FOR_ITER 4 (to L3)
STORE_FAST_LOAD_FAST 0 (i, i)
LIST_APPEND 2
JUMP_BACKWARD 6 (to L2)
L3: END_FOR
L4: SWAP 2
STORE_FAST 0 (i)
RETURN_VALUE
-- L5: SWAP 2
POP_TOP
1 SWAP 2
STORE_FAST 0 (i)
RERAISE 0
ExceptionTable:
L1 to L4 -> L5 [2]在 ExceptionTable 部分,L1 to L4 -> L5 [2] 表示一个异常处理条目:
这意味着,如果在字节码偏移量 L1 到 L4 之间(不包含 L4)的任何指令引发了异常,解释器会查找 ExceptionTable 并将执行流跳转到 L5 处的指令。
相比之下,Python 3.10 的 dis 输出中没有 ExceptionTable,而是依赖于 SETUP_FINALLY 等操作码来管理异常处理块。
ExceptionTable 的原始数据存储在代码对象的 co_exceptiontable 属性中,它是一个字节串(bytes)。dis 模块的输出是解析这个字节串的结果。我们可以通过以下方式访问和解析它:
>>> def foo(): ... c = 1 + 2 ... return c ... >>> foo.__code__.co_exceptiontable b'' # 没有异常处理,所以是空字节串 >>> def foo_with_except(): ... try: ... 1/0 ... except: ... pass ... >>> foo_with_except.__code__.co_exceptiontable b'\x82\x05\x08\x00\x88\x02\x0c\x03' # 包含异常处理信息 >>> from dis import _parse_exception_table >>> _parse_exception_table(foo_with_except.__code__) [_ExceptionTableEntry(start=4, end=14, target=16, depth=0, lasti=False), _ExceptionTableEntry(start=16, end=20, target=24, depth=1, lasti=True)]
_parse_exception_table 函数(这是一个内部函数,不推荐在生产代码中直接使用,但有助于理解)能够将原始字节串解析成更易读的 _ExceptionTableEntry 对象列表。每个 _ExceptionTableEntry 对象包含 start、end、target、depth 和 lasti 等属性,这些都对应着 ExceptionTable 的各项信息。
为了更直观地理解零成本异常处理的优势,我们对比一个简单的 try-except 块在 Python 3.10 和 Python 3.11 中的字节码差异。
Python 3.10 的字节码示例:
def f():
try:
g(0)
except:
return "fail"其在 Python 3.10 中的 dis 输出大致如下:
2 0 SETUP_FINALLY 7 (to 16) # 推入异常处理块
3 2 LOAD_GLOBAL 0 (g)
4 LOAD_CONST 1 (0)
6 CALL_NO_KW 1
8 POP_TOP
10 POP_BLOCK # 正常退出时弹出块
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
4 >> 16 POP_TOP # 异常发生时跳到这里
18 POP_TOP
20 POP_TOP
5 22 POP_EXCEPT
24 LOAD_CONST 3 ('fail')
26 RETURN_VALUE可以看到,SETUP_FINALLY 和 POP_BLOCK 是显式存在的字节码指令。即使没有异常发生,这些指令也需要被执行,从而产生开销。
Python 3.11 的字节码示例:
同样的 f() 函数在 Python 3.11 中的 dis 输出大致如下:
1 0 RESUME 0
2 2 NOP
3 4 LOAD_GLOBAL 1 (g + NULL)
16 LOAD_CONST 1 (0)
18 PRECALL 1
22 CALL 1
32 POP_TOP
34 LOAD_CONST 0 (None)
36 RETURN_VALUE
>> 38 PUSH_EXC_INFO # 异常发生时才执行
4 40 POP_TOP
5 42 POP_EXCEPT
44 LOAD_CONST 2 ('fail')
46 RETURN_VALUE
>> 48 COPY 3
50 POP_EXCEPT
52 RERAISE 1
ExceptionTable:
4 to 32 -> 38 [0]
38 to 40 -> 48 [1] lasti在 Python 3.11 中,SETUP_FINALLY 和 POP_BLOCK 等指令被移除。取而代之的是 ExceptionTable。当 g(0) 调用(字节码偏移量 22 处)发生异常时,解释器会查询 ExceptionTable。由于 22 落在 4 到 32 的范围内,解释器会跳转到 38 处的 PUSH_EXC_INFO 指令,从而进入异常处理流程。在正常执行路径下,这些跳转和异常处理逻辑完全不会被触及,从而实现了“零成本”。
ExceptionTable 是 Python 3.11 引入的一项重要内部优化,它将异常处理的元数据从字节码指令流中分离出来,实现了“零成本”异常处理。这使得在没有异常发生的常见情况下,Python 程序的执行效率更高。对于开发者而言,虽然日常编程中无需直接与 ExceptionTable 交互,但了解其存在和工作原理有助于深入理解 Python 解释器的内部机制,尤其是在进行性能分析或调试底层问题时。
以上就是Python 3.11+ 异常处理机制:深入理解 ExceptionTable的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号