Python 生成器用完了怎么办:可重复遍历需要重新创建入口

10-01 3阅读

导入任务先遍历一次检查格式,再遍历一次求和,结果却突然变成零。代码没有报错,数据文件也没有变化,问题可能就在函数接收的不是一份可重复读取的数据,而是一个已经走到末尾的生成器对象。重新写一个 for 循环,不会自动倒带。

先区分生成器函数与调用它得到的对象。函数保存了如何开始一段计算;生成器对象保存的是某一次计算进行到哪里。给同一个对象起第二个变量名,或再对它调用 iter,都不会制造第二次独立执行,这个区别必须在接口设计时说明。

Python 生成器用完了怎么办:可重复遍历需要重新创建入口

AI概念配图:用抽象物件说明本文讨论的关系,不是实际运行界面或测量结果。

用同一批数验证三种入口

下面程序只使用标准 Python,可保存为 demo.py 后运行。readings 通过 yield 逐个交出四个整数;summarize_once 故意先数个数再求和,复现迭代器耗尽之后再次读取没有任何元素的问题。零是 sum 对空输入的正常结果,所以很容易掩盖错误。

第二个函数改为接收工厂,每次遍历都调用一次。传入 readings 本身,可以获得独立的生成器;传入返回旧对象的 lambda,仍然会失败。代码将这个常见伪修复一并写成断言,避免把接口参数改名误当成已经解决生命周期问题。

def readings():
    yield from (2, 4, 6, 8)

def summarize_once(values):
    count = sum(1 for _ in values)
    return count, sum(values)

def summarize_factory(make_values):
    count = sum(1 for _ in make_values())
    return count, sum(make_values())

g = readings()
assert iter(g) is g
assert summarize_once(g) == (4, 0)
assert next(g, 'END') == 'END'
assert next(g, 'END') == 'END'
assert summarize_factory(readings) == (4, 20)
old = readings()
assert summarize_factory(lambda: old) == (4, 0)
print('exhausted: (4, 0); fresh factory: (4, 20)')

partial = readings()
assert next(partial) == 2
assert list(partial) == [4, 6, 8]
snapshot = tuple(readings())
assert summarize_once(snapshot) == (4, 20)
assert summarize_factory(lambda: iter(())) == (0, 0)

def one_pass(values):
    count = total = 0
    for value in values:
        count += 1
        total += value
    return count, total

assert one_pass(readings()) == (4, 20)
assert one_pass(iter(())) == (0, 0)
print('partial, empty, snapshot and one-pass checks passed')

部分消费也是可观察的状态

耗尽不是唯一值得检查的情形。预览函数拿走第一条以后,后续循环从第二条继续;break 只是结束当前循环,不会撤回已经取出的值。程序用 next 和 list 验证这一点,也检查走到末尾后继续 next 仍然报告没有更多数据。

在函数文档中写“接受可迭代对象”仍不够精确,因为列表与生成器都满足这个描述,却不具备相同的重放能力。如果内部只需要一遍,应直接在一次循环中累积数量和总和;如果确实需要多遍,就明确接收可重建输入的函数或有限快照。

一次循环的方案通常最朴素:每取到一个值,同时更新数量与总和。它不会反复访问外部来源,也不要求把整份输入放进内存。需要先确定总体阈值再做第二轮筛选的算法,则应承认两轮计算的需求,不能仅靠一个循环名称隐藏它。

重新执行与重放同一份数据不同

工厂能够重新创建读取过程,却不自动保证两次结果一致。如果来源是持续变化的文件、数据库查询或当前时间,每调用一次都可能读到新状态;如果读取带有计费或写入副作用,还会产生额外成本。这些属性需要由数据源契约决定。

当任务要求两轮严格使用同一批有限数据时,可先建立元组或列表快照,再重复遍历。代价是保留全部元素,而且元素若含可变对象,外层快照并不会自动冻结内部内容。本文使用整数,因此不会把复制对象内容的问题混在读取实验里。

对于文件读取工厂,要在每次调用内部打开文件并安排关闭,让资源寿命与该次遍历一致。不要把已经关闭的句柄或此前创建的生成器藏在闭包里,再声称这是新读取器;检查工厂真正执行的动作,比检查它是否可调用更有用。

把空输入也放进测试非常必要:没有记录与误读一个耗尽的对象,可能给出完全相同的统计结果。可以在业务入口保留输入批次标识和预期数量,让异常的空结果有机会被发现,同时避免为了调试而先打印 list,意外提前吃掉正式输入。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。