本文面向已有前端开发基础、正在学习 Python 的开发者。
迭代器和生成器解决的是同一个问题:数据不一定要一次性全部准备好,可以在需要的时候一个一个取出来。前端里最接近的经验是 for...of、Symbol.iterator、生成器函数 function* 和 yield。
这几个概念可以先合在一起记:
- 可迭代对象表示“可以被遍历的数据源”
- 迭代器表示“真正负责一步一步取值的对象”
- 生成器表示“用
yield快速创建出来的迭代器”
后面的for循环,本质上就是先从可迭代对象拿到迭代器,再不断从迭代器里取下一个值。
一、先把概念边界讲清楚
先记住一条主线:
1for item in obj 2 -> 先调用 iter(obj) 拿到迭代器 3 -> 再不断调用 next(迭代器) 4 -> 遇到 StopIteration 后结束 5
所以这几个概念可以这样分:
| 概念 | 关注点 | Python | JavaScript |
|---|---|---|---|
| 可迭代对象 | 能不能开始遍历 | 能被 iter() 接受 | 有 Symbol.iterator |
| 迭代器 | 这次遍历走到哪里了 | 能被 next() 调用 | 有 next() |
| 迭代协议 | 遍历接口怎么约定 | iter() -> next() -> 结束时抛异常 | Symbol.iterator -> next() |
| 生成器 | 怎么快速创建迭代器 | 函数体里写 yield | function* + yield |
两门语言只是接口名字和结束方式不同:
1Python: 2可迭代对象 -- iter() --> 迭代器 -- next() --> value / StopIteration 3 4JS: 5可迭代对象 -- Symbol.iterator() --> 迭代器 -- next() --> { value, done } 6
最关键的边界是:可迭代对象表示“能开始一次遍历”,迭代器表示“这次遍历本身”。列表、字符串这类可迭代对象可以反复遍历,因为每次都能创建新的迭代器;已经创建出来的迭代器通常只能向前走,取过的值不会自动回到起点。
生成器不算新的遍历体系,它只是更省事的迭代器写法。手写迭代器要自己维护位置和结束条件;生成器用 yield 保存暂停点,每次 next() 都从上一次暂停的位置继续执行。
是不是有点懵😳
二、从 for 循环看迭代过程
JavaScript 里,一个对象只要实现了 Symbol.iterator,就可以被 for...of 消费。
1const names = ["张三", "李四", "王五"]; 2 3for (const name of names) { 4 console.log(name); 5} 6
如果拆开看,for...of 背后大概做了这些事:
1const iterator = names[Symbol.iterator](); 2 3console.log(iterator.next()); // { value: '张三', done: false } 4console.log(iterator.next()); // { value: '李四', done: false } 5console.log(iterator.next()); // { value: '王五', done: false } 6console.log(iterator.next()); // { value: undefined, done: true } 7
所以前端里有两层概念:
| 概念 | 判断方式 | 作用 |
|---|---|---|
| iterable | 有 Symbol.iterator | 可以交给 for...of |
| iterator | 有 next() | 可以一步一步取值 |
Python 也有这两层,只是名字和结束方式不同:
| JavaScript | Python |
|---|---|
| obj[Symbol.iterator]() | iter(obj) |
| iterator.next() | next(iterator) |
| 返回 { value, done } | 返回本次值 |
| done: true 表示结束 | 抛出 StopIteration 表示结束 |
把这个对照关系记住,后面的 Python 语法就会清楚很多。
三、可迭代对象 iterable
可迭代对象就是:能被 for 循环遍历的对象。
1names = ['张三', '李四', '王五'] 2cities = ('北京', '上海', '深圳') 3msg = 'hello' 4 5for name in names: 6 print(name) 7
这些对象都能被 for 遍历,所以它们都是可迭代对象。
从协议角度看,可迭代对象要能被 iter() 接受:
1names = ['张三', '李四', '王五'] 2msg = 'hello' 3age = 18 4 5print(iter(names)) # list_iterator 6print(iter(msg)) # str_iterator 7 8# print(iter(age)) # TypeError: 'int' object is not iterable 9
也可以用 hasattr 粗略观察:
1names = ['张三', '李四', '王五'] 2msg = 'hello' 3age = 18 4 5print(hasattr(names, '__iter__')) # True 6print(hasattr(msg, '__iter__')) # True 7print(hasattr(age, '__iter__')) # False 8
这里的 __iter__ 是 Python 的魔法方法。平时开发一般不直接写 names.__iter__(),而是用内置函数 iter(names)。
1obj.__iter__() 2 -> 底层魔法方法 3 4iter(obj) 5 -> 日常使用方式 6 -> 内部会调用 obj.__iter__() 7
四、迭代器 iterator
调用 iter(可迭代对象) 之后,会得到一个迭代器。
1names = ['张三', '李四', '王五'] 2 3it = iter(names) 4 5print(next(it)) # 张三 6print(next(it)) # 李四 7print(next(it)) # 王五 8print(next(it)) # StopIteration 9
迭代器的核心能力是:记住当前取到哪里了,每次 next() 返回下一个值。
也就是说,迭代器内部有状态,类似一个指针:
1初始位置 2 -> next() 取第 1 个 3 -> next() 取第 2 个 4 -> next() 取第 3 个 5 -> 没有数据了,抛 StopIteration 6
如果用 while 手动模拟 for,大概是这样:
1names = ['张三', '李四', '王五'] 2 3it = iter(names) 4 5while True: 6 try: 7 item = next(it) 8 print(item) 9 except StopIteration: 10 break 11
所以 for item in names 并不神秘,它背后就是:
1先调用 iter(names) 得到迭代器 2再不断调用 next(迭代器) 3遇到 StopIteration 后结束循环 4
迭代器自己也是可迭代对象
迭代器一般也有 __iter__ 方法,并且返回自己。
1names = ['张三', '李四', '王五'] 2 3it = iter(names) 4 5print(iter(it) is it) # True 6
这样设计的原因是:for 循环第一步一定会调用 iter(x)。如果传进去的已经是迭代器,iter(迭代器) 必须也能正常工作。
迭代器会被消耗
迭代器不是列表,它是一次性向前取值的过程。
1names = ['张三', '李四', '王五'] 2 3it = iter(names) 4 5print(next(it)) # 张三 6 7for name in it: 8 print(name) 9 10# 只会继续输出: 11# 李四 12# 王五 13
前面已经被 next(it) 取走的值,不会在后面的 for 里重新出现。
这点很像前端里已经调用过几次 iterator.next() 后,再继续 for...of 或继续 .next(),状态会接着往后走,而不是自动重置。
五、自定义可迭代对象
如果希望自己的类能被 for 遍历,就要实现迭代器协议。
需求:让 Person 实例可以被遍历,依次取出姓名、年龄、性别、地址。
1p1 = Person('张三', 18, '男', '北京昌平') 2 3for item in p1: 4 print(item) 5
写法一:对象和迭代器分开
这种写法最清晰:Person 负责保存业务数据,PersonIterator 负责遍历过程。
1class Person: 2 def __init__(self, name, age, gender, address): 3 self.name = name 4 self.age = age 5 self.gender = gender 6 self.address = address 7 8 def __iter__(self): 9 # 返回一个专门负责遍历 Person 的迭代器 10 return PersonIterator(self) 11 12 13class PersonIterator: 14 def __init__(self, person): 15 # 保存外部传进来的 Person 对象 16 self.person = person 17 # 记录当前取到哪个位置 18 self.index = 0 19 # 配置要遍历哪些字段 20 self.attrs = [ 21 person.name, 22 person.age, 23 person.gender, 24 person.address, 25 ] 26 27 def __iter__(self): 28 # 迭代器的 __iter__ 返回自己 29 return self 30 31 def __next__(self): 32 if self.index >= len(self.attrs): 33 raise StopIteration 34 35 value = self.attrs[self.index] 36 self.index += 1 37 return value 38
执行:
1p1 = Person('张三', 18, '男', '北京昌平') 2 3for item in p1: 4 print(item) 5
输出:
1张三 218 3男 4北京昌平 5
这个写法适合业务对象比较复杂的场景。业务对象和遍历状态分开,Person 不需要关心当前遍历到第几个字段。
写法二:对象自己也是迭代器
也可以让 Person 同时实现 __iter__ 和 __next__。
1class Person: 2 def __init__(self, name, age, gender, address): 3 self.name = name 4 self.age = age 5 self.gender = gender 6 self.address = address 7 self.attrs = [name, age, gender, address] 8 9 def __iter__(self): 10 self.index = 0 11 return self 12 13 def __next__(self): 14 if self.index >= len(self.attrs): 15 raise StopIteration 16 17 value = self.attrs[self.index] 18 self.index += 1 19 return value 20
这种写法代码更少,但要注意:遍历状态放在对象自己身上。多个地方同时遍历同一个对象时,更容易相互影响。
学习阶段可以先写这种,真实业务里更推荐“对象和迭代器分开”,职责更清楚。
六、为什么需要迭代器
迭代器最大的价值是惰性计算:不一次性生成所有结果,而是在需要时才计算下一个。
比如生成斐波那契数列,如果一次性生成 100000 个数字并放进列表,内存会越来越大。
1def fib_list(total): 2 result = [] 3 a = 0 4 b = 1 5 6 for _ in range(total): 7 result.append(a) 8 a, b = b, a + b 9 10 return result 11
如果改成迭代器,每次只返回当前这个数:
1class Fibo: 2 def __init__(self, total): 3 self.total = total 4 self.index = 0 5 self.a = 0 6 self.b = 1 7 8 def __iter__(self): 9 return self 10 11 def __next__(self): 12 if self.index >= self.total: 13 raise StopIteration 14 15 value = self.a 16 self.a, self.b = self.b, self.a + self.b 17 self.index += 1 18 return value 19
使用:
1for number in Fibo(10): 2 print(number) 3
迭代器适合这些场景:
- 数据量很大,不想一次性放进内存
- 不确定用户最终会消费多少结果
- 数据来自文件、网络、数据库游标这类流式来源
- 每个结果只依赖当前状态和上一个状态
七、生成器 generator
生成器可以理解成:Python 帮你自动实现迭代器协议的语法糖。
只要一个函数体里出现 yield,这个函数就不是普通函数,而是生成器函数。
1def demo(): 2 print('demo 函数开始执行了') 3 print(100) 4 yield '我是第 1 个 yield 返回的数据' 5 6 a = 200 7 print(a) 8 yield '我是第 2 个 yield 返回的数据' 9 10 b = 300 11 print(b) 12 return '执行结束' 13
调用生成器函数时,函数体不会立刻执行,而是返回一个生成器对象。
1d = demo() 2 3print(hasattr(d, '__iter__')) # True 4print(hasattr(d, '__next__')) # True 5
生成器对象本质上是一种迭代器,所以可以用 next() 取值:
1d = demo() 2 3print(next(d)) 4print(next(d)) 5 6try: 7 print(next(d)) 8except StopIteration as e: 9 print(e.value) # 执行结束 10
执行过程可以这样理解:
1第一次 next() 2 -> 函数从开头执行 3 -> 遇到第一个 yield 暂停 4 -> yield 后面的值作为本次 next() 的返回值 5 6第二次 next() 7 -> 从上次暂停的位置继续执行 8 -> 遇到第二个 yield 再暂停 9 10第三次 next() 11 -> 继续执行 12 -> 遇到 return 13 -> 抛 StopIteration 14 -> return 后面的值会放到异常对象的 value 里 15
生成器和普通函数最大的差异是:普通函数一次调用跑到底,生成器函数可以在 yield 处暂停,下次再接着跑。
前端里可以对照 function*:
1function* demo() { 2 console.log("demo 开始执行"); 3 yield "第 1 个值"; 4 yield "第 2 个值"; 5} 6 7const d = demo(); 8 9console.log(d.next()); 10console.log(d.next()); 11console.log(d.next()); 12
八、yield 的几个常见写法
yield 写在循环里
最常见的生成器写法,是在循环里不断 yield。
1def fib(total): 2 a = 0 3 b = 1 4 5 for _ in range(total): 6 yield a 7 a, b = b, a + b 8
使用:
1for number in fib(10): 2 print(number) 3
这比手写 class Fibo 简洁很多,但效果类似:每次需要下一个值时,才继续往后计算。
yield from
yield from 可以把另一个可迭代对象里的值依次产出。
1def demo(): 2 nums = [10, 20, 30, 40] 3 yield from nums 4
它大致等价于:
1def demo(): 2 nums = [10, 20, 30, 40] 3 4 for num in nums: 5 yield num 6
所以 yield from 可以记成:
1把某个可迭代对象里的数据,一个一个 yield 出去 2
send()
生成器除了能往外吐值,也能在继续执行时接收外部传进来的值。
1def demo(): 2 print('demo 函数开始执行了') 3 4 a = yield '第 1 个 yield 的返回值' 5 print(f'a 接收到:{a}') 6 7 b = yield '第 2 个 yield 的返回值' 8 print(f'b 接收到:{b}') 9
使用:
1d = demo() 2 3print(next(d)) # 先启动生成器,停在第一个 yield 4print(d.send('张三')) # 把 '张三' 传给变量 a,然后继续执行 5 6try: 7 d.send('李四') # 把 '李四' 传给变量 b,然后继续执行到函数结束 8except StopIteration: 9 print('生成器执行结束') 10
注意:第一次启动生成器时不能直接传普通值,因为代码还没有运行到任何一个 yield 位置,没有地方接收这个值。
1d = demo() 2 3# d.send('张三') # TypeError 4d.send(None) # 等价于 next(d) 5
next() 只能取值;send(value) 既能让生成器继续执行,也能把值传回上一次暂停的 yield 表达式。
九、生成器表达式
生成器表达式是一种快速创建生成器对象的写法,长得很像列表推导式。
1nums = [10, 20, 30, 40] 2 3result1 = [n * 2 for n in nums] 4result2 = (n * 2 for n in nums) 5 6print(result1) # [20, 40, 60, 80] 7print(result2) # <generator object ...> 8
区别在于:
| 写法 | 结果 | 是否立刻生成全部结果 |
|---|---|---|
| [n * 2 for n in nums] | 列表 | 是 |
| (n * 2 for n in nums) | 生成器对象 | 否 |
生成器表达式适合“每个结果只依赖当前元素”的场景。
1nums = [10, 20, 30, 40] 2 3result = (n * 2 for n in nums) 4 5for item in result: 6 print(item) 7
它不会一次性创建 [20, 40, 60, 80],而是每次循环时才计算当前这个 item。
如果数据量很小,并且后面要反复使用结果,列表推导式更直观。如果数据量很大,只需要顺序消费一遍,生成器表达式更省内存。
十、最后怎么选
可以按这个顺序判断:
1只是遍历已有 list / tuple / dict / str 2 -> 直接 for 3 4想让自己的类能被 for 5 -> 实现 __iter__ 6 -> 如果要自己控制取值过程,再实现 __next__ 7 8要一个一个惰性产出结果 9 -> 优先写生成器函数 yield 10 11只是把一个可迭代对象映射成另一个惰性结果 12 -> 用生成器表达式 13 14需要复杂状态、多个方法、可维护的对象封装 15 -> 手写迭代器类 16
最容易混淆的点:
| 问题 | 结论 |
|---|---|
| 能 for 的一定是迭代器吗 | 不一定,可能只是可迭代对象 |
| 迭代器能 for 吗 | 能,因为迭代器的 __iter__ 返回自己 |
| iter(obj) 做了什么 | 调用 obj.__iter__(),拿到迭代器 |
| next(it) 做了什么 | 调用 it.__next__(),拿下一个值 |
| 取完后怎么结束 | Python 抛 StopIteration |
| 生成器是什么 | 用 yield 自动创建出来的迭代器 |
| 生成器会立刻执行函数体吗 | 不会,第一次 next() 才开始执行 |
| 迭代器能重复遍历吗 | 通常不能,它会被消耗 |
《Python 迭代器与生成器》 是转载文章,点击查看原文。