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