为何不应立刻将所有iterables转换为list这类具有已定义长度的对象?
很多人写Python时会形成条件反射:拿到任何可迭代对象(生成器、迭代器、map/filter返回值、文件流、数据库游标这类),第一反应就是套上list()转成列表,觉得列表能索引、能反复遍历、用着顺手。但这种不加判断就立刻转list的做法,其实会引入很多完全没必要的性能开销,具体不推荐的原因主要有这些:
平白增加无意义的内存占用
大多数惰性可迭代对象本身不会一次性把全部值加载到内存里——它们只会在你每次迭代取值的时候,临时计算生成当前需要的那个元素,处理完就会把对应的内存释放掉。
如果你上来就转成list,相当于强制把全量元素一次性全部加载进内存,还要占用一块连续的内存空间存下所有元素的引用。
- 最常见的踩坑场景:逐行读取几个G大小的日志文件做关键词匹配,本来用
for line in open("big.log"):逐行处理,全程内存占用只有单行文本的大小;如果一开始就写list(open("big.log")),会直接把整个文件的所有行全部读进内存,轻则程序运行卡顿,重则直接触发内存溢出崩溃。 - 哪怕是体量不大的可迭代对象,这种转list操作也会额外占一块内存存序列指针,除了方便没有任何实际收益。
白白浪费惰性迭代器的单次消费特性
迭代器本身的设计逻辑就是“一次遍历、用完即弃”:内部的遍历指针会随着取值不断往前移动,第一次遍历完成后,指针就停在序列末尾,再次循环不会返回任何值。
很多人会觉得“转成list不就可以反复遍历了吗”,但实际上绝大多数场景下你根本不需要对同一个序列反复遍历:
- 比如你要算生成器生成的千万级整数的平均值,本来可以边遍历边累加求和、同时计数,全程不需要把所有数字存下来,遍历完直接就能算出结果;如果一开始就转成list,相当于先把千万个数字全部存到内存里,多做了一次毫无意义的内存拷贝。
- 再比如对
map()、filter()返回的处理结果,很多人上来就转list,其实直接把这个可迭代对象丢给sum()、max()、any()这类一次性消费的函数,根本不需要生成中间的列表对象。
提前触发所有无关计算,平白增加延迟
惰性求值的核心优势,就是把元素的计算推迟到真正需要它的时候才执行。如果你拿到可迭代对象立刻就转成list,会强制触发所有元素的全量计算,哪怕你后续根本用不到那么多值。
- 典型例子:用
itertools.count()生成无限递增的整数序列,本来你可以用takewhile(lambda x: x < 10, count())拿到小于10的数就主动停止遍历,根本不会计算10以上的数值;如果上来就套list(),代码会直接卡死——因为它要把无限长的序列全部计算完才能生成列表,永远不会停下来。 - 哪怕不是无限序列,如果你只需要取可迭代对象的前3个值做逻辑判断,转list的操作也会硬生生把后面所有元素全部计算、加载完,耽误半天时间就为了拿前3个值,完全是无效等待。
缩窄代码的适用场景,降低通用性
如果你写的工具函数内部,一收到可迭代参数就立刻转成list,会把函数能适配的场景大幅缩窄。
- 本来只要你全程保持流式遍历的逻辑,不做全量加载,函数是可以直接适配无限生成器、超大文件流、数据库查询流这类大体积输入的;但只要你在函数入口写了一句
iterable = list(iterable),任何大流量的流式输入传到这个函数里,都会直接因为全量加载触发内存溢出。 - 另外list本身是可变对象,转成列表之后,后续代码很容易出现误修改序列的问题,反而破坏了原有可迭代对象的只读、一次性使用的特性。
注:我们反对的从来不是“必要时转list”,而是“拿到可迭代对象就条件反射立刻转list”的习惯。如果你确实需要随机索引、多次遍历、修改序列元素,那转list完全是合理的;但如果没有这些明确需求,优先直接使用可迭代对象本身,会比无脑转list的性能好得多。
内容的提问来源于stack exchange,提问作者TheGuy

