题眼: 没有 checked exception,但 traceback 比 Java 的 stack trace 更善于讲故事。
第 1 章:第一印象 --- 当 Java 开发者看到 Python 异常
看两段等价代码,感受一下本能反应的差异。
读一个 JSON 配置文件,如果文件不存在就用默认值:
java
// Java: 先检查,再操作
File f = new File("config.json");
Map<String, Object> config;
if (f.exists()) {
try (BufferedReader reader = new BufferedReader(new FileReader(f))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
config = new ObjectMapper().readValue(sb.toString(), Map.class);
} catch (IOException e) {
config = new HashMap<>(); // 这行几乎不会走到
}
} else {
config = new HashMap<>();
}
python
# Python: 先操作,再兜底
try:
with open("config.json") as f:
config = json.load(f)
except FileNotFoundError:
config = {}
两段代码达到同样的效果,但直觉走向完全不同:
- Java 版用
f.exists()前置检查 + try-with-resources 管理资源。即使文件存在的概率是 99%,你也必须先写if检查------这是十几年 checked exception 训练出来的肌肉记忆。 - Python 版直接
open,不存在就except兜底。文件不存在和文件存在的代码在视觉上是对等分支,不存在被前置检查挤到一边。
这不是谁对谁错。是两种生态对"异常"有不同的经济模型。Java 中异常相对昂贵,所以防御式前置检查是理性的;Python 中异常便宜,所以乐观执行+兜底是理性的。
但问题不止于此。Python 的异常还做了更多 Java 开发者可能觉得"出格"的事------比如用异常来终止 for 循环。要理解为什么,得先理解 Python 的"异常经济学"。
第 2 章:便宜的异常 --- Python 的"异常经济学"
为什么 Python 里的异常不贵
在 Java 里触发异常是一个相对昂贵的操作------Throwable.fillInStackTrace() 需要遍历原生调用栈,记录每一帧的类名、方法名、文件名和行号。一个深层调用链上的异常触发可能消耗毫秒级时间。
Python 的执行模型不同。CPython 的调用栈不是原生栈的直接映射------它是 PyFrameObject 的链表。每个帧维护自己的值栈和块栈(block stack),块栈记录了当前帧中 try/except/for/with 等结构的位置。当异常发生时:
- 解释器在当前帧的块栈中查找匹配的 except handler
- 如果找到,跳转到对应的字节码地址
- 如果没找到,沿着
f_back链回到上一帧,重复
这比 C 层级的栈回溯轻得多 ------不需要遍历符号表、不需要解析调试信息。PEP 234 直言:"especially the time-critical for loop can test very cheaply for an exception."
Brett Cannon(Python 核心开发者)在 2016 年的博客中也反复强调同一个观点:
"Since exceptions are used for control flow like in EAFP, Python implementations work hard to make exceptions a cheap operation, and so you should not worry about the cost of exceptions when writing your code."
Java 中异常 ≈ 拍 X 光 ------ 贵,但诊断信息详细
Python 中异常 ≈ 测体温 ------ 便宜,频繁用也不心疼
便宜异常的后果:EAFP 成为惯用写法
因为异常便宜,Python 社区发展出 EAFP(Easier to Ask for Forgiveness than Permission)的风格:
python
# LBYL(Java/C 常见)
if key in dict:
value = dict[key]
# EAFP(Python 惯用)
try:
value = dict[key]
except KeyError:
value = default
Python 官方 Glossary 给出了三条论证:
- 线程安全 :
if key in dict: dict[key]在多线程下不是原子的------检查完 key 存在后,另一个线程可能删除它。try: dict[key] except KeyError没有这个竞态窗口。 - 代码意图:EAFP 的 try-except 结构把"正常路径"放在 try 块中,视觉上更突出常见情况;LBYL 的 if 检查反而让"异常情况"占据了代码视觉中心。
- 简洁快速 :Glossary 原文------"This clean and fast style is characterized by the presence of many try and except statements."
Java 翻译 : Java 也有类似的实践------Spring 的
JdbcTemplate把 checked exception 转译成 unchecked 的DataAccessException,本质上也是在"让异常变便宜"从而允许更简洁的调用代码。区别在于:Java 靠框架转译,Python 靠语言底层。
第 3 章:StopIteration --- 为什么循环结束用异常
这是 Java 开发者最困惑的一条:for 循环靠 StopIteration 异常来终止?!
python
for x in [1, 2, 3]:
print(x)
# 循环结束时,迭代器的 __next__() 抛出了一个 StopIteration。
# 这个异常被 `FOR_ITER` 字节码指令内部捕获并清除,循环干净地退出。
不是随意设计 --- PEP 234 的审慎取舍
PEP 234(2001 年,迭代器协议)的 "Resolved Issues" 节记录了四个替代方案的完整推演:
┌──────────────────────────────────────────────────────────────────┐
│ 方案 A: 特殊哨兵值 End │
│ → 拒绝。如果迭代表的值恰好包含这个哨兵,循环会提前终止。 │
│ "If the experience with null-terminated C strings hasn't │
│ taught us the problems this can cause..." │
│ │
│ 方案 B: end() 测试函数,每次迭代先检查是否结束 │
│ → 拒绝。每次迭代要两次调用(end() + next()),太贵。 │
│ "Two calls is much more expensive than one call plus a │
│ test for an exception." │
│ │
│ 方案 C: 复用 IndexError │
│ → 拒绝。会掩盖真正的索引错误------你分不清是"迭代结束"还是"真的 │
│ index out of bounds"。 │
│ │
│ 方案 D: StopIteration 异常 ← 中选 │
│ → 一次调用 + 一次异常检测。语义干净,不冲突。 │
│ "Particularly the time-critical for loop can test very │
│ cheaply for an exception." │
└──────────────────────────────────────────────────────────────────┘
StopIteration 不是"hack",而是三个替代方案各自在工程上有致命缺陷后,剩下的唯一合理选项。
Java 翻译 : 这就像 C 语言用
\0终止字符串的后果------一旦你的数据恰好包含 0,字符串就被截断了。如果__next__返回 None 表示结束,那对[1, 2, None, 3]迭代怎么办?哨兵值方案的困境,C 程序员已经痛苦了几十年。
这不是孤例 --- PEP 255 把生成器的 return 也变成了异常
PEP 255(生成器)明确规定:生成器里的 return 语句在语义上等于 raise StopIteration:
"When a return statement is encountered, control proceeds as in any function return, executing the appropriate finally clauses (if any exist). Then a StopIteration exception is raised."
为什么这样设计?PEP 255 的 Q&A 节直接回应了质疑:
"Why allow return at all? Why not force termination to be spelled raise StopIteration?"
"The mechanics of StopIteration are low-level details... return means 'I'm done' in any kind of function, and that's easy to explain and to use."
设计意图很清楚:StopIteration 是底层机制,return 是面向开发者的语法糖。底层统一走异常,但开发者不需要关心。
第 4 章:异常的三张面孔 --- 不只是"出错了"
StopIteration 只是一个案例。在 Python 的设计演化中,异常逐渐承担了三类不同的语义角色,走同一条技术通道:
┌───────────────────────────────────────────────────────────────┐
│ │
│ 第一类:错误信号 --- "something went wrong" │
│ ────────────────────────────────── │
│ ValueError / TypeError / KeyError / AttributeError │
│ OSError / ZeroDivisionError / IndexError │
│ 用法:try-except 捕获,处理或恢复 │
│ │
│ 这和 Java 的 RuntimeException 用法最接近。 │
│ Java 开发者对这类用法没有认知障碍。 │
│ │
├───────────────────────────────────────────────────────────────┤
│ │
│ 第二类:协议信号 --- "protocol state reached" │
│ ────────────────────────────────────── │
│ StopIteration → for 循环终止 (PEP 234) │
│ StopAsyncIteration → async for 终止 (PEP 492) │
│ GeneratorExit → 生成器清理 (PEP 255/380) │
│ 用法:由语言内部或标准库自动处理,开发者通常不需要手动捕获 │
│ │
│ 对 Java 开发者最冲击的一类。 │
│ "异常怎么能是正常控制流?"------但这正是设计本意。 │
│ │
├───────────────────────────────────────────────────────────────┤
│ │
│ 第三类:协作信号 --- "external event requesting action" │
│ ──────────────────────────────────────────────── │
│ KeyboardInterrupt → 用户 Ctrl+C 中断 │
│ SystemExit → 程序退出请求 │
│ CancelledError → asyncio 任务取消 │
│ 用法:传播到顶层后改变程序/任务状态 │
│ │
│ Java 也有类似的:Thread.interrupt() → InterruptedException。 │
│ 区别在于:Java 里这是特例,Python 里这是模式。 │
│ │
└───────────────────────────────────────────────────────────────┘
Java 翻译 :
InterruptedException在 Java 里也是一种协作信号------它不是"出错",而是"请停止",且必须被显式处理(checked exception)。相当于 Python 把这种设计思路推广到了迭代协议、生成器清理、异步任务取消等多个领域。
三类角色的共同技术基础是同一个:CPython 的异常传播机制------沿 f_back 链回退、匹配 except handler、恢复执行或继续传播。这是 Python 唯一通用的跨帧信号通道。
注意:尽管
StopIteration和GeneratorExit都归入"协议信号",它们的继承分支不同------StopIteration在Exception下(可被except Exception捕获),GeneratorExit在BaseException下(不会被except Exception捕获)。语义角色和继承体系是两条线,切莫混淆。
GeneratorExit:生成器的"请关闭"信号
当你调用生成器的 close() 方法时,CPython 向生成器内部抛入一个 GeneratorExit 异常。生成器可以在 except GeneratorExit 中做清理,但不能吞掉它(如果 GeneratorExit 被吞掉,运行时会强制重新抛出)。PEP 255 明确约定了 GeneratorExit 的清理语义:
python
def my_generator():
try:
yield 1
yield 2
except GeneratorExit:
print("清理资源")
# 不能 yield,不能 return 非 None 值 ------ 这里做完清理后必须让 GeneratorExit 继续传播
这跟 Java 里 Thread.interrupt() 的语义相似:被中断的线程需要自己检查中断状态并退出,Java 不会强制杀死线程。
第 5 章:从 try 到 with --- 结构化异常处理的演进
try-except-else-finally:Python 的四态分支
Java 开发者的 try-catch-finally 是三段式。Python 多了 else:
python
try:
f = open("data.json") # 可能 FileNotFoundError
except FileNotFoundError:
data = {} # 文件不存在 → fallback
else:
data = json.load(f) # 只有 open 成功了才执行
finally:
f.close() if 'f' in dir() else None # 无论如何
else 的价值在于物理隔离------把"可能出错的代码"和"依赖成功结果的代码"分开,防止 except 误捕获第二段代码的异常。
with 语句:不只是语法糖
PEP 343 引入的 with 语句表面上是 try-finally 的语法糖,深层是对上下文管理协议的标准化:
python
# 表层:资源管理
with open("data.json") as f:
data = json.load(f)
# f.close() 自动调用,即使 json.load 抛异常
# 深层:行为注入
with redirect_stdout(io.StringIO()) as buf:
print("hello") # 输出被重定向到 buf
print(buf.getvalue()) # "hello\n"
__exit__ 方法的返回值决定了异常是否被吞掉:
python
class SuppressKeyError:
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is KeyError:
return True # 吞掉 KeyError
return False # 其他异常继续传播
with SuppressKeyError():
value = data["maybe_missing"]
Java 翻译 : Java 的
try-with-resources只能close()资源,close()抛出的异常被附加为主异常的 suppressed exception,不能吞掉原始异常 。Python 的__exit__通过返回 True 可以主动吞掉异常------contextlib.suppress就是一个标准库实现。这是一个关键的语义差异:Java 的 close 是纯清理,Python 的 exit 可以有控制流语义。
contextlib:组合上下文管理器
python
from contextlib import ExitStack, suppress, redirect_stdout
with ExitStack() as stack:
stack.enter_context(suppress(FileNotFoundError))
stack.enter_context(open("log.txt", "w"))
# 多个上下文管理器在 ExitStack 下统一管理
# 退出时按 LIFO 顺序清理
ExitStack 替代了手写多层嵌套的 try-finally------这是 Python 结构化异常处理的终点形态。
第 6 章:异常链 --- raise ... from ...
Python 的隐式链:不需要手动 initCause
python
try:
open("missing.json")
except FileNotFoundError as e:
raise RuntimeError("配置加载失败") from e
这条代码背后有三个属性:
异常对象
├── __cause__ = FileNotFoundError 实例 (raise ... from ...)
├── __context__ = None (被 from 覆盖)
└── __suppress_context__ = True
当 raise ... from ... 时,__cause__ 被设置,__context__ 被隐式覆盖。但如果没有 from,Python 在你于 except 块内抛出新异常时会自动设置 __context__:
python
try:
open("missing.json")
except FileNotFoundError:
# 这里的任何一个异常都会自动把 FileNotFoundError 设为 __context__
raise RuntimeError("配置加载失败")
# __context__ = FileNotFoundError 实例(自动),__cause__ = None
这个设计意味着 Python 的异常链不需要开发者手动维护------运行时自动记录"在处理 X 异常时发生了 Y 异常"的因果关系。
切断异常链
python
raise RuntimeError("配置加载失败") from None
# __cause__ = None, __suppress_context__ = True
# 前端调用者只看到 RuntimeError,看不到 FileNotFoundError
什么时候切链?当底层异常是内部实现细节,不希望暴露给 API 调用者时。
Java 翻译:
Java: Throwable.initCause() → 显式链,需要手动设置 try-with-resources suppressed → close() 异常额外附加 Python: __context__ (隐式,自动记录) + __cause__ (显式,raise ... from ...) + raise ... from None (切断链接,隐藏内部异常)Python 的异常链像 git 的 commit 链------每个新异常要么挂着父提交(
from),要么是自动 merge(__context__),要么用from None做 squash。
第 7 章:traceback --- 异常不只是类型,还有故事
traceback 是对象,不是字符串
Java 中的异常信息是 Throwable.printStackTrace() 输出的文本或 getStackTrace() 返回的 StackTraceElement[] 数组------一个扁平的帧快照。
Python 的 traceback 是一个运行时链表,每个节点携带着当前帧的完全引用:
python
import sys
try:
1 / 0
except ZeroDivisionError:
exc_type, exc_value, exc_tb = sys.exc_info()
# exc_tb 是链表
while exc_tb:
print(f"{exc_tb.tb_frame.f_code.co_filename}:{exc_tb.tb_lineno}")
# 甚至可以访问局部变量!
print(exc_tb.tb_frame.f_locals)
exc_tb = exc_tb.tb_next
关键差异:
Java StackTraceElement[]:
"com.example.MyClass.method(MyClass.java:42)" ← 字符串
"com.example.MyClass.caller(Caller.java:15)" ← 字符串
... ← 扁平数组,无法访问运行时状态
Python traceback 链表:
tb.tb_frame.f_code.co_filename ← 源文件
tb.tb_lineno ← 行号
tb.tb_frame.f_locals ← 局部变量字典(活的!)
tb.tb_frame.f_globals ← 全局变量字典
tb.tb_next → 下一帧 ... ← 链表,向上追溯
为什么 traceback 可以访问局部变量? 因为 Python 的栈帧(PyFrameObject)存活在堆上,不被垃圾回收------只要 traceback 对象存在,对应帧就不会被释放。这是 Python traceback "善于讲故事"的根本原因:它携带的不是文字记录,而是运行时上下文的活引用。
traceback 工具链
python
import traceback
try:
1 / 0
except ZeroDivisionError:
# 只打印最后几帧
traceback.print_exc(limit=3)
# 提取结构化数据
tb_list = traceback.extract_tb(sys.exc_info()[2])
# 格式化完整字符串
formatted = traceback.format_exc()
生产环境中,像 rich.traceback.install() 或 Sentry SDK 就是利用 tb_frame.f_locals 的能力,展示出比 Java stack trace 丰富得多的上下文。
Java 翻译 :
StackTraceElement[]告诉你"在哪个文件的第几行出错了";Python traceback 链表还能告诉你"出错时所有局部变量是什么值"。这就像一个 StackTraceElement 还附带了一份及时的内存快照。
第 8 章:异步异常 --- CancelledError 与协作取消
取消不是强杀的:CancelledError 继承自 BaseException
python
import asyncio
async def worker():
try:
await asyncio.sleep(10)
except Exception: # ← 注意:不会捕获 CancelledError!
print("被 except Exception 吞了")
print("这行会执行吗?")
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(0.1)
task.cancel()
# worker 收到 CancelledError,但它不是 Exception 子类
# except Exception 捕获不到 ------ 异常继续传播,task 被取消
CancelledError 继承自 BaseException(而非 Exception)------这是一个刻意的设计选择。理由和 KeyboardInterrupt、SystemExit、GeneratorExit 一样:这些信号不应该被 except Exception 无意识地吞掉。
BaseException
├── SystemExit # sys.exit()
├── KeyboardInterrupt # Ctrl+C
├── GeneratorExit # 生成器 close()
├── CancelledError # asyncio 任务取消 (3.8 起也是 BaseException 子类)
└── Exception # 用户代码的异常都在这下面
├── ValueError
├── TypeError
├── StopIteration
└── ...
协作式取消:与 Java InterruptedException 的共鸣
Java 的线程取消也是协作式的------thread.interrupt() 不会强制停止线程,它设置中断标志 + 在阻塞方法(sleep/wait/join)中抛出 InterruptedException。被中断的线程自己决定如何响应。
Python 的 asyncio.CancelledError 遵循相同的哲学:
python
async def worker():
try:
await some_io()
except asyncio.CancelledError:
# 收到取消信号,做清理
await cleanup() # 清理本身可以包含 await
raise # 重新抛出,让调度器知道任务已取消
Java 翻译 :
Thread.interrupt()→InterruptedException的协作取消模式,在 Python 中被task.cancel()→CancelledError继承并推广到整个 asyncio 生态。核心设计思想一致:取消是"请停止",不是"强制终止"------任务有权利在收到取消信号后做清理,并可以选择继续抛出或包装成其他异常。
异步异常的更大图景
CancelledError 和 KeyboardInterrupt 属于同一类:外部事件请求任务状态变更 。asyncio 调度器只在 await 点注入 CancelledError(Python 3.11 起这一行为更严格),保证了任务不会被随意地在任意位置打断------这保护了数据一致性。
不展开 asyncio 的 shield()、TaskGroup 等特性(这些留给独立的 asyncio 文章)。本文的核心信息是:异步取消走的是同一条异常通道,这不是 hack,而是 Python 异常模型"不止是错误信号"的自然延伸。
ExceptionGroup:当多个任务同时失败
前面的讨论都基于一个隐含前提:一次只有一个异常在传播 。但在并发场景中,多个任务可能同时失败------Python 3.11 引入的 ExceptionGroup(PEP 654)和 except* 语法直接回应了这个问题。
ExceptionGroup 本身是一个异常(继承自 Exception),但它的特殊之处在于包裹了一组子异常 。except* 则不是简单的"匹配第一个"------它遍历组中所有异常,把匹配到指定类型的子异常提取出来,未匹配的继续保留在组中:
python
try:
raise ExceptionGroup("tasks failed", [
ValueError("bad value"),
TypeError("wrong type"),
ConnectionError("timeout"),
])
except* ValueError as eg:
print(f"值错误:{eg.exceptions}") # 提取出 ValueError
except* (TypeError, ConnectionError) as eg:
print(f"其他错误:{eg.exceptions}") # 提取出 TypeError + ConnectionError
Java 翻译 : Java 的
try-with-resources在关闭多个资源时,如果close()本身也抛出异常,原始异常和关闭异常通过addSuppressed()关联。但 Java 的 suppressed exceptions 是附加在主异常上的补充信息 (有主次之分),而ExceptionGroup中的多个异常是平等的------没有谁挂在谁下面。这是两种不同的哲学:Java 倾向于一个主异常+附属,Python 的新范式允许多个异常平级共存。
ExceptionGroup 的出现不是对现有异常模型的否定,而是自然的扩张------"协同信号"不再只是单个异常的消息,还可以是一组消息的集合。它和 StopIteration、CancelledError 一脉相承:异常通道足够通用,可以承载任何跨帧通信需求。
第 9 章:常见陷阱 --- Java 开发者容易踩的坑
陷阱 1:裸 except 吞掉一切
python
# 危险
try:
do_something()
except: # 等价于 except BaseException
pass # 吞掉了 KeyboardInterrupt, SystemExit, GeneratorExit...
即使不用裸 except,只用 except Exception 也不是万能的------GeneratorExit 和 CancelledError 在 Exception 体系之外。(Python 社区正通过 PEP 760 推动在未来版本中禁止裸 except:------这意味着这不仅是风格建议,而是即将到来的语言约束。)
陷阱 2:finally 里的 return 覆盖异常
python
def dangerous():
try:
raise ValueError("出错了")
finally:
return "一切正常" # 返回值覆盖了异常!
print(dangerous()) # "一切正常" ------ ValueError 被静默丢弃
这个坑 Python 和 Java 共享------finally 块中的 return 会覆盖正在传播的异常。
陷阱 3:except 捕获范围过大
python
try:
value = compute(data[key])
except KeyError:
value = default # 如果 compute() 内部也抛了 KeyError?
# 这个 except 也会捕获它 ------ 可能不是你想的
解决方案 :用 else 子句隔离,或让 try 块尽可能小。
陷阱 4:生成器清理时机不确定
python
def gen():
try:
yield 1
finally:
print("清理") # 如果生成器从未被消费完,这行可能永远不执行
g = gen()
next(g) # 拿到 1
# g 没有被 close(),也没有被 GC ------ "清理" 可能永远不会打印
使用 with closing(gen) 或确保生成器被完整消费。
陷阱 5:raise 不带参数只能在 except 块内
python
# 正确:重新抛出当前正在处理的异常
try:
...
except ValueError:
log("出错了")
raise # ← 必须在这个 except 块内
# 错误:不在 except 块内
def func():
raise # RuntimeError: No active exception to re-raise
陷阱 6:except 顺序很重要
python
try:
...
except Exception: # 先捕获父类 → 后面的子类永远不会被匹配
...
except ValueError: # 永远不会执行!
...
Python 的 except 匹配顺序是自顶向下、首个匹配,和 Java 的 catch 顺序一样。把更具体的异常放在前面。
第 10 章:收束三件套
选型决策框架:何时 EAFP,何时 LBYL?
Brett Cannon 的博客和 Guido 在 PEP 463 拒绝意见中都强调过一个核心信息:EAFP 不是教条 。Guido 原话------"I disagree with the position that EAFP is better than LBYL, or 'generally recommended' by Python."
实际上,选择取决于场景:
使用 EAFP 的情况: 使用 LBYL 的情况:
┌───────────────────────────────────┐ ┌───────────────────────────────┐
│ 正常路径是高频事件 │ │ 异常情况本身是常规逻辑分支 │
│ 例:dict[key] 时 key 通常存在 │ │ 例:用户输入验证 │
│ │ │ │
│ 异常类型精确可控 │ │ 抛异常成本不可忽略 │
│ 例:KeyError 只在 key 缺失时抛出 │ │ 例:循环体内的 try-except │
│ │ │ │
│ 并发场景(避免竞态) │ │ 需要提前做多个条件的组合判断 │
│ 例:多线程访问共享字典 │ │ 例:表单所有字段校验 │
└───────────────────────────────────┘ └───────────────────────────────┘
核心原则 :选择让你的代码意图更清晰的那种写法。如果 LBYL 能让分支逻辑更明显,就用 LBYL。
生产排查工具
场景 工具
─────────────────────────────────────────────────────
开发中看到漂亮的 traceback rich.traceback.install()
better_exceptions
记录异常到日志 logging.exception()
logger.error("...", exc_info=True)
全局异常捕获钩子 sys.excepthook = custom_handler
(线程内部用 threading.excepthook)
生产环境异常追踪 Sentry / 自建 traceback 收集
利用 tb_frame.f_locals 的上下文信息
调试时检查异常上下文 sys.exc_info() 三元组
traceback.extract_tb()
traceback.format_exc()
关键收获
这篇文章的核心信息不是"学 Python 异常语法"------那些是常规文档的范围。而是理解 Python 背后的异常经济模型:
- 便宜异常 → 允许控制流走异常通道,改变了异常的使用频率和场景
- 三张面孔 → 异常不止是错误信号,还是协议信号和协作信号------理解这个转换,是 Java 开发者认知翻译的核心
- PEP 有据 → 每个"为什么这样设计"的问题,PEP 文档都有明确的工程推演(哨兵值、end() 函数、IndexError 复用为何被否决)
- 不是更优,而是不同 → Python 的异常模型不是"Java 的改进版",而是在不同约束条件(动态类型、字节码解释器、GIL)下演化出的不同策略
- 协作取消是模式不是特例 → 从 InterruptedException 到 CancelledError,异常作为协作信号跨越了两门语言的边界
下一篇预告:函数式特性 --- 生成器不只是语法糖,它是 Python 的惰性计算原语。