1

可迭代的对象、迭代器和生成器

理念

迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据
项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)。

看个例子

import re
import reprlib

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


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

    def __getitem__(self, index):
        return self.words[index]

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

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


s = Sentence('"The time has come," the Walrus said,')
print(s)

for world in s:
    print(world)
for 循环就是 用到了index, index在__getitem__被用到了

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

解释器需要迭代对象 x 时,会自动调用 iter(x)。

内置的 iter 函数有以下作用。

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

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

Python 从可迭代的对象中获取迭代器

标准的迭代器接口有两个方法。

next
  返回下一个可用的元素,如果没有元素了,抛出 StopIteration 异常。
iter
  返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中。

clipboard.png

abc.Iterator 类

from  abc import abstractmethod

class Iterator(Iterable):
    __slots__ = ()

    @abstractmethod
    def __next__(self):
        'Return the next item from the iterator. When exhausted, raise StopIteration'
        raise StopIteration

    def __iter__(self):

        return self

    @classmethod
    def __subclasshook__(cls, C):

        if cls is Iterator:
            if (any("__next__" in B.__dict__ for B in C.__mro__) and
                    any("__iter__" in B.__dict__ for B in C.__mro__)):
                return True

        return NotImplemented

如何使用 next(...) 函数使用迭代器

import re
import reprlib

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


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

    def __getitem__(self, index):
        return self.words[index]

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

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


# s = Sentence('"The time has come," the Walrus said,')
# print(s)
#
# for world in s:
#     print(world)

## it就是构造后的迭代器
## iter(s3) 就是构建迭代器的可迭代对象。
s3 = Sentence('Pig and Pepper')
it = iter(s3)

print(next(it))
print(next(it))
print(next(it))

# 报错 StopIteration
# print(next(it))

print(list(it))

print(list(iter(s3)))
print(list(iter(s3)))

因为迭代器只需 nextiter 两个方法,所以除了调用 next() 方法,以及捕获 StopIteration 异常之外,没有办法检查是否还有遗留的元素。

此外,也没有办法“还原”迭代器。

如果想再次迭代,那就要调用 iter(...),传入之前构建迭代器的可迭代对象。

传入迭代器本身没用,因为前面说过 Iterator.__iter__ 方法的实现方式是
返回实例本身,所以传入迭代器无法还原已经耗尽的迭代器。

典型的迭代器

import re
import reprlib

RE_WORD = re.compile('\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):
        return SentenceIterator(self.words)


class SentenceIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __next__(self):
        try:
            word = self.words[self.index]
        except IndexError:
            raise StopIteration()

        self.index += 1
        return word

    def __iter__(self):
        return self

s = Sentence('"The time has come," the Walrus said,')
for word in s:
    print(word)

❶ 与前一版相比,这里只多了一个 iter 方法。这一版没有 getitem 方法,为
的是明确表明这个类可以迭代,因为实现了 iter 方法。
❷ 根据可迭代协议,__iter__ 方法实例化并返回一个迭代器。

Sentence 类中,__iter__ 方法调用 SentenceIterator 类的构造方法创建一个迭代器并将其返回。

为什么 不写在一起

  • 把Sentence变成迭代器:坏主意

构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。

要知道,可迭代的对象有个 iter 方法,每次都实例化一个新的迭代器;
而迭代器要实现 next 方法,返回单个元素,此外还要实现 iter 方法,返回迭代器本身。

因此,迭代器可以迭代,但是可迭代的对象不是迭代器。

除了 iter 方法之外,你可能还想在 Sentence 类中实现 next 方法,让
Sentence 实例既是可迭代的对象,也是自身的迭代器。

可是,这种想法非常糟糕。根据有大量 Python 代码审查经验的 Alex Martelli 所说,这也是常见的反模式。

python的正确解决之道

生成器函数

import re
import reprlib

RE_WORD = re.compile('\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:
            yield word
        return


s = Sentence('"The time has come," the Walrus said,')
for word in s:
    print(word)

❸ 这个 return 语句不是必要的;这个函数可以直接“落空”,自动返回。不管有没有
return 语句,生成器函数都不会抛出 StopIteration 异常,而是在生成完全部值之后会直接退出。
❹ 不用再单独定义一个迭代器类!

迭代器其实是生成器对象,每次调用 iter 方法都会自动创建,因为这里的 iter 方法是生成器函数。
生成器函数的工作原理
  • 只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂
def gen_123():  # ➊
    yield 1  # ➋
    yield 2
    yield 3

print(gen_123)

print(gen_123())

for i in gen_123():  # ➎
    print(i)

g = gen_123()  # ➏
print(next(g))
print(next(g))
print(next(g))

## 报错 StopIteration
# print(next(g))

❸ 仔细看,gen_123 是函数对象。
❻ 为了仔细检查,我们把生成器对象赋值给 g。
❽ 生成器函数的定义体执行完毕后,生成器对象会抛出 StopIteration 异常。

把生成器传给next(...) 函数时,生成器函数会向前,执行函数定义体中的下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂停。

最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。

惰性实现

import re
import reprlib

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


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

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

    def __iter__(self):
        # 返回一个迭代器
        for match in RE_WORD.finditer(self.text):
            yield match.group()

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成
器,按需生成 re.MatchObject 实例。

如果有很多匹配,re.finditer 函数能节省大量内存。
我们要使用这个函数让第 4 版 Sentence 类变得懒惰,即只在需要时才生成下一个单词。

❶ 不再需要 words 列表。
❷ finditer 函数构建一个迭代器,包含 self.text 中匹配 RE_WORD 的单词,产出
MatchObject 实例。
❸ match.group() 方法从 MatchObject 实例中提取匹配正则表达式的具体文本。

生成器表达式

生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。

def gen_AB():  # ➊
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end.')


res1 = [x * 3 for x in gen_AB()]

for i in res1:  # ➌
    print('-->', i)

res2 = (x * 3 for x in gen_AB())  # ➍
print(res2)  # ➎

for i in res2:  # ➏
    print('-->', i)

❷ 列表推导迫切地迭代 gen_AB() 函数生成的生成器对象产出的元素:'A' 和 'B'。注意,下面的输出是 start、continue 和 end.。

❸ 这个 for 循环迭代列表推导生成的 res1 列表。

❹ 把生成器表达式返回的值赋值给 res2。只需调用 gen_AB() 函数,虽然调用时会返回
一个生成器,但是这里并不使用。

❺ res2 是一个生成器对象。
❻ 只有 for 循环迭代 res2 时,gen_AB 函数的定义体才会真正执行。
for 循环每次迭代时会隐式调用 next(res2),前进到 gen_AB 函数中的下一个yield 语句。

注意,gen_AB 函数的输出与 for 循环中 print 函数的输出夹杂在一起。

何时使用生成器表达式

根据我的经验,选择使用哪种句法很容易判断:如果生成器表达式要分成多行写,我倾向
于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用。

8 另一个示例:等差数列生成器

class ArithmeticProgression:
    def __init__(self, begin, step, end=None):  # ➊
        self.begin = begin
        self.step = step
        self.end = end  # None -> 无穷数列


def __iter__(self):
    result = type(self.begin + self.step)(self.begin)  # ➋
    forever = self.end is None  # ➌
    index = 0
    while forever or result < self.end:  # ➍
        yield result  # ➎
    index += 1
    result = self.begin + self.step * index  # ➏

❸ 为了提高可读性,我们创建了 forever 变量,如果 self.end 属性的值是 None那么forever 的值是 True,因此生成的是无穷数列。
❹ 这个循环要么一直执行下去,要么当 result 大于或等于 self.end 时结束。如果循环退出了,那么这个函数也随之退出。

在示例 14-11 中的最后一行,我没有直接使用 self.step 不断地增加 result,

而是选择使用 index 变量,把 self.begin 与 self.step 和 index 的乘积加,计算 result 的各个值,

以此降低处理浮点数时累积效应致错的风险。

标准库里有大量的生成器轮子 page408

深入分析iter函数

可是,iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调
用的对象创建迭代器。

这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;

第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。

iter 函数掷骰子,直到掷出 1 点为止

from random import randint

def d6():
    return randint(1, 6)

d6_iter = iter(d6, 1)

print(d6_iter)

for roll in d6_iter:
    print(roll)

总结

  • 迭代器 都有 iter这个方法(函数), (每个都调用__getitem__做兼容) next()到底.
  • for每次迭代都是next()
  • 对付大文件,大内存. 先返回迭代器对象, 在执行迭代器的具体 操作
  • python 用yield 作为生成器的特征
  • send可以发送给 那个状态的生成器
  • iter别的用法 有两个参数 第二个是哨符 (遇到就停了)

小小梁
23 声望15 粉丝