最近在梳理 iterator ,不得不说, 即使自己写了很多年的代码,我仍然没有在实际应用中看到自定义的迭代器。即使读了很多书,但是这些书中的示例大多是滥竽充数,不具备实际应用意义。所以顺着网线爬上 V 站请教各位。
如果一个对象定义了
__iter__()
方法或定义了__getitem__()
方法,那么这样的对象称为可迭代对象(iterable)。
如果一个对象定义了
__iter__()
方法和__next__()
方法,那么这样的对象称为迭代器(iterator)。
注:
1.后续的讨论都是基于以上两个定义。
2.因迭代器常和可迭代对象结合使用,故引如可迭代对象这一概念,但迭代器的概念先于生成器(generator),在后续的讨论中请勿涉及生成器。
python 3 的 range() 是一个可迭代对象,其实现使用了迭代器。使用迭代器后不是直接生成列表,节省了内存,体现了迭代器的应用意义。
《 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
该示例虽然创建了一个迭代器,但就功能而言其实就是“将奇数位置的字符放在前半段,将偶数位置的字符放在后半段”,完全没有必要使用迭代器。关于迭代器的实力,本人看到的大多是这样的毫无实际应用意义,令人深恶痛绝!
PEP 234 中写到 iterator 的 virtues 有:
- It provides an extensible iterator interface.
- It allows performance enhancements to list iteration.
- It allows big performance enhancements to dictionary iteration.
- It allows one to provide an interface for just iteration without pretending to provide random access to elements.
- 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
}.- It makes code iterating over non-sequence collections more concise and readable.
中译版:
如果包含该提案的所有部分,则会以一致且灵活的方式解决许多问题。其主要优点包括以下四点不,五点不,六点
- 它提供了一个可扩展的迭代器接口。
- 它允许对列表迭代进行性能优化。
- 它允许对字典迭代进行大幅度性能提升。
- 它允许为仅迭代提供接口,而无需假装提供对元素的随机访问。
- 它与所有现有的用户定义类和模拟序列和映射的扩展对象向后兼容,即使是仅实现了 {
__getitem__
,keys
,values
,items
} 子集的映射。- 它使遍历非序列集合的代码更加简洁易读。
上面所列出的优点较抽象,各位能否提供一些具体的例子?
各位在实际应用中是否自己实现过迭代器?如果有麻烦提供一些例子。
[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/
1 wn990916 5 小时 56 分钟前 AI 的回答: **迭代器产生的原因**,主要源于以下几点实际需求和技术演进: - **统一数据遍历的接口和规范** 早期,遍历不同容器(列表、字典、集合等)通常写不同代码,如果能抽象出一致的“遍历方式”,开发效率和代码通用性会大幅提升。迭代器协议就做到了这一点无论底层是啥容器,只要实现了 `__iter__()` 和 `__next__()`,都能用 for 循环等统一方式遍历。 - **节省内存与懒加载** 数据量特别大或来源延迟的场景,如读取大文件、数据库游标、网络流,不能一次性全部加载到内存。迭代器允许“按需一条条产生数据”,极大提高了资源利用率与性能。 - **支持生成无限序列** 某些算法或业务场景仅需下一个数据,比如生成斐波那契数列、随机数、实时传感器数据流。迭代器天生支持“懒惰生成”,可以用很少的资源处理无限数据流。 - **简化复杂遍历逻辑** 对于树、图等复杂数据结构,传统遍历实现难以通用。通过迭代器,可以封装自己的遍历规则,外部用 for 统一访问,极大提升了灵活性和代码复用。 - **提升代码表达力和可读性** 迭代器结合 for 、map 、filter 、生成器等语法,大幅简化了数据处理链式表达。例如 Python 的生成器 yield 表达法,代码优雅可读。 - **底层技术支撑面向对象和协议设计** 迭代器协议是一种“接口化”的思想,有助于构建高解耦的代码架构,也是诸如 Java 、C# 等主流语言的标准特性。 简单来说,**迭代器**解决了“如何统一、高效、安全地访问数据集合”的问题,为现代编程语言的数据处理提供了基础机制。 |
![]() | 2 momocraft 5 小时 45 分钟前 ![]() 一个能从里面拿对象出来的东西就可以是 iterator 不管什么对象,不管是又穷还是无穷 如果你有个可以拉出鸽子和兔子的帽子,这个帽子就是 iterator 字节流是,字符串流是,一次 sql 查询结果是,dict 和 range 和 map 和 list 和 tuple 都是,机器学习读训练数据的 DataLoader 也是 不发明新种类不等于用不上 |
3 Alias4ck 5 小时 34 分钟前 ![]() Lazy evaluation |
4 craftsmanship 5 小时 29 分钟前 via Android ![]() @Livid 1 楼复制粘贴 AI 生成的技术类内容 |
![]() | 5 seers 5 小时 11 分钟前 ![]() 用有限对抗无限的栅栏 |
6 kneo 5 小时 0 分钟前 via Android ![]() 迭代器是给库作者用的。你用的语言的标准库应该就有大量应用。 普通程序员学会 list 和 dict 就够用了。 |
7 Hstar 4 小时 56 分钟前 当你有迭代需求,简单来说就是需要用 for ... in ... 来挨个读取某个东西的时候,就可以把这个东西写成一个迭代器。你用不到说明你遇到这种情况的时候,都会事先把需要的这个东西变成一个 list ,所以就不需要实现迭代器了。 我举个例子,当一个对象 A 有个 keys 属性,包含了好多 key ,然后有个 map 存储各个 key 指定的 value 。但是突然你需要用 for value in A 这样的写法来遍历各个 key 的 value 时但你又不想拿到 key ,你就可以写一个关于 value 的迭代器。 |
8 fds 4 小时 56 分钟前 ![]() 确实应用场景很少,而且也不是必须用迭代器不可。比如模拟 4 个人打牌的场景。你就可以给每人单独运行个迭代器,每次输出本轮打什么牌。迭代器就相当于一个闭包,把一个人当前有哪些牌的数据,正在用什么策略走到第几步之类的逻辑都包装起来了,你这边只要定期拿到输出即可。这样代码逻辑就正交了起来。 |
9 bytesfold 4 小时 53 分钟前 via iPhone ![]() 读大文件啊,总不能全部 load 到内存吧; 你们举例也太抽象了 |
10 Martin123123 4 小时 52 分钟前 ![]() 举一个很简单但是一般业务场景不会使用的例子,比如你有一个任务负责从队列中获取最新的记录进行处理但是有一些通用的处理方法时就有用,假设你的消费端需要的是一个 pydantic 对象,可以用类型注释传进去序列化后再触发方法,如果消息不合规,则通过 MQ 路由或者其他方式去进行消费,甚至于你可以穿一个 match 的方法自己去实现一个路由 |
11 henix 4 小时 49 分钟前 ![]() 感觉迭代器主要用于声明一些接口的参数类型 例如,[any]( https://docs.python.org/3/library/functions.html#any) 的第一个参数,如果是 list 的话那必须全部展开,把每个值都算出来,但有时候不想算全部的值,或者出于性能考虑不想往后面算 所以像 any, all 这类函数,参数类型声明为 Iterable 表明其对参数的要求比 List 更弱,只需要一个可迭代对象即可,可以是 list 也可以是 set 更不必说内置函数的 map, filter, zip 等的参数类型都是 Iterable 还有内置的 itertools 包提供了很多强大功能,如 itertools.product 计算任意多序列的笛卡尔积 |
12 neoblackcap 4 小时 47 分钟前 ![]() 迭代器的使用实际上就是面向接口编程。作为库或者其他函数提供者,你不再需要了解传入的参数是不是一个列表。只要能用迭代器对应的方法即可。 |
13 crackidz 4 小时 41 分钟前 ![]() 或者我们可以让在一个具体的场景里面理解这个问题,举个例子,你需要从远程数据库中大量的数据。当然,你可以选择一次性把所有的数据都读取出来,另外一种做法是返回一个 cursor ,然后根据游标去 iter 这个表里的数据。iter 在这个时候的的好处是,你无需一次性的占用大量的内存和网络带宽/IO 用于传输存储数据,尤其是没有 patch 的同步环境下可能导致的长时间阻塞 |
![]() | 14 Cooky 4 小时 35 分钟前 ![]() 延迟求值 |
15 henix 4 小时 29 分钟前 如果我要实现类似 Makefile 的功能:一个文件的内容依赖于若干其他文件,当这些依赖的文件的任意一个的修改时间比目标文件新,就执行生成指令 假设 modify_time 函数可以获取文件的修改时间,使用如下 generator expression: ``` if any(modify_time(dep) > target_mtime for dep in deps): ``` 可以表达“只要有一个依赖文件比目标文件新”,后面的文件都可以不用打开(不调用 modify_time ) |
![]() | 16 codists OP 作为一个已经工作了 8 年的程序员,看到大家的回答,虽然心里......but "love and peace,谢谢大家"。我们限定一下问题“大家在实际工作中自己实现过的迭代器是怎么样的?” |
17 aloxaf 4 小时 20 分钟前 ![]() 不知道为什么,楼主的语气给我一种故作谦虚但内心高傲的感觉…… |
![]() | 18 Ketteiron 4 小时 11 分钟前 ![]() 自定义迭代器主要有几个场景,一是修改可迭代数据的行为,例如将有序列表对象以乱序读取,例如给一个 gif 抽帧;二是给不可迭代数据添加一个迭代行为,例如可以把一棵树以某种策略输出为扁平化列表;三是迭代行为与其他逻辑紧密相连,例如读取文件信息、写入数据、关闭资源,下一个文件的选择、处理方式与上一个有关,这么一坨东西可以放迭代器里。 对外部模块来说,这一部分复杂度就转移进迭代器里了,它不用关心多余逻辑,只管 for 就行了,本质上是抽象与组织代码的权衡。 相比生成器,迭代器的优势场景在于: 1. 维护复杂的内部状态管理,简单依靠恢复/暂停不好实现,例如要维护一个状态机。 2. 要公开某些状态给外部,例如公开进度条、统计信息。 3. 行为逻辑更加 OOP ,如果你的 Python 代码充满了 OOP 的味道会更合适。 其余情况还是用生成器更好。 当然,并非一定要用迭代器/生成器实现,完全不使用也照样能做,取决于个人习惯。你说实际应用中没看过自定义的迭代器,非常正常,因为该程序员不希望把逻辑放进迭代器的内部,这本质上是垃圾放一楼还是二楼的问题,其实没有区别。编写代码有 114514 种抽象方法,你不用 xxx 代码照写世界照常运转并没什么影响,唯一影响是你没尝试过不能切身判断它到底好不好,适不适合你或者你的代码。 |
19 Martin123123 4 小时 9 分钟前 @codists 其实我认为工作中用不到,很大一部分原因是可读性、维护性的原因,如果团队中的成员水平参差不齐,过分的追求语法糖只会增加自己的工作量 |
20 aloxaf 3 小时 41 分钟前 @codists 考虑到楼主特地排除了生成器,我猜楼主真正想问的是不是「**手动**定义迭代器的实际场景」? 如果是这个问题,我好像确实没有在 Python 中实际这么干过,因为生成器已经足够好用了。在某些有迭代器但没有生成器的语言(如 Rust )中,才会常常需要手动定义迭代器。 |
![]() | 21 Livid MOD PRO @craftsmanship 谢谢,1 楼的账号已经被彻底 ban 。 |