Python中迭代器的实际应用场景是什么?

概述

最近在梳理 iterator ,不得不说, 即使自己写了很多年的代码,我仍然没有在实际应用中看到自定义的迭代器。即使读了很多书,但是这些书中的示例大多是滥竽充数,不具备实际应用意义。所以顺着网线爬上 V 站请教各位。

可迭代对象 & 迭代器定义

可迭代对象

如果一个对象定义了 __iter__() 方法或定义了 __getitem__() 方法,那么这样的对象称为可迭代对象(iterable)。

迭代器

如果一个对象定义了 __iter__() 方法和 __next__() 方法,那么这样的对象称为迭代器(iterator)。

1.后续的讨论都是基于以上两个定义。

2.因迭代器常和可迭代对象结合使用,故引如可迭代对象这一概念,但迭代器的概念先于生成器(generator),在后续的讨论中请勿涉及生成器。

一些示例

示例 1

python 3 的 range() 是一个可迭代对象,其实现使用了迭代器。使用迭代器后不是直接生成列表,节省了内存,体现了迭代器的应用意义。

示例 2

《 Learn Python Programming(4th)》 第 246 页:

class OddEven:
    def __init__(self, data):
        self._data = data
        self.indexes = list(range(0, len(data), 2)) + list(range(1, len(data), 2))
def __iter__(self):
    return self

def __next__(self):
    if self.indexes:
        return self._data[self.indexes.pop(0)]
    raise StopIteration

Testing the OddEven class

oddeven = OddEven(“0123456789”) print("".join(c for c in oddeven)) # 0246813579

oddeven = OddEven(“ABCD”) # or manually… it = iter(oddeven) # this calls oddeven.iter internally print(next(it)) # A print(next(it)) # C print(next(it)) # B print(next(it)) # D

该示例虽然创建了一个迭代器,但就功能而言其实就是“将奇数位置的字符放在前半段,将偶数位置的字符放在后半段”,完全没有必要使用迭代器。关于迭代器的实力,本人看到的大多是这样的——毫无实际应用意义,令人深恶痛绝!

问题

问题 1

PEP 234 中写到 iterator 的 virtues 有:

  1. It provides an extensible iterator interface.
  2. It allows performance enhancements to list iteration.
  3. It allows big performance enhancements to dictionary iteration.
  4. It allows one to provide an interface for just iteration without pretending to provide random access to elements.
  5. It is backward-compatible with all existing user-defined classes and extension objects that emulate sequences and mappings, even mappings that only implement a subset of {__getitem__, keys, values, items}.
  6. It makes code iterating over non-sequence collections more concise and readable.

中译版:

如果包含该提案的所有部分,则会以一致且灵活的方式解决许多问题。其主要优点包括以下四点——不,五点——不,六点

  1. 它提供了一个可扩展的迭代器接口。
  2. 它允许对列表迭代进行性能优化。
  3. 它允许对字典迭代进行大幅度性能提升。
  4. 它允许为仅迭代提供接口,而无需假装提供对元素的随机访问。
  5. 它与所有现有的用户定义类和模拟序列和映射的扩展对象向后兼容,即使是仅实现了 {__getitem__, keys, values, items} 子集的映射。
  6. 它使遍历非序列集合的代码更加简洁易读。

上面所列出的优点较抽象,各位能否提供一些具体的例子?

问题 2

各位在实际应用中是否自己实现过迭代器?如果有麻烦提供一些例子。

参考资料

[1] Python Document Glossary ,iterator: https://docs.python.org/3/glossary.html#term-iterator

[2] PEP 234 – Iterators: https://peps.python.org/pep-0234

[3] PEP 234 – 迭代器: https://peps.pythonlang.cn/pep-0234/


Python中迭代器的实际应用场景是什么?

35 回复

迭代器在Python里最实用的地方就是处理流式数据或者大数据集,特别是当你不想一次性把所有数据都加载到内存里的时候。

举个最常见的例子,读取大文件。用open()打开文件拿到的就是迭代器,可以一行一行读,内存里永远只存当前这一行:

with open('huge_log.txt') as f:
    for line in f:  # 这里f就是迭代器
        process(line)

另一个典型场景是数据库查询。像SQLAlchemy这样的ORM,查询结果返回的就是迭代器,数据是随着遍历从数据库里逐步取出来的:

for user in session.query(User).filter(User.active == True):
    send_notification(user)

自己写迭代器也简单,用生成器函数最方便。比如要生成无限序列或者处理管道数据:

def read_sensor():
    while True:
        yield get_sensor_data()  # 每次yield返回一个值

def batch_process(items, batch_size=100):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == batch_size:
            yield batch
            batch = []
    if batch:
        yield batch

内置的range()zip()map()这些返回的都是迭代器,惰性求值省内存。itertools模块里还有一堆现成的迭代器工具,像chain()拼接多个可迭代对象、islice()做切片,用起来很顺手。

总结:迭代器主要用来省内存和处理流数据。


一个能从里面拿对象出来的东西就可以是 iterator
不管什么对象,不管是又穷还是无穷
如果你有个可以拉出鸽子和兔子的帽子,这个帽子就是 iterator

字节流是,字符串流是,一次 sql 查询结果是,dict 和 range 和 map 和 list 和 tuple 都是,机器学习读训练数据的 DataLoader 也是

不发明新种类不等于用不上

Lazy evaluation

1 楼复制粘贴 AI 生成的技术类内容

用有限对抗无限的栅栏

迭代器是给库作者用的。你用的语言的标准库应该就有大量应用。
普通程序员学会 list 和 dict 就够用了。

当你有迭代需求,简单来说就是需要用 for … in … 来挨个读取某个东西的时候,就可以把这个东西写成一个迭代器。你用不到说明你遇到这种情况的时候,都会事先把需要的这个东西变成一个 list ,所以就不需要实现迭代器了。
我举个例子,当一个对象 A 有个 keys 属性,包含了好多 key ,然后有个 map 存储各个 key 指定的 value 。但是突然你需要用 for value in A 这样的写法来遍历各个 key 的 value 时但你又不想拿到 key ,你就可以写一个关于 value 的迭代器。

确实应用场景很少,而且也不是必须用迭代器不可。比如模拟 4 个人打牌的场景。你就可以给每人单独运行个迭代器,每次输出本轮打什么牌。迭代器就相当于一个闭包,把一个人当前有哪些牌的数据,正在用什么策略走到第几步之类的逻辑都包装起来了,你这边只要定期拿到输出即可。这样代码逻辑就正交了起来。

读大文件啊,总不能全部 load 到内存吧;
你们举例也太抽象了

举一个很简单但是一般业务场景不会使用的例子,比如你有一个任务负责从队列中获取最新的记录进行处理但是有一些通用的处理方法时就有用,假设你的消费端需要的是一个 pydantic 对象,可以用类型注释传进去序列化后再触发方法,如果消息不合规,则通过 MQ 路由或者其他方式去进行消费,甚至于你可以穿一个 match 的方法自己去实现一个路由

感觉迭代器主要用于声明一些接口的参数类型
例如,[any]( https://docs.python.org/3/library/functions.html#any) 的第一个参数,如果是 list 的话那必须全部展开,把每个值都算出来,但有时候不想算全部的值,或者出于性能考虑不想往后面算
所以像 any, all 这类函数,参数类型声明为 Iterable 表明其对参数的要求比 List 更弱,只需要一个可迭代对象即可,可以是 list 也可以是 set
更不必说内置函数的 map, filter, zip 等的参数类型都是 Iterable
还有内置的 itertools 包提供了很多强大功能,如 itertools.product 计算任意多序列的笛卡尔积

迭代器的使用实际上就是面向接口编程。作为库或者其他函数提供者,你不再需要了解传入的参数是不是一个列表。只要能用迭代器对应的方法即可。

或者我们可以让在一个具体的场景里面理解这个问题,举个例子,你需要从远程数据库中大量的数据。当然,你可以选择一次性把所有的数据都读取出来,另外一种做法是返回一个 cursor ,然后根据游标去 iter 这个表里的数据。iter 在这个时候的的好处是,你无需一次性的占用大量的内存和网络带宽/IO 用于传输存储数据,尤其是没有 patch 的同步环境下可能导致的长时间阻塞

延迟求值

如果我要实现类似 Makefile 的功能:一个文件的内容依赖于若干其他文件,当这些依赖的文件的任意一个的修改时间比目标文件新,就执行生成指令
假设 modify_time 函数可以获取文件的修改时间,使用如下 generator expression:
<br>if any(modify_time(dep) &gt; target_mtime for dep in deps):<br>
可以表达“只要有一个依赖文件比目标文件新”,后面的文件都可以不用打开(不调用 modify_time )

作为一个已经工作了 8 年的程序员,看到大家的回答,虽然心里…but “love and peace,谢谢大家”。我们限定一下问题————“大家在实际工作中自己实现过的迭代器是怎么样的?”

不知道为什么,楼主的语气给我一种故作谦虚但内心高傲的感觉……

自定义迭代器主要有几个场景,一是修改可迭代数据的行为,例如将有序列表对象以乱序读取,例如给一个 gif 抽帧;二是给不可迭代数据添加一个迭代行为,例如可以把一棵树以某种策略输出为扁平化列表;三是迭代行为与其他逻辑紧密相连,例如读取文件信息、写入数据、关闭资源,下一个文件的选择、处理方式与上一个有关,这么一坨东西可以放迭代器里。
对外部模块来说,这一部分复杂度就转移进迭代器里了,它不用关心多余逻辑,只管 for 就行了,本质上是抽象与组织代码的权衡。

相比生成器,迭代器的优势场景在于:
1. 维护复杂的内部状态管理,简单依靠恢复/暂停不好实现,例如要维护一个状态机。
2. 要公开某些状态给外部,例如公开进度条、统计信息。
3. 行为逻辑更加 OOP ,如果你的 Python 代码充满了 OOP 的味道会更合适。
其余情况还是用生成器更好。

当然,并非一定要用迭代器/生成器实现,完全不使用也照样能做,取决于个人习惯。你说实际应用中没看过自定义的迭代器,非常正常,因为该程序员不希望把逻辑放进迭代器的内部,这本质上是垃圾放一楼还是二楼的问题,其实没有区别。编写代码有 114514 种抽象方法,你不用 xxx 代码照写世界照常运转并没什么影响,唯一影响是你没尝试过不能切身判断它到底好不好,适不适合你或者你的代码。

其实我认为工作中用不到,很大一部分原因是可读性、维护性的原因,如果团队中的成员水平参差不齐,过分的追求语法糖只会增加自己的工作量

考虑到楼主特地排除了生成器,我猜楼主真正想问的是不是「手动定义迭代器的实际场景」?

如果是这个问题,我好像确实没有在 Python 中实际这么干过,因为生成器已经足够好用了。在某些有迭代器但没有生成器的语言(如 Rust )中,才会常常需要手动定义迭代器。

谢谢,1 楼的账号已经被彻底 ban 。

#17 毕竟工作八年,那很牛了

#16

99%的码农自己写的迭代器, 都是因为上下游接口要求你传递过去的东西必须是可迭代的…

具体就根据上下游的要求来了

生产者消费者模式?手搓一个消息队列?

你要写迭代器的论文非得找个实践的例子?自定义迭代器__iter__是基本数据结构用的,实际用的话一般都是基于基本结构写 yield 生成器函数

如果你需要处理所有的迭代器返回值,那迭代器就是节约了一些资源的负载峰值。
如果你未必需要处理所有迭代器返回值,只是顺序查找某个合适的返回值,那迭代器节约了大量的资源。

好比,你做个跳舞链来做精确覆盖。那肯定用迭代器。陆续返回客户所需要的解,让用户去判断是否继续查找解。

有时候写爬虫,需要翻页,我就用迭代器。函数内部判断需不需要翻页,能够一定程度上提升可读性,写起来也方便。有点语法糖的意思。

我主要省内存吧 我们原来修一些数据 bug 十几亿数据读出来 原来不知道怎么处理 一会脚本挂了 又搞什么内存提高使用 监控 cpu 啥的 后来用迭代器 内存不增长

我要处理的数据量很大,单个数据也很大,不想先生成都放到内存里,用迭代器模拟数组,可以无缝嵌入现有的逻辑

兄弟,你是没用过 async await 吗?

就算你是学生没用过 async await 。那你刷 leetcode 没用过 range 吗?如果你是竞赛生你刷 codeforce 没用过迭代器递归转迭代吗?

和 async/await 有啥关系? iterator 不是很古代的时候就已经有了吗?

最开始是为了 lazy load 。consumer 调用时 producer 再生产数据。特性和协程很像,所以 python 最开始引入 coroutine 的时候底层就是用 iterator 实现的,古早代码可以看见很多和 iterator 语法混用。

#21 一次回答就彻底 BAN 了, 会不会太严了?

感觉 op 的目的是想看看在实际项目中,具体是怎么自定义迭代器的(实现__iter__,__next__这种 magic 函数),这种应该非常少见。大部分不是自定义,是通过 yield 方式来实现的

回到顶部