Python深入-13-迭代器、生成器

 迭代是数据处理的基石:程序将计算应用于数据序列,从像素到核苷酸。如果数据在内存中放不下,则需 要惰性获取数据项,即按需一次获取一项。这就是迭代器的作用。  
 在 Python 中,所有容器都是可迭代对象。Python 使用可迭代对象提供的迭代器支持以下操作: 
  • for 循环;

  • 列表、字典和集合推导式;

  • 拆包赋值;

  • 构造容器实例。  

一、单词序列

我们将实现一个 Sentence 类,以此开启探索可迭代对象的旅程。向这个类的构造函数传入包含一些文本 的字符串,然后便可以逐个单词迭代。

import re
import reprlib

RE_WORD = re.compile(r'w+')


class Sentence:
    def __init__(self, text):
        self.text = text
        # .findall 函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配
        self.words = RE_WORD.findall(text)

    def __getitem__(self, index):
        # self.words 存储 .findall 返回的结果,因此直接返回指定索引位上的单词
        return self.words[index]

    # 为了完善序列协议,我们实现了 __len__ 方法。不过,为了让对象可以迭代,没必要实现这个方法
    def __len__(self):
        return len(self.words)

    def __repr__(self):
        # reprlib.repr 这个实用函数用于生成大型数据结构的简略字符串表示形式
        return 'Sentence(%s)' % reprlib.repr(self.text)


if __name__ == "__main__":
    # 传入一个字符串,创建一个 Sentence 实例
    s = Sentence('"The time has come," the Walrus said,')
    # 注意,__repr__ 方法的输出中包含 reprlib.repr 函数生成的
    print(s)  # Sentence('"The time ha... Walrus said,')

    # Sentence 实例可以迭代,稍后说明原因。
    for word in s:
        print(word)

    # 因为可以迭代,所以 Sentence 对象可用于构建列表和其他可迭代类型
    print(list(s))  # ['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']

二、 序列可以迭代的原因:iter 函数

需要迭代对象 x 时,Python 自动调用 iter(x)。  
内置函数 iter 执行以下操作:

  • 1. 检查对象是否实现了 iter方法,如果实现了就调用它,获取一个迭代器。

  • 2. 如果没有实现 iter__` 方法,但是实现了 `__getitem 方法,那么 iter() 创建一个迭代器,尝试按索引(从 0 开始)获取项。

  • 3. 如果尝试失败,则 Python 抛出 TypeError 异常,通常会提示“’C’ object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。

    所有 Python 序列都可迭代的原因是,按照定义,序列都实现了 __getitem__ 方法。其实,标准的序列也都实现了 __iter__ 方法,因此你也应该这么做。之所以能通过 __getitem__ 方法迭代,是为了向后兼容。

    如果一个类提供了 __getitem__ 方法,那么内置函数iter() 接受的可迭代对象就可以是该类的实例, 据此构建一个迭代器。Python 的迭代机制以从 0 开始的索引调用 __getitem__ 方法,没有剩余项时抛出 IndexError

从 Python 3.10 开始,检查对象 x 能否迭代,最准确的方法是调用 iter(x) 函数,如果不可迭代,则处理 TypeError 异常。这比使用isinstance(x, abc. Iterable) 更准确,因为 iter(x) 函数还会考虑过时的 __getitem__方法,而抽象基类 Iterable 则不考虑

三、 可迭代对象与迭代器

使用内置函数 iter 可以获取迭代器的对象。如果对象实现了能返回迭代器的 __iter__ 方法,那么 对象就是可迭代的。序列都可以迭代。实现了__getitem__ 方法,而且接受从 0 开始的索引,这种对象也可以迭代。  
我们要明确可迭代对象与迭代器之间的关系:**Python 从可迭代对象中获取迭代器。  **
下面是一个简单的 for 循环,迭代一个字符串。这里,字符串 ‘ABC’ 是可迭代对象。表面上看不出来,但 是背后有一个迭代器。

>>> s = 'ABC'
>>> for char in s:
...     print(char)
...
A
B
C

假如没有 for 语句,我们就不得不使用 while 循环模拟,要像下面这样写。

s = 'ABC'
it = iter(s)  # 根据可迭代对象构建迭代器 it
while True:
    try:
        print(next(it))  # 不断在迭代器上调用 next 函数,获取下一项。
    except StopIteration:  # 没有剩余项时,迭代器抛出 StopIteration
        del it  # 释放对 it 的引用,即废弃迭代器对象
        break  # 出循环。

StopIteration 表明迭代器已耗尽。内置函数 iter() 在内部自行处理 for 循环和其他迭代上下文(例 如列表推导式、可迭代对象拆包等)中的 StopIteration 异常。

Python 标准的迭代器接口有以下两个方法

  • next 返回序列中的下一项,如果没有项了,则抛出 StopIteration

  • __iter__ 返回 self,以便在预期可迭代对象的地方使用迭代器,例如 for 循环中。

    这个接口由抽象基类 collections.abc.Iterator 确立。这个抽象基类定义了抽象方法 __next__, 还从Iterable 类继承了抽象方法__iter__。  

    Python深入-13-迭代器、生成器
    image.png

四、 为单词序列类实现 `iter` 方法

4.1 经典迭代器

import re
import reprlib

RE_WORD = re.compile(r'w+')


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'

    # 这里只多了一个 __iter__ 方法。这一版没有 __getitem__ 方法,为的是明确表明这
    # 个类之所以可以迭代,是因为实现了 __iter__ 方法
    def __iter__(self):
        return SentenceIterator(self.words)  # 根据可迭代协议,__iter__ 方法实例化并返回一个迭代器


class SentenceIterator:
    def __init__(self, words):
        self.words = words  # SentenceIterator 存储一个单词列表引用。

        self.index = 0  # self.index 确定下一个要获取的单词

    def __next__(self):
        try:
            word = self.words[self.index]  # 获取 self.index 索引位上的单词
        except IndexError:
            raise StopIteration()  # 如果 self.index 索引位上没有单词,则抛出 StopIteration 异常
        self.index += 1  # 递增 self.index 的值。
        return word  # 返回单词

    def __iter__(self):  # 实现 self.__iter__ 方法。
        return self

注意,对这个示例来说,其实没必要在 SentenceIterator 类中实现 __iter__ 方法,不过这么做是对的,因为迭代器应该实现 __next____iter__ 两个方法,而且这么做能让迭代器通过 issubclass(SentenceIterator, abc.Iterator) 测试。如果让 SentenceIterator 子类化 abc.Iterator,那么它会继承具体方法 abc.Iterator.__iter__

4.2  不要把可迭代对象变成迭代器

构建可迭代对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代对象有个__iter__方 法,每次都实例化一个新迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现 __iter__方法,返回迭代器本身。 因此,迭代器也是可迭代对象,但是可迭代对象不是迭代器

4.3 生成器函数

实现相同功能,但符合 Python 习惯的方式是,用生成器代替 SentenceIterator 类

import re
import reprlib

RE_WORD = re.compile(r'w+')


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for word in self.words:  # 迭代 self.words。
            yield word  # 产出当前的 word。
            # 无须 return 语句;这个函数可以直接“落空”,自动返回。不管有没有 return 语句,生成器函数都不
            # 抛出 StopIteration 异常,而是在全部值生成完毕后直接退出

# 不用再单独定义一个迭代器类

4.4 生成器的工作原理

只要 Python 函数的主体中有yield 关键字,该函数就是生成器函数。调用生成器函数,返回一个生成器 对象。也就是说,生成器函数是生成器工厂 。

普通函数与生成器函数在句法上唯一的区别是,后者的主体中有 yield 关键字

** 一个简单的函数说明生成器的行为  **

def gen_123():
    yield 1  # 生成器函数的主体中通常多次使用 yield,不过这不是必要条件。这里我重复 3 次使用 yield。
    yield 2
    yield 3


# 仔细看,gen_123 是函数对象
print(gen_123)  # <function gen_123 at 0x00000281BFBB04A0>
# 但是调用时,gen_123() 返回一个生成器对象。
print(gen_123())  # <generator object gen_123 at 0x000001CCF3ECB5E0>

# 生成器对象实现了 Iterator 接口,因此生成器对象也可以迭代。
for i in gen_123():
    print(i)

# 为了仔细检查,我们把生成器对象赋值给 g
g = gen_123()
# 因为 g 是迭代器,所以调用 next(g) 会获取 yield 产出的下一项
print(next(g))  # 1
print(next(g))  # 2
print(next(g))  # 3
# 生成器函数返回后,生成器对象抛出 StopIteration 异常
# print(next(g))  # 没有了,就报错了StopIteration

生成器函数创建一个生成器对象,包装生成器函数的主体。把生成器对象传给 next() 函数时,生成器函 数提前执行函数主体中的下一个 yield 语句,返回产出的值,并在函数主体的当前位置暂停。最终,函数 的主体返回时,Python 创建的外层生成器对象抛出StopIteration异常——这一点与 Iterator 协议一 致。

使用准确的词语描述从生成器中获取值的过程,有助于理解生成器。说生成器“返 回”值,会让人摸不着头脑。应该这样说:函数返回值;调用生成器函数返回生成器;生成器产出值。 生成器不以常规的方式“返回”值:生成器函数主体中的 return 语句触发生成器对象抛出 StopIteration 异常。如果生成器中有 return x 语句,则调用方能从 StopIteration 异常中获 取 x 的值,但是我们往往把这个操作交给 yield from 句法

** 使用 for 循环更清楚地说明了生成器函数主体的执行过程  **

def gen_AB():
    print("start")  # 在 for 循环中,第 1 次隐式调用 next() 函数(标号❹)打印 'start',然后停在第 1 个 yield 语句处,生成值 'A'。
    yield "A"
    print("countinue")
    yield "B"  # 在 for 循环中,第 2 次隐式调用 next() 函数打印 'continue',然后停在第 2 个 yield 语句处,生成值 'B'
    print("end.")  # 第 3 次调用 next() 函数打印 'end.',然后到达函数主体的末尾,导致生成器对象抛出StopIteration 异常。


# 迭代时,for 机制的作用与 g = iter(gen_AB()) 一样,用于获取生成器对象,并在每次迭代时调用# next(g)。
for c in gen_AB():  # 这里是标号4
    print("--->", c)  # 循环打印 --> 和 next(g) 返回的值。但是,生成器函数中的 print 函数输出结果之后才会看到这个输出。

'''
start
---> A
countinue
---> B
end.

'''

Sentence.__iter__ 方法的作用了:__iter__ 是一个生成器函数,调用时构建一个实现了Iterator接口的生成器对象,因此不再需要SentenceIterator类。

4.5  惰性生成器

 惰性实现是指尽可能延后生成值。这样做能节省内存,或许还可以避免浪费 CPU 循环。 
 目前实现的几版`Sentence`类都不具有惰性,因为 `__init__` 方法及早构建好了文本中的单词列表,然后 将其绑定到 `self.words` 属性上。这样就得处理整个文本,列表使用的内存量可能与文本本身一样多(或 许更多,这取决于文本中有多少非单词字符)。如果用户只需迭代前几个单词,那么大多数工作属于白费力气。   

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成器,按需产出 re.MatchObject 实例。如果匹配项较多,那么 re.finditer 函数能节省大量内存。  ** 使用一个调用生成器函数 re.finditer 的生成器函数实现 Sentence 类 **

import re
import reprlib

RE_WORD = re.compile(r'w+')


class Sentence:
    def __init__(self, text):
        self.text = text  # 不再需要 words 列表。

    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'

    def __iter__(self):
        # finditer 函数构建一个迭代器,包含 self.text 中匹配 RE_WORD 的单词,产出 MatchObject 实例。
        for match in RE_WORD.finditer(self.text):
            # match.group() 方法从 MatchObject 实例中提取匹配的文本
            yield match.group()

4.6  惰性生成器表达式

简单的生成器函数,可以替换成生成器表达式。列 表推导式构建列表,而生成器表达式构建生成器对象。

**先在列表推导式中使用生成器函数 gen_AB,然后在生成器表达式中使用  **

def gen_AB():
    print("start")  # 在 for 循环中,第 1 次隐式调用 next() 函数(标号❹)打印 'start',然后停在第 1 个 yield 语句处,生成值 'A'。
    yield "A"
    print("countinue")
    yield "B"  # 在 for 循环中,第 2 次隐式调用 next() 函数打印 'continue',然后停在第 2 个 yield 语句处,生成值 'B'
    print("end.")  # 第 3 次调用 next() 函数打印 'end.',然后到达函数主体的末尾,导致生成器对象抛出StopIteration 异常。


# 列表推导式及早迭代 gen_AB() 函数返回的生成器对象产出的项,即 'A' 和 'B'。注意,下面的输出
# 是 start、continue 和 end.
res1 = [x * 3 for x in gen_AB()]
print(res1)
'''
start
countinue
end.
['AAA', 'BBB']
'''

# 这个 for 循环迭代列表推导式构建的 res1 列表
for i in res1:
    print("--->", i)
    """
    ---> AAA
    ---> BBB
    """


# 生成器表达式返回一个生成器对象,res2。该生成器在这里没有使用
res2 = (x * 3 for x in gen_AB())
print(res2)  # <generator object <genexpr> at 0x0000026A17658040>

# 仅当 for 循环迭代 res2 时,这个生成器才从 gen_AB 中获取项。for 循环每迭代一次就隐式调用一次
# next(res2),而它又在 gen_AB() 返回的生成器对象上调用 next(),提前执行到下一个 yield 语句
for i in res2:
    print("===>", i)

    # 注意,gen_AB() 的输出与 for 循环中 print 函数的输出交替出现
    """
    start
    ===> AAA
    countinue
    ===> BBB
    end.
    """

** 使用生成器表达式实现 Sentence 类 **

import re
import reprlib

RE_WORD = re.compile(r'w+')


class Sentence:
    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'

    def __iter__(self):
        return (match.group() for match in RE_WORD.finditer(self.text))

这里所用的不是生成器函数了(没有 yield),而是使用生 成器表达式构建生成器,然后将其返回。不过,最终效果一样:__iter__ 方法的调用方得到一个生成器对象。

五、 何时使用生成器表达式

 生成器表达式是创建生成器的简洁句法,无须先定义函数再调用。不过,生成器函 数更为灵活,可以使用多个语句实现复杂的逻辑,甚至可以作为协程使用 。

简单的情况下可以使用生成器表达式,因为这样扫一眼就知道代码的作用。

迭代器
泛指实现了 __next__ 方法的对象。迭代器用于生成供客户代码使用的数据,即客户代码通过 for 循环或其他迭代方式,或者直接在迭代器上调用 next(it) 驱动迭代器。不过,显式调用 next() 并不常见。实际上,我们在 Python 中使用的迭代器多数都是生成器。
生成器
由 Python 编译器构建的迭代器。为了创建生成器,我们不实现 __next__ 方法,而是使用 yield 关键字得到生成器函数(创建生成器对象的工厂)。生成器表达式是构建生成器对象的另一种 方式。生成器对象提供了 __next__ 方法,因此生成器对象是迭代器。Python 3.5 之后,还可以使用 async def声明异步生成器 。

六、 yield from:从子生成器中产出

Python 3.3 新增的 yield from 表达式句法可把一个生成器的工作委托给一个子生成器。  
引入 yield from 之前,如果一个生成器根据另一个生成器生成的值产出值,则需要使用 for 循环

def sub_gen():
    yield 1.1
    yield 1.2


def gen():
    yield 1
    for i in sub_gen():
        yield i
    yield 2


for x in gen():
    print(x)

使用 yield from 可以达到相同的效果

def sub_gen():
    yield 1.1
    yield 1.2


def gen():
    yield 1
    yield  from sub_gen()
    yield 2


for x in gen():
    print(x)

for 循环是客户代码,gen 是委托生成器,sub_gen 是子生成器。注意,yield from 暂停 gen,sub_gen 接手,直到它耗尽。sub_gen 产出的值绕过 gen,直接传给客户代码中的 for 循 环。在此期间,gen 处在暂停状态,看不到绕过它的那些值。当 sub_ gen 耗尽后,gen 恢复执行。

子生成器中有 return 语句时,返回一个值,在委托生成器中,通过含有 yield from 的表达式可以捕获 那个值。

def sub_gen():
    yield 1.1
    yield 1.2
    return 'Done!'


def gen():
    yield 1
    result = yield from sub_gen()
    print('<--', result)
    yield 2


for x in gen():
    print(x)

七、 经典协程

“PEP 342—Coroutines via Enhanced Generators”引入 .send() 方法和其他功能后,生成器可以 用作协程。在 PEP 342 中,“协程”一词的意思与我在这里使用的意思一样。 然而,Python 官方文档和标准库在指代用作协程的生成器时用语却不统一。因此,我不得不加上限定 语,使用“经典协程”,与新出现的“原生协程”对象区分开。 Python 3.5 发布之后,“协程”通常就是指“原生协程”。但是,PEP 342 还未废弃,经典协程最初的作用 没有改变,尽管已经不受 asyncio 支持。

生成器也有类似的情况。生成器通常用作迭代器,但是也可以用作协程。协程其实就是生成器函数,通过 主体中含有 yield 关键字的函数创建。自然,协程对象就是生成器对象。

7.1 例子 使用协程计算累计平均值

from collections.abc import Generator


# 这个函数返回一个生成器,该生成器产出 float 值,通过 .send() 接受 float 值,而且不返回有用
# 的值
def averager() -> Generator[float, float, None]:
    total = 0.0
    count = 0
    average = 0.0
    while True:  # 这个无限循环表明,只要客户代码不断发送值,它就会一直产出平均值。
        term = yield average  # 这里的 yield 语句暂停执行协程,把结果发给客户,而且稍后还用于接收调用方后面发给协程的值,再次开始无限循环迭代。
        total += term
        count += 1
        average = total / count

使用协程的好处是,total 和 count 声明为局部变量即可,在协程暂停并等待下一次调用 .send() 期 间,无须使用实例属性或闭包保持上下文。正是这一点吸引人们在异步编程中把回调换成协程,因为在多 次激活之间,协程能保持局部状态

from collections.abc import Generator


# 这个函数返回一个生成器,该生成器产出 float 值,通过 .send() 接受 float 值,而且不返回有用
# 的值
def averager() -> Generator[float, float, None]:
    total = 0.0
    count = 0
    average = 0.0
    while True:  # 这个无限循环表明,只要客户代码不断发送值,它就会一直产出平均值。
        term = yield average  # 这里的 yield 语句暂停执行协程,把结果发给客户,而且稍后还用于接收调用方后面发给协程的值,再次开始无限循环迭代。
        total += term
        count += 1
        average = total / count


if __name__ == "__main__":
    coro_avg = averager()  # 创建协程对象
    print(next(coro_avg))  # 0.0 开始执行协程。这里产出 average 变量的初始值,即 0.0。
    print(coro_avg.send(10))  # 10.0 真正计算累计平均值:多次调用 .send() 方法,产出当前平均值
    print(coro_avg.send(30))  # 20.0
    print(coro_avg.send(5))  # 15.0

调用 next(coro_avg) 后,协程向前执行到yield,产出 average变量的初始值。另外,也可以调用 coro_avg.send(None) 开始执行协程——这其实就是内置函数next()的作用。但是,不能发送 None 之外的值,因为协程只能在 yield 处暂停时接受发送的值。调用 next().send(None) 向前执行到第一个 yield 的过程叫作“预激协程”。
每次激活之后,协程在 yield 处暂停,等待发送值。coro_avg.send(10) 那一行发送一个值,激活协 程,yield 表达式把得到的值(10)赋给 term 变量。循环余下的部分更新 total、count 和 average 这 3 个变量的值。while 循环的下一次迭代产出 average 变量的值,协程在 yield 关键字处再一次暂停。

7.2  让协程返回一个值

from collections.abc import Generator
from typing import Union, NamedTuple


class Result(NamedTuple):
    count: int  # type: ignore 
    average: float


class Sentinel:
    def __repr__(self):
        return f'<Sentinel>'


STOP = Sentinel()
SendType = Union[float, Sentinel]


原文始发于微信公众号(Python之家):Python深入-13-迭代器、生成器

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198369.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!