第14章:可迭代的对象、迭代器、生成器

迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)。为此,Python 语言实现了“迭代器”和“生成器”。

在 Python 中,所有集合都可以迭代。在 Python 语言内部,迭代器用于支持:

  • for 循环
  • 构建和扩展集合类型
  • 逐行遍历文本文件
  • 列表推导、字典推导和集合推导
  • 元组拆包
  • 调用函数时,使用 * 拆包实参

本章涵盖以下话题:

  • 语言内部如何使用 iter(...) 内置函数处理可迭代对象
  • 如何使用 Python 实现经典的迭代器模式
  • 详细说明生成器函数的工作原理
  • 如何使用生成器函数或生成器表达式代替经典的迭代器
  • 如何使用 yield from 语句合并生成器
  • 为什么生成器和协程看似相同,实则差别很大,不能混淆

14.1 可迭代对象:iter() 函数如何把序列变得可以迭代

序列可以迭代的原因:iter 函数,解释器需要迭代对象 x 时,会隐式自动调用 iter(x)iter(x) 有一下几个作用:

  1. 检查对象是否实现了 __iter__ 方法,如果实现了就调用它,获取一个迭代器。
  2. 如果没有实现 __iter__ 方法,但是实现了 __getitem__ 方法, Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。
  3. 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“C object is not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。

        任何 Python 序列都可迭代的原因是,它们都实现了 __getitem__ 方法。 其实,标准的序列也都实现了 __iter__ 方法,之所以对 __getitem__ 方法做特殊处理,是为了向后兼容。

综上所属,我们可以给“可迭代对象”一个定义:

  1. 任何实现了 __iter__ 方法且返回一个迭代器的对象;
  2. 或者是实现了 __getitem__ 方法且索引从 0 开始的对象;

示例 14-1-1 自定义一个序列

class Sentence:

    def __init__(self, text: str):
        self.words = text.split()

    # 实现 __getitem__ 方法,且索引从 0 开始
    def __getitem__(self, item):
        print('call __getitem__ ...')
        return self.words[item]
    
    # 实现 __iter__ 方法
    def __iter__(self):
        print('call __iter__ ...')
        return iter(self.words)

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


s = Sentence('Hello Python')

# for 隐式调用 iter() 函数迭代序列
for i in s:
    print(i)

# 结果输出:
# call __iter__ ...
# Hello
# Python

从调用结果上看,Python 是调用了 __iter__ 来迭代 Sentence 序列,如果注释掉 __iter__ 的话,那么就会调用 __getitem__ 来迭代序列!

 

14.2 可迭代的对象与迭代器的对比

首先,我们要明确“可迭代对象”和“迭代器”之间的关系:Python 从可迭代的对象中获取迭代器。

例如下面这个示例:

s = 'ABC'

for i in s:
    print(i)

这是一个简单的 for 循环,迭代一个字符串,字符串 'ABC' 是可迭代对象。背后是有迭代器的,只不过我们看不到。

执行这段代码,Python 进行了以下工作:

  1. 使用可迭代的对象 s 构建迭代器 it

  2. 不断在迭代器上调用 next 函数,获取下一个字符;

  3. 如果没有字符了,迭代器会抛出 StopIteration 异常(for 内部会处理 StopIteration 异常);

  4. 释放对 it 的引用,即废弃迭代器对象;

  5. 退出循环;

从上可见,可迭代对象是 s,迭代器(不可见)是 it,可迭代对象 s 通过它的 __iter__ 方法来获取一个迭代器 it,然后迭代器 it 通过 __next__ 方法返回自身的下一个元素,如果没有元素了就抛出 StopIteration 异常,而且迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。

标准的迭代器接口必须实现两个方法:

  1. __next__:返回下一个可用的元素,如果没有元素了,抛出 StopIteration 异常。

  2. __iter__:返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中。

可迭代对象和迭代器的区别:
        可迭代的对象有个 __iter__ 方法,每次都实例化一个新的迭代器;而迭代器要实现 __next__ 方法,返回单个元素,此外还要实现 __iter__ 方法,返回迭代器本身。

 

14.3 实现典型的迭代器

⚠️ 注意,下面的示例并不符合 Python 的习惯做法,后面重构时会说明原因。不过,通过这一版能明确可迭代对象和迭代器对象之间的关系。

示例 14-3-1 中定义的 Sentence 类可以迭代,因为它实现了特殊的 __iter__ 方法,构建并返回一个 SentenceIterator 实例。

class SentenceIterator:
    """迭代器"""

    def __init__(self, words):
        # 1. 引用单词列表
        self.words = words
        # 2. self.index 用于确定下一个要获取的单词
        self.index = 0

    def __next__(self):
        try:
            # 3. 获取 self.index 索引位上的单词
            word = self.words[self.index]  
        # 4-1. 如果 self.index 索引位上没有单词,那么抛出 StopIteration 异常。
        except IndexError:  
            raise StopIteration()
        # 4-2. 获取值之后,递增 self.index,以便下次调用 __next__ 获取下一个元素。
        self.index += 1
        return word
    
    # 实现 self.__iter__ 方法,返回自身
    def __iter__(self):
        return self


class Sentence:
    """可迭代对象"""

    def __init__(self, text: str):
        self.words = text.split()
    
    # 根据可迭代协议,__iter__ 方法实例化并返回一个迭代器。
    def __iter__(self):
        return SentenceIterator(self.words)

    def __len__(self):
        return len(self.words)


s = Sentence('Hello World')

for i in s:
    print(i)

# 输出结果:
# Hello
# World

注意,对这个示例来说,其实没必要在 SentenceIterator 类中实现 __iter__ 方法(不实现也可以达到相同的效果),不过这么做是对的,因为迭代器应该实现 __next____iter__ 两个方法,如果不实现 __iter__ 那么迭代器自身便不可迭代了,例如这样:

st = SentenceIterator(['Hello', 'World'])

for i in st:
    print(i)

# 运行报错:
# Traceback (most recent call last):
#   File "/Users/PycharmProjects/FluentPython/demo.py", line 40, in <module>
#     for i in st:
# TypeError: 'SentenceIterator' object is not iterable

 

14.4 生成器函数

示例 14-4-1:更符合 Python 习惯的做法,用生成器函数重构 Sentence 类,不用再单独定义一个迭代器类;

class Sentence:
    """可迭代对象"""

    def __init__(self, text: str):
        self.words = text.split()

    def __iter__(self):
        # 迭代 self.words
        for word in self.words:
            # 产出当前的 word
            yield word

    def __len__(self):
        return len(self.words)


s = Sentence('Hello World')

for i in s:
    print(i)

# 输出结果:
# Hello
# World

在示例 14-3-1 定义的 Sentence 类中,__iter__ 方法调用 SentenceIterator 类的构造方法创建一个迭代器并将其返回。本节示例中,迭代器其实是生成器对象,每次调用 __iter__ 方法都会自动创建,因为这里的 __iter__ 方法是生成器函数。

生成器函数的工作原理:

生成器函数的定义:Python 函数的定义体中有 yield 关键字,该函数就是生成器函数;

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

原理:

  1. 生成器函数会创建一个生成器对象,包装生成器函数的定义体。
  2. 把生成器对象传给 next() 函数时,生成器对象会向前执行函数定义体中的下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂停;
  3. 执行外部的代码,然后再回到函数内暂停的位置执行后续的代码,直到下一个 yield 语句。
  4. 最终,生成器函数的定义体执行完毕,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。

示例 14-4-2 使用 for 循环清楚地说明了生成器函数定义体的执行过程

def gen_demo():
    print('start...')
    yield 'A'
    print('continue...')
    yield 'B'
    print('continue...')
    yield 'C'
    print('end!')


g = gen_demo()

print(next(g))  # 第一次调用 next() 函数时,会打印 'start...',然后停在第一个 yield 语句,生成值 'A';
# start...
# A
print(next(g))  # 第二次调用 next() 函数时,会打印 'continue...',然后停在第二个 yield 语句,生成值 'B';
# continue...
# B
print(next(g))  # 依此类推...
# continue...
# C
print(next(g))  # 三个 yield 执行完毕后,再回到生成器函数的定义体中执行后续代码(如有),代码执行完毕后由生成器对象抛出 StopIteration 异常
# end!

# Traceback (most recent call last):
#   File "/Users/PycharmProjects/FluentPython/demo.py", line 31, in <module>
#     print(next(g))
# StopIteration

 

14.5 惰性实现

“惰性求值”和“及早求值” 是编程语言理论方面的技术术语。Python 在设计 Iterator 接口时考虑到了惰性求值:next(my_iterator) 一次生成一个元素。

目前实现的几版 Sentence 类都不具有惰性,因为 __init__ 方法急迫地构建好了文本中的单词列表,然后将其绑定到 self.words 属性上。 这样就得处理整个文本,列表使用的内存量可能与文本本身一样多(或许更多,这取决于文本中有多少非单词字符)。

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成器,按需生成 re.MatchObject 实例。如果有很多匹配,re.finditer 函数能节省大量内存。我们要使用这个函数让 Sentence 类变得懒惰,即只在需要时才生成下一个单词。

示例 14-5-1

import re

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


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

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

 

14.6 生成器表达式

简单的生成器函数,如上面的 Sentence 类中使用的那个(见示例 14-5-1),可以替换成生成器表达式。

生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表, 而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂。

示例 14-6-1 演示了一个简单的生成器表达式,并且与列表推导做了对比;

# 列表推导
l1 = [x * 3 for x in gen()]
print(l1)  # ['AAA', 'BBB', 'CCC']

# 生成器表达式
g = (x * 3 for x in gen())
print(g)  # <generator object <genexpr> at 0x107eadd68>

print(next(g))  # AAA
print(next(g))  # BBB
print(next(g))  # CCC

示例 14-6-2 使用生成器表达式实现 Sentence

import re

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


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

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

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

14.7 yield from

如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的 for 循环,例如下面我们实现的生成器:

def chain(*iterables):
    for it in iterables:
        for i in it:
            yield i


s = 'ABC'
l = [1, 2, 3]

print(list(chain(s, l)))  # ['A', 'B', 'C', 1, 2, 3]

chain 生成器函数把操作依次交给接收到的各个可迭代对象处理。

然而,在 Python3.3 版本中引入了 yield from 语法,它可以从另一个可迭代对象或者迭代器获取元素,例如:

def chain(*iterables):
    for it in iterables:
        yield from it  # 直接从 it 中依次取值


s = 'ABC'
l = [1, 2, 3]

print(list(chain(s, l)))  # ['A', 'B', 'C', 1, 2, 3]

可以看出,yield from it 完全代替了内层的 for 循环。在这个示例中使用 yield from 是对的,而且代码读起来更顺畅,不过感觉更像是语法糖。除了代替循环之外,yield from 还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个 通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供的值。第 16 章会深入讲解协程,其中有几页会说明为什么 yield from 不只是语法糖而已。

 

14.8 深入分析 iter 函数

如前所述,在 Python 中迭代对象 x 时会调用 iter(x)

可是,iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个参数是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,停止产出值。

from random import randint


def demo():
    """返回一个随机值"""
    return randint(1, 5)


# 第一个参数必须是可调用对象,传方法名不要加()
# 第二个参数是哨符,iter 会不断调用 demo 返回随机值,知道随机值返回为 1 时,停止调用,并抛出 StopIteration 异常
d = iter(demo, 1)
print(d)  # <callable_iterator object at 0x10652fc50>

for i in d:  # 让 d 不断迭代,直到 demo 返回的值 == 1,并且隐式处理 StopIteration 异常;
    print(i)

# 结果输出:
# 3
# 5
# 5
# 3
# 4

 

14.9 把生成器当成协程

Python 2.2 引入了 yield 关键字实现的生成器函数,大约五年后, Python 2.5 实现了“PEP 342”,这个提案为 生成器对象添加了额外的方法和功能,其中最值得关注的是 .send() 方法。

.__next__() 方法一样,.send() 方法致使生成器前进到下一个 yield 语句。不过,.send() 方法还允许使用生成器的客户把数据发给自己,即不管传给 .send() 方法什么参数,那个参数都会成为生成器函数定义体中对应的 yield 表达式的值。也就是说,.send() 方法允许在客户代码和生成器之间双向交换数据。而 .__next__() 方法只允许客户从生成器中获取数据。

这是一项重要的“改进”,甚至改变了生成器的本性:像这样使用的话, 生成器就变身为协程。但是生成器不是协程,两者不能混为一谈,所以在 PyCon US 2009 期间举办的一场著名的课程中 (http://www.dabeaz.com/coroutines/),David Beazley提醒道:

  • 生成器用于生成供迭代的数据

  • 协程是数据的消费者

  • 为了避免脑袋炸裂,不能把这两个概念混为一谈

  • 协程与迭代无关

  • 虽然在协程中会使用 yield 产出值,但这与迭代无关

请务必遵循以上建议;至此本章结束,不涉及把生成器当成协程使用的 send() 方法和其他特性,这些会在第16章协程中讨论。

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值