Python中的Iterable、Iterator、Generator详解

Python中的Iterable、Iterator、Generator详解

本文主要参考自 B 站码农高天的视频:

本文将讨论 python 中的可迭代对象 iterable、迭代器 iterator 和生成器 generator。大部分的 python 程序员都会听过这三个概念,但是却可能对其缺乏深入的理解。

for loop

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 背后的动作

for loop 背后的动作其实也并不复杂,可以看做两步:

  1. 首先对 lst 取 iter,得到迭代器 iterator

    iterator = iter(lst)
    
  2. 然后对 iterator 不断地取 next,拿出其中元素

    item0 = next(iterator)
    item1 = next(iterator)
    # ...
    

    直至遇到 StopIteration 这个 exception

这里的 iternext 是 python 内置的两个函数。分别作用于可迭代对象 iterable 和迭代器 iterator,功能分别是从可迭代对象得到迭代器,和从迭代器中一个一个地取元素。也就是说,我们这个例子中的 lst 需要是一个可迭代对象,iterator 是它生成的迭代器。

那么,回到最初我们的问题,对于字典、文件对象这种复杂的数据结构,for loop 是如何进行遍历的呢?python 是如何知道怎么取数据结构中的下一个元素的呢?这就涉及到可迭代对象和迭代器分别必须实现的两个魔法方法 __iter__ / __getitem____next__

Iterable 可迭代对象

我们刚才已经提到,for xxx in yyy 这里的 yyy 必须是一个可迭代对象 iterable。我们可以通过将可迭代对象传入 iter 方法,来得到一个迭代器。那么 iter 方法是如何从一个可迭代对象得到一个迭代器的呢?答案就是根据这个可迭代对象实现的魔法方法:__iter____getitem__

比如 __iter__ ,该方法需要返回一个迭代器。

Iterator 迭代器

在通过 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__ 方法,直接返回其本身。

Generator 生成器

生成器可能是很多 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)

Ref

猜你喜欢

转载自blog.csdn.net/weixin_44966641/article/details/131501576
今日推荐