正在阅读从名字绑定到生成器:21 个 Python 细节与 22 组实验
香港天气加载中…
QWeather ↗

从名字绑定到生成器:21 个 Python 细节与 22 组实验

合并今天的逐次追问与手工笔记,细分赋值与对象创建、id/is/==、Heap 与循环引用、函数对象与返回值、延迟调用、iter/next、生成器暂停与耗尽。附 22 组已运行实验、原笔记核对表及十道折叠复盘题。

从今天的追问开始:细小区别怎样连成一条线

这篇笔记合并了 2026 年 10 月 20 日的 Python 讨论与 Mokin 自己整理的笔记。起点是“名字什么时候绑定对象”,随后追问到对象身份、Heap、容器引用、函数调用和生成器。保留这些小问题,是为了以后看到一行代码时,能说清楚它实际做了什么。

复习组这组要回答的问题入口
名字与对象什么时候建立绑定?有没有创建对象?身份变了吗?01–05
容器与存活列表保存什么?环是不是死锁?Heap 管什么?06–08
函数与调用函数对象、引用、返回值与打印有什么区别?09–12
迭代与按需执行谁记录进度?什么时候计算?耗尽后怎么处理?13–21

每个标注“独立运行”的程序使用新进程运行;先预测输出,再展开答案。篇末有原笔记核对表与十道复盘题。本篇沿用普通同步 Python 示例,涉及 CPython 内存实现时会明确标出。

前置知识可回看 Namespace、Scope 与 LEGB;后续路线在 Python 进阶 Guidebook。这次讨论主要补充 Guidebook 的阶段 0、1 和 6。

01 · “执行前确定”的是什么:作用域类别还是对象绑定?

你的问题:普通变量的绑定,是不是在代码执行前就建立好了?

需要分开两件事:对普通函数,Python 会分析整个函数代码块中的绑定操作,确定名字属于局部名、自由变量或全局名等类别;具体绑定到哪个对象,则要看运行时实际执行了什么。知道这里有一个局部名字 results,不等于它已经指向某个列表。

LEGB 用来理解普通函数裸名解析涉及的作用域层次。它不能被机械理解成“每次先找 L,没值就继续找 E、G、B”。已经判定为局部名,但读取时尚未绑定,会抛出 UnboundLocalError。

局部名已分类,不代表已经绑定

Python · 独立运行
results = ["GLOBAL"]

def reset():
    print(results)
    results = []

try:
    reset()
except UnboundLocalError as exc:
    print(type(exc).__name__)

print(results)
展开参考输出:先预测,再核对
UnboundLocalError
['GLOBAL']

函数后面出现对 results 的赋值,已足以影响整段函数中该名字的类别;它不需要先执行到赋值那一行。第一次读取失败以后,也不会借用模块里的同名值。

把两个时间点写在笔记上:代码分析确定如何解析这个名字;语句执行建立或改变实际绑定。参数绑定、函数定义、类定义和 import 也可能建立绑定,赋值不是唯一入口。

如果需要修改另一份绑定:global 指向 G;nonlocal 指向最近包含对应绑定定义的外层函数 E。E 可以嵌套多层。详见原文的 E/G 同名实验。

规则依据:名字分类与解析。

02 · 普通赋值:先求值右侧,再把结果赋给目标

你的原句:“普通赋值也需要执行,才会建立绑定。”这点正确。后面的“得到一个新对象”要去掉“新”。

“对表达式求值”是这里更准确的说法。表达式不一定是算术运算,也可以是名字查找、列表构造或函数调用。左侧是绑定目标;它不必预先指向一个对象。

先完成右侧求值,再改变名字绑定

Python · 独立运行
results = ["OLD"]

def build_results():
    print("RHS sees:", results)
    return ["NEW"]

results = build_results()
print("after binding:", results)

try:
    results = 1 / 0
except ZeroDivisionError as exc:
    print(type(exc).__name__)

print("after failed RHS:", results)
展开参考输出:先预测,再核对
RHS sees: ['OLD']
after binding: ['NEW']
ZeroDivisionError
after failed RHS: ['NEW']

右侧函数先执行,其中的打印先出现,之后才轮到左侧绑定。右侧抛出异常时,本例中左侧名字的新绑定尚未建立,原来的绑定仍在。注意:右侧在失败前已经产生的其他副作用,不会因此自动撤销。

边界:这里说的是普通赋值。增强赋值 += 会先求值目标;items[0] = value 和 obj.name = value 则分别更新元素或属性,不能一概叫“建立一个局部名字”。

规则依据:普通赋值与增强赋值。

03 · 创建对象、首次绑定、重新绑定:可以同时发生吗?

可以。对象是否创建,看右侧做了什么;名字是否重新绑定,看这个绑定之前是否存在。它们是两个判断维度。

依次执行对象侧名字侧
a = []创建列表 A首次将 a 绑定到 A
b = a取得已有列表 A,不复制若 b 此前未绑定,这是 b 的首次绑定
a = []创建另一个列表 Ba 重新绑定到 B;b 仍绑定 A
b = a取得已有列表 Bb 重新绑定到 B

创建对象与建立绑定是两个维度

Python · 独立运行
a = []
first_id = id(a)
b = a
print(b is a)

a = []
print(id(a) == first_id)
print(id(b) == first_id)
print(a is b)
展开参考输出:先预测,再核对
True
False
True
False

第一行和第三行的 id(a) 会不会变化?在这个例子里会。旧列表由 b 保留,新列表是另一个同时存活的对象,所以两者 id 不同。变化的是 a 当前取得的对象;旧对象自身的身份没有改变。

id 在对象存活期间保持不变。对象结束生命周期后,其 id 数值将来可能被复用,所以不要用“先打印一个已销毁对象的 id,再比较另一个对象”来证明对象相同。

[] 每次求值产生新列表,但 b = a 不复制它;函数调用也不保证新建对象,函数完全可以返回已有对象。整数和字符串还可能复用已有对象,不能从字面量出现了几次推断对象分配次数。

规则依据:赋值不会复制对象;id 的生命周期。

04 · == 与 is:相等和同一个对象有什么区别?

你的理解:“== 判断值是否相等;is 判断是不是同一个对象。”用于入门理解是对的,再补一句:相等的具体规则由类型定义,自定义类可以实现 __eq__;is 比较对象身份,不能通过 __eq__ 改写。

相等与同一对象分别判断

Python · 独立运行
a = [1, 2]
b = a
c = [1, 2]

print(a == b)
print(a is b)
print(a == c)
print(a is c)
展开参考输出:先预测,再核对
True
True
True
False

分别创建的两个列表可以内容相等,却不是同一个列表;两个名字也可以引用同一列表。后续修改对象时,是否共享身份就有实际影响。

需要判断是否为 None,通常写 value is None。比较数值、字符串或业务内容则用相等规则,不依赖整数缓存或字符串复用。

一个精度补充:同一对象一定 == 自己吗?

不应把这个当作普遍规律。例如 NaN 与自身比较也不相等,自定义类型的相等规则也可能不同。这不会改变 is 对身份的判断。

规则依据:对象身份与相等比较。

05 · clear() 和 = []:为什么别名看到的结果不同?

results.clear() 先通过 results 找到列表,再修改这个列表。results = [] 创建新列表,并改变所选名字的绑定。其他名字不会自动跟着改绑。

clear 修改原对象,赋值可以改变绑定

Python · 独立运行
results = ["PASS"]
alias = results

results.clear()
print(results, alias, results is alias)

results = ["NEW"]
print(results, alias, results is alias)
展开参考输出:先预测,再核对
[] [] True
['NEW'] [] False

函数里调用 results.clear() 可以通过名字解析找到模块中的列表并修改它,通常不需要 global;函数里写 results = [],在没有 global/nonlocal 时建立的是该函数调用的局部绑定。

如果目的是重新绑定模块中的 results,才写 global results。声明改变的是名字所指向的绑定环境,不是把列表变成一种“全局对象”。同一对象可以同时被局部名字、模块名字和容器引用。

继续区分复制:other = results.copy() 创建新的外层列表,但其中元素仍可引用相同的对象;浅复制不等于把整张对象关系图复制一遍。

规则依据:赋值目标;浅复制。

06 · 为什么有列表、字典、字符串?列表能保存哪些对象?

看具体表达式,而不要把“Python 有这些类型”误读成“任何列表内部都有它们”。例如:

Python · 说明片段
results = [{"status": "PASS"}]
代码部分在对象关系中扮演的角色
最外层 [...]一个列表,其第一个元素引用字典
{"status": "PASS"}一个字典,把键对象与值对象关联起来
"status"、"PASS"字符串对象,分别参与键和值的关联;不据此断言每次都重新分配字符串
results绑定到最外层列表的名字

内置列表可以引用任意 Python 对象,元素不要求同类型,也不要求可哈希。字典、函数、类、实例、模块、None 都可以成为元素;同一对象也可以出现在多个位置。

列表可以保存不同类型对象的引用

Python · 独立运行
def run_case():
    return "PASS"

case = {"status": "PASS"}
items = [case, case, run_case, None]
items[0]["status"] = "FAIL"

print(items[1]["status"])
print(items[0] is items[1])
print(items[2] is run_case)
print(items[3] is None)
展开参考输出:先预测,再核对
FAIL
True
True
True

两个位置引用同一字典时,通过一个位置修改字典,另一个位置也会看到变化。列表保存的引用关系,让这些对象成为可从列表访问的对象;没有隐含的元素复制。

在 CPython 中,list 使用的元素数组保存对象引用;引用槽位连续,不要求被引用对象的本体也连续。相关存储视角见 数据结构分类中的 list 引用槽位。

规则依据:容器与列表元素;CPython 的列表实现。

07 · Heap 里放什么?是谁决定可用内存?

先分层:名字绑定和对象身份是 Python 的语言概念;Heap、分配器和操作系统是在解释这些对象如何得到存储空间。不要把它们混成“名字必定放栈,对象必定放某块固定堆”的规则。

以 CPython 常见实现为例,解释器管理动态对象和辅助数据结构所需的内存:对象的类型等管理信息、对象自身的数据,以及列表引用数组、字典内部表等。空列表没有元素,也仍然需要对象自身的存储。

观察角度要回答的问题
Namespace哪个名字关联到哪个对象?
对象关系列表、字典、函数或其他对象又保留了哪些引用?
解释器分配器这些对象和辅助结构需要多少空间,哪些内存块可以复用?
进程与操作系统虚拟地址空间、可用资源及进程或容器的限制,允许继续使用多少内存?

创建对象时,解释器可能复用已经管理的内存,不一定每次都向操作系统申请;需要扩展时才与底层分配机制交互。因此 Heap 不能简单理解为启动时由 OS 划定的一块永不变化的固定容量。

可用空间受机器与操作系统资源、地址空间、进程限制、容器内存上限等共同影响。CPython 的分配方式也可能随版本、对象大小及构建方式变化。本篇不把某次运行的对象地址或内存占用数字当成跨环境保证。

同理,函数返回不意味着所有关联对象立即销毁:调用者、容器、闭包等仍可能保留引用。可以回看 闭包与绑定的生命周期。学习当前问题时,先画清楚引用关系,通常比推测物理内存位置更有帮助。

规则依据:CPython 内存管理;进程资源限制;Linux 容器内存限制。

08 · 列表引用自身,会不会死循环、死锁或无法回收?

你的问题:“a 的第一个元素引用 a 自己,会不会出问题,类似死锁?”创建这样的引用关系本身是合法的。

自引用列表仍然只有一个元素

Python · 独立运行
a = []
a.append(a)

print(len(a))
print(a[0] is a)
print(a)
for item in a:
    print("loop:", item is a)
展开参考输出:先预测,再核对
1
True
[[...]]
loop: True

这里始终只有一个列表对象。append 只执行了一次,新增的那个元素引用列表自己;没有不断创建新列表。普通的一层 for 只访问一个元素,也能结束。

内置列表的表示机制会防止递归展开,因而输出 [[...]]。这里的 ... 是显示用的标记;它不代表该元素是 Ellipsis 对象,真实元素仍是列表自己。

概念描述什么本例的情况
循环引用对象之间的引用关系形成环存在
死循环/无终止递归执行过程不能按预期停止引用关系本身不等于无限执行;不识别环的递归处理可能出问题
死锁执行单元因资源等待关系而无法推进这个单线程列表构造没有建立这种等待关系

同一个对象,交给不同操作会有不同结果

len 和普通列表显示能处理本例;一个“遇到列表就递归下去”的简单遍历函数,若没有识别已访问对象,则可能最终触发 RecursionError。JSON 也不是通用的对象引用图格式:

JSON 编码器默认检查循环引用

Python · 独立运行
import json

a = []
a.append(a)

try:
    json.dumps(a)
except ValueError as exc:
    print(type(exc).__name__)
展开参考输出:先预测,再核对
ValueError

json.dumps 默认检查循环引用,本例抛出 ValueError。关闭环检查不会把这张引用图变成可正常表达的 JSON 树。

不用以后,环由谁处理?

在 CPython 中,循环垃圾回收是对引用计数的补充,能够处理这类已经不可达的普通列表环。del a 只删除一个名字的绑定:如果 other、容器、闭包或尚存活的调用状态仍保留它,这个对象就不一定已经不可达。例如,上面 for 循环结束后,item 也仍引用这个列表,因此只执行 del a 并不足以让它不可达。即使没有外部可达路径,也不要假定回收发生在删除名字的同一瞬间。

你的复习句可以保留:先检查处理它的算法能否识别环,再检查不再使用时是否仍有外部引用保留它。对象回收与归还操作系统内存,还要按上一节的三个事件分别考虑。

规则依据:JSON 环检查;循环垃圾回收。

09 · “函数引用就是函数对象吗?”把三个概念分开

你的问题:“如果只是稍后调用 run_case,保存函数引用已经足够;函数引用就是函数对象吗?”表达式 run_case 求值得到函数对象,而“引用”描述的是访问这个对象的关系。

概念在普通 def 示例中是什么
名字run_case 这个标识符
函数对象执行 def 语句时创建、可被调用的对象
绑定/引用关系名字或容器元素关联到这个对象,使程序能访问它

多个访问路径可以引用同一个函数

Python · 独立运行
def run_case():
    return "PASS"

another_name = run_case
tasks = [run_case]

print(type(run_case).__name__)
print(another_name is run_case)
print(tasks[0] is run_case)
print(run_case(), another_name(), tasks[0]())
展开参考输出:先预测,再核对
function
True
True
PASS PASS PASS

another_name、run_case 和列表里的那个元素可以访问同一个函数。没有复制函数,也没有额外创建一个需要你操纵的“函数引用包装对象”。调用 tasks[0](),就是先取出那个函数对象,再调用它。

type(run_case) 查看的是名字求值后得到的对象的类型,不是在检查源代码中这个单词的拼写。执行普通 def 会创建并绑定函数,但不会在定义这一刻运行函数体;本节不涉及装饰器替换绑定的情况。

规则依据:函数定义的执行;名字表达式。

10 · [run_case()] 为什么先打印?results 最终绑定什么?

你的推理方向正确:为了完成右侧求值,需要执行其中的调用。把“对 Object 进行计算”改成“对右侧表达式求值”,就更精确了。

函数调用先发生,返回值成为列表元素

Python · 独立运行
def run_case():
    print("Running...")
    return "PASS"

results = [run_case()]
print(results)
展开参考输出:先预测,再核对
Running...
['PASS']
表达式/名字求值取得的对象
run_case函数对象
run_case()函数返回的字符串对象 "PASS"
[run_case()]包含返回值的列表对象 ["PASS"]
赋值完成后的 results上面那个完整列表

按执行顺序看:查找 run_case,调用函数,函数体打印 Running...,函数返回 PASS,右侧列表表达式完成求值,最后把 results 绑定到列表。这里的步骤用于追踪可观察行为,不据此推断列表底层分配内存的精确时点。

打印是执行中的副作用,返回值是调用表达式取得的结果。print 输出的文字不会自动变成外层函数的返回值;如果删掉 return "PASS" 而保留打印,这个普通函数返回 None,列表就会保存 None。

规则依据:列表表达式与调用;普通函数返回。

11 · return None、return 和不写 return,何时可以互换?

对于普通函数,执行到末尾没有显式返回值,会返回 None;裸 return 与 return None 也都返回 None。

普通函数的三种 None 返回方式

Python · 独立运行
def implicit():
    pass

def bare_return():
    return

def explicit_none():
    return None

print(implicit())
print(bare_return())
print(explicit_none())
展开参考输出:先预测,再核对
None
None
None

但 “返回同样的值”不等于“删除这行不影响控制流程”。放在分支中的 return 还负责让本次调用提前结束:

删除提前返回会改变后续控制流

Python · 独立运行
def with_return(enabled):
    if not enabled:
        print("Skipped")
        return None
    print("Running...")

def without_return(enabled):
    if not enabled:
        print("Skipped")
    print("Running...")

print("with return:")
with_return(False)
print("without return:")
without_return(False)
展开参考输出:先预测,再核对
with return:
Skipped
without return:
Skipped
Running...

当 enabled 为假时,带提前返回的版本在 Skipped 后结束;删掉 return 的版本会继续执行后面的 Running...。因此,末尾 return None 往往可以省略,中途 return None 可以改成 return,却不能随意删掉。

本节限定普通函数。生成器里的 return 表示结束迭代,之后会在生成器部分单独说明。

规则依据:return 语句。

12 · 只保存函数就可以延迟执行,为什么还会谈惰性求值?

你的联想:[run_case] 让我想到 lazy evaluation。这个联系有道理,准确地说,保存可调用对象为“之后再调用”提供了条件;还需要调用方安排何时执行。

保存函数以后,可以选择调用时机

Python · 独立运行
def run_case():
    print("Running...")
    return "PASS"

tasks = [run_case]
print("saved")

result = tasks[0]()
print("result:", result)
展开参考输出:先预测,再核对
saved
Running...
result: PASS
执行 tasks = [run_case] 时是否发生
查找 run_case 并取得函数对象已经发生
创建列表并保存对函数的引用已经发生
调用 run_case、运行它的函数体、取得调用的返回值尚未发生

之后执行 result = tasks[0]() 才会调用。这体现了 deferred execution(延迟执行);如果按照结果需求来决定调用,则形成按需计算。仅仅保存函数,不意味着后台自动执行、异步调度或自动缓存。

如果目标只是“稍后调用这一个函数”,这样已经足够。下面引入生成器,是继续研究“怎样把一连串结果逐项提供给使用者”。

13 · iter() 是“转换”吗?为什么 iter(iterator) 不会重置?

你的原句:“iter() 将可迭代对象转换为迭代器。”作为第一印象可以理解,但笔记里建议写成:单参数 iter(obj) 按迭代协议或序列协议取得一个迭代器。

对象/操作含义
可迭代对象 iterable能让 iter(obj) 取得迭代器的对象
迭代器 iterator可由 next() 推进,提供下一项或抛出 StopIteration
iter(列表)取得一个新的列表迭代器,具有自己的遍历位置
iter(符合协议的迭代器)返回迭代器自身;不复制,也不从头开始

取得迭代器可能返回原对象

Python · 独立运行
tasks = ["A", "B"]
first = iter(tasks)
second = iter(tasks)
same = iter(first)

print(first is second)
print(same is first)
print(next(first))
print(next(same))
print(next(second))
展开参考输出:先预测,再核对
False
True
A
B
A

first 与 same 是同一个迭代器的两个名字,共享进度;second 是另外创建的列表迭代器,所以可以独立从第一项开始。列表自身没有被“转换掉”,仍然可以用它获取新的迭代器。

“可迭代”不保证“每次 iter 都从头提供一份新数据”。生成器对象自己就是迭代器,也是可迭代对象;对它调用 iter() 取得的仍是它本身。

这里讨论单参数形式;iter(callable, sentinel) 是另外一种用法,当前不需要展开。

规则依据:iter();迭代器协议。

14 · 函数放进 list 再逐项调用,为什么不可以?完全可以

你的追问:“为什么不能将函数引用放在 list 中,然后逐项获取呢?”可以,而且你指出了一个比较时必须保留的区别:函数列表不是已经计算好的结果列表。

取得函数与取得计算结果是两种接口

Python · 独立运行
def run_case():
    print("Running...")
    return "PASS"

tasks = [run_case, run_case]
task_iter = iter(tasks)
print("task iterator created")
task = next(task_iter)
print("next returned function:", task is run_case)
print("first result:", task())
print("second result:", next(task_iter)())

results = (task() for task in tasks)
print("result generator created")
print("first result:", next(results))
print("second result:", next(results))
展开参考输出:先预测,再核对
task iterator created
next returned function: True
Running...
first result: PASS
Running...
second result: PASS
result generator created
Running...
first result: PASS
Running...
second result: PASS
方式创建阶段取一次结果进度由谁保留
[task() for task in tasks]逐个调用,保存全部返回值从结果列表取值后续访问结果列表的代码/迭代器
task_iter = iter(tasks)取得遍历函数对象的迭代器,不调用任务next(task_iter)()列表迭代器保留任务位置
results = (task() for task in tasks)创建生成器并取得最外层迭代器,不调用任务next(results)生成器保留执行状态,内部迭代器保留任务位置

在本例里,next(task_iter) 返回函数对象,需要再调用;next(results) 直接返回任务执行结果。生成器把“取下一个任务 → 调用 → 提供结果”封装成一个可迭代的结果生产过程。

两种延迟方式都能只算需要的部分、记录进度,并避免保留所有结果。给现有 tasks 列表包一层生成器,也不会让列表本身的内存占用消失。普通 for 循环同样可以边处理边使用结果。

所以,简单场景下函数列表已经够用;当你希望消费者只需取得结果,或要把筛选、转换、暂停位置组织在一起时,生成器接口会更方便。它不意味着自动并发或自动缓存。

比较的边界:异常情况下也完全等价吗?

这里的并排实验限定任务正常返回。若任务调用的异常没有在生成器内部处理而逸出,生成器会结束;在外部手动调用任务时,列表迭代器可能仍可继续使用。不能仅凭正常输出一样,就断言所有行为都等价。

规则依据:迭代器与生成器的组织方式。

15 · 生成器为什么能暂停?局部变量为什么没有重置?

普通 def 的自身函数体中包含 yield,会使它成为生成器函数;只在另一个嵌套函数中出现 yield,不会把外层函数也变成生成器函数。调用生成器函数取得生成器对象,函数体暂时不运行。

next() 开始或恢复执行;到 yield 时把一个值交给调用者并暂停,保留继续执行所需的局部绑定、迭代进度和位置。下次推进从暂停处继续,不从函数第一行重新执行。

生成器保留局部状态与继续执行的位置

Python · 独立运行
from inspect import getgeneratorstate

def generate_results():
    count = 0
    for label in ("A", "B"):
        count += 1
        print("body:", label, count)
        yield label
    print("body: end")

results = generate_results()
print("created:", getgeneratorstate(results))
print("next:", next(results))
print("paused:", getgeneratorstate(results))
print("next:", next(results))
print("paused:", getgeneratorstate(results))
try:
    next(results)
except StopIteration as exc:
    print(type(exc).__name__)
print("finished:", getgeneratorstate(results))
展开参考输出:先预测,再核对
created: GEN_CREATED
body: A 1
next: A
paused: GEN_SUSPENDED
body: B 2
next: B
paused: GEN_SUSPENDED
body: end
StopIteration
finished: GEN_CLOSED
观察时点本例的状态发生的事情
调用 generate_results() 之后GEN_CREATED取得生成器对象;count = 0 尚未在函数体中执行
第一次 next() 返回后GEN_SUSPENDED交出 A,保存 count = 1 及暂停位置
第二次 next() 返回后GEN_SUSPENDED交出 B,保存 count = 2;还没执行末尾的打印
第三次 next()最终为 GEN_CLOSED继续执行,打印 body: end,结束并报告 StopIteration

正在执行时还有 GEN_RUNNING;本例从生成器外部、调用间隙检查,所以没有直接打印该状态。交出最后一个值时可能仍暂停,下一次推进才运行到结束。

普通函数的 return 结束调用;生成器的 yield 交出元素并暂停,return 或执行到末尾结束迭代。生成器 return 的值不会成为额外的 yield 元素;更进一步的例子见 Guidebook 阶段 6。

调用时的实参表达式仍先求值。例如 generate_results(make_tasks()) 若对应定义接收该参数,会先调用 make_tasks,再创建生成器对象;延迟的是生成器函数体的执行。

规则依据:yield 与恢复执行;生成器状态检查。

16 · 右侧还是要先求值,为什么 task() 没有马上执行?

在 results = (task() for task in tasks) 中,赋值规则没有改变。右侧立即求值得到的对象就是生成器,之后左侧名字绑定到它;右侧不是先算好一整份任务结果。

但“生成器是惰性的”也不能理解成右侧什么都没做。最左侧 for 的可迭代表达式会立即求值,并取得其迭代器;元素表达式 task() 随后在推进生成器时才执行。

生成器表达式会立即取得最外层迭代器

Python · 独立运行
def run_case():
    print("Running...")
    return "PASS"

class Tasks:
    def __iter__(self):
        print("__iter__")
        return iter([run_case, run_case])

print("before expression")
results = (task() for task in Tasks())
print("after expression")
print(next(results))
展开参考输出:先预测,再核对
before expression
__iter__
after expression
Running...
PASS

__iter__ 出现在 after expression 之前,说明构造阶段已取得迭代器;Running... 则等到 next(results) 才出现。下面的展开方式有助于理解时机:

Python · 机制展开,tasks 沿用前文
def generate_results(task_iterator):
    for task in task_iterator:
        yield task()

results = generate_results(iter(tasks))

把 iter(tasks) 放在调用参数里,是为了表现最外层迭代器立即取得的时间点。这个展开用于解释本例,不要求把生成器表达式想成预先存了一份所有结果的隐藏列表。

本例没有过滤条件,因此每次成功取到结果会运行一个任务。带过滤的生成器一次 next 可能检查多个输入;也有生成器始终不耗尽。不要把这个小例子的“一次取值对应一个任务”推广为所有生成器的规则。

规则依据:生成器表达式的求值时机。

17 · 有小括号,为什么不是元组?

你的问题:“(task() for task in tasks) 的右侧为什么不是元组?”要看完整语法。元组表达式中的逗号起决定作用;带 for 的这套语法是生成器表达式。

写法结果
(1)整数;括号用于分组
(1,)一个元素的元组
(1, 2)两个元素的元组
()空元组的特殊写法
(num for num in range(3))生成器对象
[num for num in range(3)]列表推导式,得到列表
{num for num in range(3)}集合推导式,得到集合

完整语法决定对象类型

Python · 独立运行
print(type((1)).__name__)
print(type((1,)).__name__)
print(type(()).__name__)

numbers = (num for num in range(3))
print(type(numbers).__name__)
print(tuple(numbers))
print(tuple(numbers))
展开参考输出:先预测,再核对
int
tuple
tuple
generator
(0, 1, 2)
()

tuple(numbers) 会消费迭代器来构造元组,因此第一次得到 (0, 1, 2),第二次面对已经耗尽的同一个生成器,只得到空元组。想保存函数对象可以用 (run_case, run_case);写成 (run_case(), run_case()) 就会立即调用两次。

类似地,tuple(task() for task in tasks) 虽然内部用了生成器,但外面的 tuple 构造过程会立即消费它,从而执行任务。set(...) 也是调用;空集合写 set(),{} 则是空字典。

规则依据:括号表达式;元组。

19 · 生成器也会耗尽吗?“只能遍历一次”该怎么理解?

会。生成器对象就是一种迭代器。同一个有限生成器走到结尾后不会自动从头开始;没有默认值的 next 会抛出 StopIteration,for 则把这个信号作为迭代正常结束。

耗尽后再次迭代不会自动重启

Python · 独立运行
numbers = (num for num in range(3))
print(next(numbers))
print(list(numbers))
print(list(numbers))
print(next(numbers, "DONE"))
print(iter(numbers) is numbers)

try:
    next(numbers)
except StopIteration as exc:
    print(type(exc).__name__)
展开参考输出:先预测,再核对
0
[1, 2]
[]
DONE
True
StopIteration

第一项 0 已经取走;第一次 list(numbers) 只收集剩下的 1、2,并继续推进到结束;第二次 list(numbers) 得到 []。带默认值的 next(numbers, "DONE") 在耗尽时返回默认值,省略默认值则会抛出异常。

“一次性”不等于只能调用一次 next,也不等于只能写一个 for。你可以先消费一项,暂停,之后继续消费剩余项;耗尽后,才没有后续元素可取。若 for 中提前 break,普通生成器也不会因此自动从头重启,之后通常可以继续推进它。

iter(numbers) is numbers 仍为 True,它不负责重置;已经耗尽的生成器还可以被名字引用、被打印,耗尽不等于对象立即消失。

规则依据:迭代器耗尽约定。

20 · 重新创建生成器,和把旧生成器重置,是同一回事吗?

重新执行生成器表达式,会得到一个新的生成器对象,再把名字绑定到它。原来的对象如果还有别名,仍保持原来的进度或耗尽状态。

新建生成器与重置上游是两件事

Python · 独立运行
tasks = [0, 1]
results = (item for item in tasks)
old = results
print(list(results))

results = (item for item in tasks)
print(results is old)
print(list(results))
print(list(old))

upstream = iter(tasks)
print(list(upstream))
wrapped = (item for item in upstream)
print(list(wrapped))
fresh = (item for item in iter(tasks))
print(list(fresh))
展开参考输出:先预测,再核对
[0, 1]
False
[0, 1]
[]
[0, 1]
[]
[0, 1]

前半部分的 tasks 是列表,可以再次取得新的列表迭代器;results 绑定新的生成器,old 仍指向旧的、已经耗尽的生成器。两者不是一个对象。

后半部分把列表的迭代器 upstream 先消费完了,再用生成器包它,得到的依然是空结果。新包装不会恢复已经耗尽的输入。需要从可重复遍历的原数据重新获取迭代器,或重新打开/构造数据来源。

如果表达式里是 task(),新建生成器再消费,会重新调用这些任务。它不自动记住上一次的结果;任务如果读取时间、网络或外部状态,第二次结果也未必相同。

21 · 想反复查看结果,要保存函数、生成器,还是结果列表?

取决于你希望重复的是“计算”还是“读取已算好的结果”。

需求可以保存什么需要知道的代价
之后选择时机再运行任务函数对象,或包含函数的列表每次显式调用都会执行任务
逐项计算,只处理需要的部分生成器,或任务迭代器加显式调用消费会推进状态;结果不会自动缓存
反复查看同一批已经计算的结果saved = list(results) 等结果容器立即消费剩余项,并占用保存这些结果所需的空间

保存结果可以复用,但只收集剩余项

Python · 独立运行
def run_case():
    print("Running...")
    return "PASS"

tasks = [run_case, run_case]
results = (task() for task in tasks)
saved = list(results)
print("saved once:", saved)
print("saved again:", saved)
print("generator now:", list(results))

numbers = (num for num in range(3))
print("taken:", next(numbers))
print("saved remaining:", list(numbers))
展开参考输出:先预测,再核对
Running...
Running...
saved once: ['PASS', 'PASS']
saved again: ['PASS', 'PASS']
generator now: []
taken: 0
saved remaining: [1, 2]

Running... 只在 list(results) 消费生成器时出现。之后两次打印 saved 都只是读取已保存的结果,不会再次调用任务。最后一段提醒:已经被 next 取走的 0 不会被自动补回,list 只取得尚未消费的 [1, 2]。

如果生成器永远没有结束,直接 list 它就无法正常收集完;对很大的结果流也应考虑内存。所以是否物化为列表,应由“是否需要反复访问全部结果”决定。

规则依据:迭代器消费与惰性计算。

22 · 你的手工笔记:哪些保留,哪些补上条件?

这张表对应你发来的原笔记。原来的观察大部分可以直接保留;修订集中在“是否新建”“何时执行”“是否重置”这几个词。

原笔记中的理解整理后的准确表述回看
执行前确定普通函数中名字的作用域类别 —— LEGB区分静态类别判定与运行时名字解析;局部未绑定不会自动退到 G01
右侧求值,得到一个新对象得到一个对象;是否新建由右侧表达式决定02
先求值右侧,再把结果赋给目标保留,适用于这里的普通赋值;与 += 等情况分别看02
== 规则可由 __eq__ 定义;is 判断身份保留,不把身份等同于值比较,也不把内存地址当作语言层定义04
函数末尾返回 None;打印内容不是返回值保留,明确是普通函数11
return None 还负责提前结束保留;分支中的它可以改成 return,不能随意整行删除11
[[...]] 表示递归引用,没有不断创建列表保留;... 是显示标记,真实元素仍引用原列表08
JSON 默认检查循环引用并报错保留;本例抛 ValueError,关闭检查也不能正常表达这个环08
循环垃圾回收补充引用计数保留 CPython 的限定,并检查不可达性;不保证立即回收07、08
[run_case] 保存函数,未执行函数体保留;名字查找和列表创建已经发生,调用时机由后续代码决定12
函数引用与函数对象可以分成名字/对象/关系三层保留;run_case 表达式求值得到函数对象,没有额外的引用包装对象09
results = [run_case()] 绑定整个列表,返回值成为元素保留;Running... 是右侧求值期间函数执行的副作用10
iter() 从可迭代对象创建迭代器改为“取得迭代器”;对符合协议的迭代器返回自身,不保证新建13
迭代器只能遍历一次,耗尽要重新创建可以分多次消费;耗尽后不自动重启,重新创建还要考虑上游能否重新提供数据19、20
生成器保留状态、暂停并逐项交出结果保留;函数列表加迭代器也能保存进度,生成器进一步封装结果生产14、15
函数体包含 yield,调用后先得到生成器对象保留,限定该 def 自身函数体;调用参数仍先求值15
逗号决定元组,(... for ... in ...) 是生成器表达式保留,补充空元组 () 与 tuple(...) 消费输入的行为17

这些修订都围绕一个习惯:先指出是哪一个表达式、哪一个对象、哪一份绑定、哪一次推进,再描述它发生了什么。

23 · 十道复盘题:先解释,再运行

下面是复习提示,答案默认折叠。读过文章、文章已发布,以及自己能独立解释,是不同的状态;学习地图中的复盘标记由你自己确认。

1. 为什么函数里先 print(results),后 results = [],可能连第一句都失败?

函数的整个代码块中有对 results 的绑定操作,且没有 global/nonlocal,所以它被分类为局部名。读取时这个局部名尚未绑定,抛 UnboundLocalError;不会临时借用模块里的 results。

2. a = [];b = a;a = []:创建了几个列表?b 是否改了指向?

两次 [] 创建两个列表。b = a 只建立绑定,不复制;第三句只让 a 绑定新列表,b 仍引用第一个。旧列表仍由 b 保留,因此这一例里两次观察到的 id(a) 不同。

3. 两个列表 == 为 True,能推出 is 为 True 吗?

不能。同样内容可以存在于两个不同列表中。要判断共享对象身份,需要看 is;自定义类型还可以定义自己的相等规则。

4. a.append(a) 之后为什么 len(a) 是 1?... 是真实元素吗?

append 只新增一个引用原列表的元素。... 是递归显示保护标记,真实元素不是 Ellipsis。是否出现无限递归,要看处理它的算法;环本身不是死锁。

5. [run_case]、[run_case()]、run_case() 分别得到什么?

第一项是包含函数对象的列表,不执行任务;第二项先调用,再得到包含返回值的列表;第三项得到函数返回值。函数打印的文字和返回值必须分别追踪。

6. 把 if 分支中的 return None 删除,为什么可能改变行为?

它既返回 None,也让调用提前结束。可以用裸 return 取得相同效果,整行删除后则会继续执行分支之后的语句。

7. 把函数放进列表,再用 next(task_iter)(),是否也是按需执行?

是。列表迭代器保存遍历进度,调用者决定何时调用取出的函数。生成器 results 则可把这两个动作封装起来,使 next(results) 直接返回任务结果;两者不是“一个必然全算,一个才能按需”。

8. results = (task() for task in tasks) 创建时,什么已发生,什么未发生?

已创建生成器,并立即求值最左侧可迭代表达式、取得其迭代器;本例尚未调用 task。推进到元素计算时才执行 task()。右侧已经求值,只是结果为生成器对象。

9. print(g) 两次、next(g) 两次、iter(g) 三者的作用有什么不同?

print(g) 显示同一生成器的对象描述;next(g) 开始或恢复迭代,取得下一项或报告结束;iter(g) 返回同一个生成器,不推进也不重置。

10. 耗尽后重建生成器,是否一定能重新拿到值?怎样避免重复计算?

要看上游。列表可以提供新迭代器,已耗尽的同一个上游迭代器则不能靠新包装恢复。若要复用已计算结果,可以在首次消费时保存它们;list(g) 只收集剩余项,并会使用保存这些项所需的内存。

建议复习方式:第一次只做 1–5,第二次做 6–10;每题用“对象、绑定、执行时机、进度”四个词检查自己的解释。回到 学习地图,可以沿本次新增的两条路径回看相关文章。

24 · 资料、实验范围与下一步

本文依据本次对话与用户提供的手工笔记原创整理。语言规则核对 Python 3.14 官方文档;文章中 22 个标注“独立运行”的完整实验在 CPython 3.12.14 中实际执行,并比对页面显示的预期输出。解释性片段、复盘变式和内存管理说明不声称经过完整跨实现验证。

检查直接提取文章里的代码,而不是只执行另一份相似示例。每个实验独立运行,检查退出码、标准错误、实际输出与显示输出,并记录源代码摘要。可在源码仓库运行 python3 scripts/verify_python_details.py;执行记录保存在 docs/verification/2026-10-20-python-details.json。

继续学习时可以从 Guidebook 阶段 1:闭包与默认参数理解函数如何保留环境,或者从 阶段 6:生成器状态继续实验 yield 与 return。先能解释本页的小时机差异,再扩展到装饰器、上下文管理器和协程。

PythonNamespace对象与引用函数对象迭代器生成器惰性求值复习笔记

整理于 2026-10-20 · 合并本次讨论与 Mokin 手工笔记。22 个完整实验在 CPython 3.12.14 中核验;语言规则参考 Python 3.14 官方文档。

MORE IN DEVELOPMENT3.1 数据结构分类:逻辑关系、存储方式与 Python 容器