本文主要参考自 B 站码农高天的视频:
- 【python】对迭代器一知半解?看完这个视频就会了。涉及的每个概念,都给你讲清楚!
- 【python】生成器是什么?怎么用?能干啥?一期视频解决你所有疑问!
本文将讨论 python 中的可迭代对象 iterable、迭代器 iterator 和生成器 generator。大部分的 python 程序员都会听过这三个概念,但是却可能对其缺乏深入的理解。
lst = [1, 2, 3]
for item in lst:
# do something
psss
for loop 是 python 中最常见不过的操作了。即使是学过半天 python 的初学者,也能熟练掌握 for loop 的用法。这得益于 python 中 for loop 的语义真的很容易理解。for item in lst
从 lst 一个一个地拿出 item,进行处理。对于列表,其中的元素就是一个个地有序排列这,这非常自然。但是对于无序的字典呢?或者对于更复杂的文件对象呢?在 python 中,这些都可以通过 for loop 来遍历,这是怎么做到的呢?for loop 背后在做什么事情呢?
实际上,for loop 背后做的事情(在一定抽象程度上)也并不复杂,但是理解它,对于我们深入理解 python 中的可迭代对象、迭代器和生成器很有帮助。
for loop 背后的动作其实也并不复杂,可以看做两步:
首先对 lst 取 iter,得到迭代器 iterator
iterator = iter(lst)
然后对 iterator 不断地取 next,拿出其中元素
item0 = next(iterator)
item1 = next(iterator)
# ...
直至遇到 StopIteration 这个 exception
这里的 iter
和 next
是 python 内置的两个函数。分别作用于可迭代对象 iterable 和迭代器 iterator,功能分别是从可迭代对象得到迭代器,和从迭代器中一个一个地取元素。也就是说,我们这个例子中的 lst
需要是一个可迭代对象,iterator
是它生成的迭代器。
那么,回到最初我们的问题,对于字典、文件对象这种复杂的数据结构,for loop 是如何进行遍历的呢?python 是如何知道怎么取数据结构中的下一个元素的呢?这就涉及到可迭代对象和迭代器分别必须实现的两个魔法方法 __iter__
/ __getitem__
和 __next__
。
我们刚才已经提到,for xxx in yyy
这里的 yyy
必须是一个可迭代对象 iterable。我们可以通过将可迭代对象传入 iter
方法,来得到一个迭代器。那么 iter
方法是如何从一个可迭代对象得到一个迭代器的呢?答案就是根据这个可迭代对象实现的魔法方法:__iter__
或 __getitem__
。
比如 __iter__
,该方法需要返回一个迭代器。
在通过 iter
方法在拿到迭代器之后,我们可以将迭代器传入 next
方法,从而不断地从迭代器中取出元素。如何取出元素?靠的是该迭代器对象实现的 __next__
方法。
需要注意的是,python 官方文档 建议我们实现的迭代器也要是一个可迭代对象,即也要实现 __iter__
方法。这是为了保证如果我们显式地对一个可迭代对象取了 iter
,得到一个迭代器之后,这个迭代器还要能够通过 for loop / 再取 iter
等方式来遍历。比如这种情形:
lst = [1, 2, 3]
ite = iter(lst)
next(ite)
for item in ite:
# do sth
pass
如果迭代器本身不是可迭代对象的话,放入 for loop 就会报错,因为它没有实现 __iter__
方法。当然,为了保证一个迭代器同时是可迭代对象,我们要实现的 __iter__
方法通常非常简单,多数情况下,只需要返回本身即可。即:
def __iter__(self):
return self
至此,我们就理解了可迭代对象 iterable 和迭代器 iterator 各自需要实现哪些魔法方法,以及他们的区别和联系,
下面我们以链表为例,来实现其迭代器和可迭代对象:
class NodeIter:
def __init__(self, node):
self.curr_node = node
def __next__(self):
if self.curr_node is None:
raise StopIteration
node, self.curr_node = self.curr_node, self.curr_node.next
return node
def __iter__(self):
return self
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __iter__(self):
return NodeIter(self)
这里,Node
是一个可迭代对象,可以放到 for loop 中去遍历,也可以直接 iter
取其迭代器。其对应的迭代器就是 NodeIter
,根据其实现的 __next__
方法来去元素,直到没有元素,raise 一个 StopIteration。注意为了保证迭代器 NodeIter
也是可迭代对象,我们同样为它实现了 __iter__
方法,直接返回其本身。
生成器可能是很多 python 初学者比较陌生的一种语法。实际上,生成器就是一种特殊的迭代器。
from typing import Iterator
def gen(num):
while num > 0:
yield num
num -= 1
return
g = gen(5)
print(isinstance(g, Iterator))
first = next(g)
print(first)
print('in for loop: ')
for i in g:
print(i)
# 输出:
# True
# 5
# in for loop:
# 4
# 3
# 2
# 1
比如在上面就是对一个生成器进行遍历的例子,其中 gen
称为生成器函数, g
称为生成器对象。它可以用我们之前在迭代器小节中介绍的 next、for loop 等方式来使用,因为生成器也是一种迭代器。
以下主要介绍生成器与一般迭代器不同的地方。
容易发现,所谓生成器函数中,都有一个 yield 关键字,注意,这里我们特意也写了一个 return 关键字。如果是在一般的函数中,很明显 gen
函数会返回一个 None。但是,python 解释器在看到有 yield 关键字存在的函数时,会将这个函数标记为一个生成器函数。生成器函数被调用时不会运行其函数本体,也不会返回值,而是返回一个生成器对象(本例中的 g
)。
而当生成器对象被传入 next 方法时,才会真正运行其对应的生成器函数。在生成器函数运行时(即生成器对象被 next 方法调用时),函数会在运行到 yield 语句时将 yield 后面的值返回出来。但是可以看到,在 yield 语句之后,函数还有一些语句没有执行,本次调用已经返回,不会再执行了。此时生成器函数相当于被按了一个暂停键,在下一次 next 被调用时,生成器函数会从本次 yiled 语句之后继续运行。因此,我们本例中的 num 会在每次迭代时减一。
在 num 不断减少之后,函数会跳出 while 循环,执行 return。在生成器函数中,return 语句相当于迭代器中 raise 了一个 StopIteration。注意,无论生成器函数中的 return 是 return 了一个 None 还是 return 了一个值,这个值都不会在生成器对象被 next 调用时返回出来,next 只会返回 yield 语句的值。如果真的有需求去获取生成器函数中 return 的值,需要去 catch StopIteration 这个 exception,然后拿到返回值。
从使用者的角度来看,生成器这种特殊的迭代器与普通迭代器的使用方式几乎没有任何不同。从实现原理的角度来看,普通迭代器通过类成员变量来保存当前的迭代状态,而在生成器中,迭代状态保存在函数的栈帧中,通过函数的运行状态来保存。生成器的实现通常会比普通迭代器更加简洁。可对比下面的生成器示例和上面的迭代器示例。
我们说:生成器与普通迭代器的使用方式几乎没有任何不同。那么不同之处在哪呢?这里我们介绍生成器的一个高级用法:send。send 方法可以在调用生成器函数进行 yield 取值的同时,会将 send 函数的参数作为 yiled 语句( yield xxx
)的值传入。在生成器函数中,可以接收 yield 语句的值,来进行处理。这就使得我们能够在迭代生成器的时候,可以通过 send 方法传入一些值来改变生成器内部的状态,实现与生成器的交互。
def gen(num):
while num > 0:
tmp = yield num
if tmp is not None:
num = tmp
num -= 1
g = gen(5)
first = next(g) # first = g.send(None)
print(f"first: {first}")
print(f"send: {g.send(10)}")
for i in g:
print(i)
# 输出:
first: 5
send: 9
8
7
6
5
4
3
2
1
直接调用 next 方法相当于 g.send(None)。而如果生成器函数内部没有使用一个变量接受 yield 语句的返回值,并进行处理的逻辑,那么 send 什么值进去都相当于被直接丢掉了,此时 g.send(xxx) 无论 xxx 是什么东西,都相当于直接调用 next 方法。
同样以链表的实现为例。之前我们通过一个 NodeIter
类来实现 Node
的遍历,Node
这个 Iterable 在被 iter
方法调用时,会返回 NodeIter
这个 Iterator。
这里,我们将 Node
的 __iter__
方法直接实现为一个生成器函数,当被 iter 方法调用时,会返回一个生成器对象,我们知道生成器对象就是一个特殊的 iterator,当然可以正常遍历。这样,我们就通过生成器更简洁地实现了链表 Node 的遍历。并且,这对使用者来说完全是透明的,调用方式、遍历方式与之前 NodeIter 的实现完全一致。
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __iter__(self):
node = self
while node is not None:
yield node
node = node.next
node1 = Node('node1')
node2 = Node('node2')
node3 = Node('node3')
node1.next = node2
node2.next = node3
for node in node1:
print(node.name)