迭代器和生成器

可迭代对象(iterable): 一般像list, tuple, dictionary这种,内部需要实现__iter__方法,该方法用于创建一个迭代器。
迭代器(iterator): 由可迭代对象创建,在for循环刚开始时自动创建,也可以通过iter(iterable)内置函数来创建。 其内部需要实现__next__方法。

可以把迭代器当成是对可迭代对象的流式处理,循环时,就像代表着可迭代对象中的每一个元素一样。
实际内部运行机制: 循环开始时,内部会调用可迭代对象的__iter__方法来创建一个迭代器,随后,迭代器通过每次执行一下其内部的__next__方法,来返回一个值,这个__next__方法可由我们自己来实现,当然我们的目的就是让其返回可迭代对象中的值。 同时我们要在方法内部实现停止循环的条件,即抛出StopIteration异常,这就标志着已经迭代完了所有元素。

先看下python对可迭代对象和迭代器的判断标准:
python内部将实现了__iter__方法的对象叫做可迭代对象。 将同时实现了__iter____next__两个方法的对象称为迭代器。

from collections.abc import Iterable, Iterator

class Foo:
    def __iter__(self):
        pass

print(issubclass(Foo, Iterable))
print(issubclass(Foo, Iterator))

print('-' * 25, '分隔线', '-' * 25)

class Bar:
    def __iter__(self):
        pass

    def __next__(self):
        pass

print(issubclass(Bar, Iterable))
print(issubclass(Bar, Iterator))

输出结果:

True
False
------------------------- 分隔线 -------------------------
True
True

其实我们的迭代器如果不实现__iter__方法,也能进行迭代。 但是它就不能像可迭代对象一样放在for ... in 的后边。

下面使用链表结构模拟可迭代对象和迭代器。 这里的迭代器我只实现了__next__方法,没有实现__iter__方法。

class Node:
    def __init__(self, value):
        self.value = value
        self.next_node = None

    def __iter__(self):
        return NodeIterator(self)

class NodeIterator:
    def __init__(self, node: Node):
        self.current_node = node

    def __next__(self):
        node, self.current_node = self.current_node, self.current_node.next_node
        return node


node1 = Node('first')
node2 = Node('second')
node3 = Node('third')

node1.next_node = node2
node2.next_node = node3

# 不使用for loop循环
it = iter(node1)  # 使用iter函数作用在可迭代对象上,创建一个迭代器
print(it.current_node.value)

print('-' * 25, '分隔线', '-' * 25)

first_node = next(it)
print(first_node.value)
second_node = next(it)
print(second_node.value)
third_node = next(it)
print(third_node.value)

# forth_node = next(it)  # 继续使用next操作将抛出StopIteration异常

输出结果:

first
------------------------- 分隔线 -------------------------
first
second
third

可以看到,我们使用iter函数得到了一个迭代器,然后在使用next函数作用于这个迭代器,每次都返回一个值。
下面我们使用for循环:

# 下面使用循环操作
print('-' * 25, '分隔线', '-' * 25)
for item in node1:
    print(item.value)

输出结果:

------------------------- 分隔线 -------------------------
first
second
third
Traceback (most recent call last):
  File "F:\RolandWork\PythonProjects\studyPython\test.py", line 42, in <module>
    for item in node1:
  File "F:\RolandWork\PythonProjects\studyPython\test.py", line 14, in __next__
    node, self.current_node = self.current_node, self.current_node.next_node
                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'next_node'

可以看到,for循环打印出了这三个节点的值,但最终报错了。原因是迭代器已经指向最后一个节点之后,我们的next函数再执行时,它已经是None了,就没有next_node属性了。 因此这时候,我们应该手动抛出StopIteration异常,这样for循环就能正常停止了。 我们只需要像下面这样修改一个迭代器的__next__方法。

class NodeIterator:
    def __init__(self, node: Node):
        self.current_node = node

    def __next__(self):
        if self.current_node is None:
            raise StopIteration
        node, self.current_node = self.current_node, self.current_node.next_node
        return node

再次运行,for循环就不报错,而是正常结束循环了。

看起来我们的迭代器工作正常。 但是为什么官方会要求迭代器自身也要实现__iter__方法呢?(即让迭代器也是一个可迭代对象)
我们来看下面这种情况:

it = iter(node1)
node = next(it) # 这行代码运行后,迭代器已经指向了第二个节点
print(node.value)

print('-' * 25, '分隔线', '-' * 25)
# 这时我们想接下来使用for循环从当前指向的节点继续迭代下去
for item in it:
    print(item.value)

输出结果:

first
------------------------- 分隔线 -------------------------
Traceback (most recent call last):
  File "F:\RolandWork\PythonProjects\studyPython\test.py", line 34, in <module>
    for item in it:
TypeError: 'NodeIterator' object is not iterable

可以看到,运行报错了。 看样子我们不能直接把迭代器放在for循环in的后边。 这是因为,in后面要放一个可迭代对象,因为像我们一开始说的,for循环内部开始时,会先对我们in后面的对象执行iter()方法,来返回一个迭代器。而我们的迭代器目前没有实现__iter__方法,所以它不算是可迭代对象。
如果我们想这么用,就在迭代器里实现__iter__方法,而实现也很简单,就直接返回它自己就可以了。这样,迭代器自己返回了自己,当前指向哪个节点它当然也记得住。
在迭代器NodeIterator类中添加如下方法就可以了。

def __iter__(self):  # 迭代器也实现__iter__方法,以便可以用在for in 后边
   return self 

修改后再次运行,结果如下:

first
------------------------- 分隔线 -------------------------
second
third

现在还有一个问题,就是当我们运行完一遍for循环后,再对这个迭代器运行一次for循环,我们发现就没有再打印任何节点了。 这与我们认为的for循环每次都会从头开始打印一遍的想法不一样。

it = iter(node1)
node = next(it) # 这行代码运行后,迭代器已经指向了第二个节点
print(node.value)

print('-' * 25, '分隔线', '-' * 25)
# 这时我们想接下来使用for循环从当前指向的节点继续迭代下去
for item in it:
    print(item.value)

print('-' * 25, '分隔线', '-' * 25)
for item in it:
    print(item.value)

输出结果:

first
------------------------- 分隔线 -------------------------
second
third
------------------------- 分隔线 -------------------------

而如果我们把第二次for循环的in后面的it换成node1,或者iter(node1),就可以正常打印第二次循环的内容。

it = iter(node1)
node = next(it) # 这行代码运行后,迭代器已经指向了第二个节点
print(node.value)

print('-' * 25, '分隔线', '-' * 25)
# 这时我们想接下来使用for循环从当前指向的节点继续迭代下去
for item in it:
    print(item.value)

print('-' * 25, '分隔线', '-' * 25)
for item in node1:  # 或者换成iter(node1)
    print(item.value)

输出结果:

first
------------------------- 分隔线 -------------------------
second
third
------------------------- 分隔线 -------------------------
first
second
third

这是因为,换成node1或iter(node1)时,迭代器对象被使用iter()方法在循环一开始重新获取到了。 而之前我们的迭代器对象(it)已经指向最后一个节点了,再对它循环,就相当于再对它执行next()函数,内部就会立刻遇到StopIteration异常,从而结束循环。 为了让迭代器对象可以重复使用,我们可以在每次其指向最后一个节点,并在遇到StopIteration异常之前,重新让它指向第一个节点,为再次使用for循环做准备。
于是可以像下面这样修改代码: 在抛出异常前,加一句 self.current_node = node1

class NodeIterator:
    def __init__(self, node: Node):
        self.current_node = node

    def __next__(self):
        if self.current_node is None:
            self.current_node = node1  # 每次迭代到最后一个节点时,再将current_node属性指回第一个节点,这样下次循环迭代器,就可以又重头开始。
            raise StopIteration
        node, self.current_node = self.current_node, self.current_node.next_node
        return node

    def __iter__(self):  # 迭代器也实现__iter__方法,以便可以用在for in 后边
        return self

生成器(generator): 生成器是特殊的迭代器,在英语文档中也称generator iterator。 为了在语义上进行区分,我们可以完整的称其为生成器对象,而生成器对象是由生成器函数创建的。
生成器函数(generator function): 函数体中存在yield关键字的函数就是生成器函数。 生成器函数在执行时,返回一个生成器对象,而不是正常运行函数体的代码。
生成器表达式: 语法很像列表推导式或字典推导式,只不过外层是小括号。注意这不是元组推导式。python其实不有元组推导式,如果想像使用列表推导式那样创建一个元组,我们可以tupe(for i in range(10))这样,使用tuple函数作用于一个生成器表达式。
print((i for i in range(5))) 这行代码输出结果是: <generator object <genexpr> at 0x000001DD45028040>, 由此可见,列表推导式返回的是一个生成器,而不是元组。
如果数据量非常大时,生成器不会占用太大的内存空间,而是在迭代过程中动态的去计算并返回他的下一个值。 而列表则需要把数据都存储在内存中,占用很大空间。举个简单例子:
列表的例子:

nums = [i for i in range(50000000000000)]  # python需要花很长时间来生成这个列表
for i in nums:
    print(i)

生成器例子:

gen = (i for i in range(50000000000000))  # python直接会创建好生成器,并很快开始打印。
for i in gen:
    print(i)

我们知道迭代器的机制就是在其内部记录它迭代的当前节点以及下一个要迭代的节点。 而生成器记住迭代状态的机制就是把它们都放在生成器函数中,开始迭代以后,其运行到了函数中的哪个位置就会一直被缓存起来,包括函数中各个变量的当前值。遇到yield就会将yield后面的值返回给生成器调用方(next()函数或for循环),如果运行到再函数的结束(包括运行到return语句,或函数运行结束),就会抛出StopIteration异常。如果遇到return语句,异常的value属性就是return后面的值。如果return后面为None,或者函数没有Return语句,异常的value属性也是None
下面写一个简单的生成器示例:

# 函数中有yield,所以它是一个生成器函数
def gen(x):
    print('x is',x)
    yield 1
    print('第二次')
    yield 2
    print('第三次')
    yield 3
    print('第四次')
    print('第五次...')

g = gen('abc')  # 执行生成器函数会返回一个生成器对象
print(type(g), g)
print('-' * 25, '分隔线', '-' * 25)
# 生成器是特殊的迭代器,所以也可以用for循环。
for item in g:
    print(item)
print('-' * 25, '分隔线', '-' * 25)

输出结果:

<class 'generator'> <generator object gen at 0x0000011774DE8040>
------------------------- 分隔线 -------------------------
x is abc
1
第二次
2
第三次
3
第四次
第五次...
------------------------- 分隔线 -------------------------

我们再使用next()函数来遍历生成器:

new_obj = gen('def')
print(next(new_obj))
print(next(new_obj))
print(next(new_obj))
try:
    print(next(new_obj))
except StopIteration as e:
    print('e.value is', e.value)
print('-' * 25, '分隔线', '-' * 25)

输出结果:

x is def
1
第二次
2
第三次
3
第四次
第五次...
e.value is None
------------------------- 分隔线 -------------------------

可以看到,跟迭代器一样,使用next()函数就会迭代一次,即执行到yiled语句,并返回yield后面的值。当执行到再也遇不到yield语句后,就抛出StopIteration异常。

那么如果我们定义一个生成器函数,但让其代码永远走不到yiled语句的地方,那么它还会返回一个生成器么? 答案是会。

def gen2():
    if 1 > 2:
        yield '1 > 2'
    elif 4 > 5:
        yield '4 > 5'
    else:
        return '1 < 2'

obj2 = gen2()
print(type(gen2), obj2)  # 虽然函数执行时走不到yield语句,但仍然返回一个生成器。


try:
    print(next(obj2))
except Exception as e:
    print('e.value is', e.value)

print('-' * 25, '分隔线', '-' * 25)
# 由于生成器函数走不到yield语句,所以for循环不会迭代到任何内容
for item in gen2():
    print(item)
print('-' * 25, '分隔线', '-' * 25)

输出结果:

<class 'function'> <generator object gen2 at 0x000001E5296B62C0>
e.value is 1 < 2
------------------------- 分隔线 -------------------------
------------------------- 分隔线 -------------------------

从输出结果上可以看到,生成器函数虽然走不到yield语句,但执行后还是返回了一个生成器。 但对这个生成器执行next()函数或for循环,不会迭代到任何内容。

generator.send(value): 生成器的send函数首先会起到与next()函数相同的作用,就是进行一次迭代。并且,会把参数值传给yied表达式等号左边的变量,例如val = yield num。send方法通常不该用于对生成器的第一次迭代。一般最好用next()方法。如果非要用send,那么参数应该为None,否则会报错。即generator.send(None)。 当一次迭代执行到yield并将控制返回给调用方时,下一次使用send()方法时,会首先对yield表达式要赋值的变量的值修改成send方法的参数值,然后再继续执行下面的代码。直到下一次遇到yield语句,或者函数代码执行完或遇到return语句。

def gen(max_num):
    print('生成器开始运行')
    num = 1
    temp = None
    while True:
        print('temp is', temp, ', num is', num, ', max_num is', max_num)
        if num < max_num:
            temp = yield num
            if temp is not None:
                max_num = temp
            num += 1
        else:
            break
    print('生成器结束了')
    return '循环结束了'

g = gen(3)
print('-' * 25, '迭代1分隔线', '-' * 25)
print('迭代1打印: ', g.send(None))
print('-' * 25, '迭代2分隔线', '-' * 25)
print('迭代2打印: ', g.send(5))  # 可以通过打印的内容观察到send函数的作用。
print('-' * 25, '迭代3分隔线', '-' * 25)
print('迭代3打印: ', next(g))
print('-' * 25, '迭代4分隔线', '-' * 25)
print('迭代4打印: ', next(g))
print('-' * 25, '迭代5分隔线', '-' * 25)
print('迭代5打印: ', next(g))
print('-' * 25, '迭代6分隔线', '-' * 25)
print('迭代6打印: ', next(g))

输出结果:

------------------------ 迭代1分隔线 -------------------------
生成器开始运行
temp is None , num is 1 , max_num is 3
迭代1打印:  1
------------------------- 迭代2分隔线 -------------------------
temp is 5 , num is 2 , max_num is 5
迭代2打印:  2
------------------------- 迭代3分隔线 -------------------------
temp is None , num is 3 , max_num is 5
迭代3打印:  3
------------------------- 迭代4分隔线 -------------------------
temp is None , num is 4 , max_num is 5
迭代4打印:  4
------------------------- 迭代5分隔线 -------------------------
temp is None , num is 5 , max_num is 5
生成器结束了
Traceback (most recent call last):
  File "F:\RolandWork\PythonProjects\studyPython\iterator.py", line 27, in <module>
    print('迭代5打印: ', next(g))
                     ^^^^^^^
StopIteration: 循环结束了

yield from 表达式: yield from 后面接一个生成器,用于实现生成器的嵌套。并可以将子生成器函数的返回值(return语句的返回值,若没有则是None)获取到,并赋给一个变量。比如:result = yield from gen()
一个简单的yield from的应用例子如下:

def subgen():
    print('subgen 1')
    yield 1
    print('subgen 2')
    yield 2
    print('subgen 3')
    return 8

def delegator():
    print('delegator开始')
    result = yield from subgen()
    print('subgen结束')
    yield result
    print('delegator结束')

gen = delegator()
print(next(gen))  # subgen里第1次yield返回 (属于yield from语句)
print(next(gen))  # subgen里第2次yield返回 (属于yield from语句)
print(next(gen))  # yield result (result是subgen()函数的return的值。如果没有return语句,则返回None)
print(next(gen))  # 这次迭代会遇到StopIteration,因为再没有yield语句。

输出结果:

delegator开始
subgen 1
1
subgen 2
2
subgen 3
subgen结束
8
delegator结束
Traceback (most recent call last):
  File "F:\RolandWork\PythonProjects\studyPython\forTest.py", line 20, in <module>
    print(next(gen))  # 这次迭代会遇到StopIteration,因为再没有yield语句。
          ^^^^^^^^^
StopIteration

我们其实可以用代码的方式来验证生成器也是迭代器,即判断它是否实现了__iter__和__next__方法。

def gen_fun():
    yield 1
    yield 2
    yield 3

gen = gen_fun()

print(hasattr(gen, '__iter__'))  # True
print(hasattr(gen, '__next__'))  # True

可见,生成器都实现了__iter__和__next__方法,所以它当然可以使用iter()函数,也可以使用next()函数,同时也可以用在for循环中。

posted @   RolandHe  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示