前言

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

所有生成器都是迭代器,因为生成器完全实现了迭代器接口

在Python3 中,生成器有广泛的用途。现在,即使是内置的range()函数也返回一个类似生成器的对象,而以前则返回完整的列表

iter()

解释器需要迭代对象x时,会自动调用iter(x).
内置的iter函数有以下作用:

  • 检查对象是否实现了__iter__(),如果实现了就调用它,从而获取一个迭代器
  • __iter__()不存在时转而调用__getitem__(), python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素
  • 抛出TypeError, 提示object is not iterable

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

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

1
2
3
4
5
6
s = 'ABC'
for ch in s:
    print(s)
>>> A
>>> B
>>> C

若使用while, 使用可迭代的对象构建迭代器it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
s = 'ABC'
it = iter(s)
while True:
    try:
        print(next(it))
    except StopIteration:
        del it
        break
>>> A
>>> B
>>> C

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

  • __next__(), 返回下一个可用的元素,如果没有元素了则抛出StopIteration异常
  • __iter__(), 返回self, 以便在应该使用可迭代对象的地方使用迭代器

这个接口在collections.abc.Iterator抽象基类中制定。这个类定义了__next__()抽象方法,而且继承自 Iterable类;__iter__抽象方法则在Iterable类中定义, 如下图所示

【图片】

检查对象x是否为迭代器最好的方式是调用isinstance(x, abc.Iterator)

Iterators in Python aren’t a matter of type but of protocol. A large and changing number of builtin types implement some flavor of iterator. Don’t check the type! Use hasattr to check for both __iter__ and __next__ attributes instead. 即python里的迭代器不是类型,是协议

把Sentence变成迭代器:坏主意

原版Sentence

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import re
import reprlib

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = re.compile('\w+').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 word in s:
    print(word)

使用迭代器模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import re
import reprlib

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = re.compile('\w+').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,')
print(s)

for word in s:
    print(word)

迭代器模式可用来:

  • 访问一个聚合对象的内容而无需暴露它的内部表示
  • 支持对聚合对象的多种遍历
  • 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)

为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态,因此这一模式正确的实现方式是,每次调用iter(my_iterable)都新建一个独立的迭代器

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

ps:这里我就不明白了,对于迭代器的__iter__()改个名字不久行了?

可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__方法,但不能实现__next__方法。

更符合Python习惯的方式实现Sentence类(使用生成器)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import re
import reprlib

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = re.compile('\w+').findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)
    
    def __iter__(self):
        for word in self.words:
            yield word

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

本书的审查Alex Martelli建议这样编写__iter__

1
2
def __iter__(self):
    return iter(self.words)

即利用words(list类型)的__iter__来实现

生成器函数的工作原理

只要python函数的定义体中有yield关键字, 该函数就是生成器函数。调用生成器函数时会返回一个生成器对象,也就是说生成器函数是生成器工厂(我理解为生成器的工厂函数)(yield理解为生产出)

生成器不会以常规的方式“返回”值(即return:生成器函数定义体中的return语句会触发生成器对象抛出StopIteration异常

一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def gen_123():
    yield 1
    yield 2
    yield 3
print(gen_123)
>>> <function gen_123 at 0x7f8dd8044e18>
print(gen_123())
>>> <generator object gen_123 at 0x7f8dd7f67468>

for x in gen_123():
    print(x)
>>> 1
>>> 2
>>> 3

g = gen_123()
print(next(g))
>>> 1
print(next(g))
>>> 2
print(next(g))
>>> 3
print(next(g))
>>> StopIteration

for机制会捕获异常,因此循环终止时没有报错

Python3.3之前,如果生成器函数中的return语句有返回值,那么会报错。现在可以这么做,不过return语句 会导致StopIteration异常抛出。调用方可以从异常对象中获取返回值,不过只有把生成器函数当成协程使用 时,这么做才有意义

re.finditer函数是re.findall函数的惰性版本,返回的不是列表,而是一个生成器,按需生成re.MatchObject实例。如果有很多匹配,re.finditer函数能节省大量内存

生成器表达式

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

使用生成器表达式实现Sentence

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import re
import reprlib

class Sentence:
    def __init__(self, text):
        self.text = text
    
    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)
    
    def __iter__(self):
        return (match.group() for match in re.compile('\w+').finditer(self.text))

生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利

等差数列生成器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class ArithmeticProgression:
    def __init__(self, begin, step, end=None):
        self.begin = begin
        self.step = step
        self.end = end

    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
g = ArithmeticProgression(0, 2)
for i, v in enumerate(g):
    print(i, v)
    if i > 10:
        break
    

itertools模块

使用itertools.count()生成等差数列

1
2
3
4
5
6
from itertools import count
gen = count(0, 2)
for i, v in enumerate(gen):
    print(i, v)
    if i > 10:
        break

itertools.takewhile()

itertools.takewhile(predicate, iterable), 对于iterable中的元素,满足predicate的条件,则结束,大致等于

1
2
3
4
5
6
7
def takewhile(predicate, iterable):
    # takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
    for x in iterable:
        if predicate(x):
            yield x
        else:
            break

利用takewhile()实现之前的那两个例子(生成从0开始的12个偶数)

1
2
3
from itertools import count, takewhile
gen = takewhile(lambda n:n<=22, count(0, 2))
print(list(gen))

标准库中的生成器函数

第一组是用于过滤的生成器函数:从输入的可迭代对象中产出元素的子集,而且不修改元素本身

模块 函数 说明
itertools compress(it, selector_it) 并行处理两个可迭代的对象;如果selector_it中的元素是真值,产出it中对应的元素
itertools dropwhile(predicate, it) 处理it, 跳过predicate的计算结果为真值的元素,然后产出剩下的各个元素
内置 filter(predicate, it) it中的各个元素传给predicate, 如果predicate(item)返回True,那么产出对应的元素
itertools filterfalse(predicate, it) predicate返回假值时产出对应的元素
itertools islice(it, stop)或者islice(it, start, stop, step=1) 产出it的切片, 作用类似于s[:stop]s[start:stop:step], 不过it可以是任何可迭代的对象, 而且这个函数实现的是惰性操作
itertools takewhile(predicate, it) predicate返回真值时产出对应的元素,然后立即停止,不再继续

『图片』

第二组生成器函数会从输入的可迭代对象中的各个元素中产出一个元素。如果输入来自多个可迭代的对象,第一个可迭代的对象到头后就停止输出。

模块 函数 说明
itertools accumulate(it, [func]) 产出累积的总和; 如果提供了func, 那么把前两个元素传给它, 然后把计算结果和下一个元素传给它,以此类推,最后产出结果
内置 enumerate(iterable, start=0) 产出由两个元素组成的元组,结构是(index, item), 其中indexstart开始计数, item则从iterable中获取
内置 map(func, it1,[it2, ..., itN]) it中的各个元素传给func,产出结果;如果传入N个可迭代的对象,那么func必须能接受 N 个参数,而且要并行处理各个可迭代的对象
itertools starmap(func, it) it中的各个元素传给func ,产出结果;输入的可迭代对象应该产出可迭代的元素 iit ,然后以 func(*iit) 这种形式调用 func

『图片』

第三组是用于合并的生成器函数,这些函数都从输入的多个可迭代对象中产出元素。chainchain.from_iterable按顺序(一个接一个)处理输入的可迭代对象,而productzipzip_longest并行处理输入的各个可迭代对象

模块 函数 说明
itertools chain(it1, ..., itN) 先产出it1中的所有元素,然后产出it2中的所有元素,以此类推,无缝连接在一起`
itertools chain.from_iterable(it) 产出it生成的各个可迭代对象中的元素,一个接一个,无缝连接在一起; it应该产出可迭代的元素,例如可迭代的对象列表
itertools product(it1, ..., itN, repeat=1) 计算笛卡儿积, repeat 指明重复处理多少次输入的可迭代对象
内置 zip(it1, ..., itN) 并行从输入的各个可迭代对象中获取元素,产出由 N 个元素组成的元组,只要有一个可迭代的对象到头了,就默默地停止
itertools zip_longest(it1, ...,itN, fillvalue=None) 并行从输入的各个可迭代对象中获取元素,产出由N个元素组成的元组,等到最长的可迭代对象到头后才停止,空缺的值使用fillvalue填充

第四组生成器函数会从一个元素中产出多个值,扩展输入的可迭代对象

模块 函数 说明
itertools combinations(it, out_len) it产出的out_len个元素组合在一起(数学意义上的组合),然后产出
itertools combinations_with_replacement(it,out_len) it产出的out_len个元素组合在一起,然后产出,包含相同元素的组合`
itertools count(start=0, step=1) start开始不断产出数字,按step指定的步幅增加
itertools cycle(it) it中产出各个元素, 存储各个元素的副本,然后按顺序重复不断地产出各个元素
itertools permutations(it, out_len=None) out_lenit产出的元素排列在一起,然后产出这些排列; out_len的默认值等于len(list(it)) (即生成数学意义上的排列,长度为out_len)
itertools repeat(item, [times]) 重复不断地产出指定的元素,除非提供times指定次数

第五组生成器函数用于产出输入的可迭代对象中的全部元素,不过会以某种方式重新排列

模块 函数 说明
itertools groupby(it,key=None) 产出由两个元素组成的元素,形式为(key, group), 其中key是分组标准, group是生成器,用于产出分组里的元素(假定输入的可迭代对象要使用分组标准排序)
内置 reversed(seq) 从后向前,倒序产出 seq 中的元素; seq 必须是序列,或者是实现了__reversed__特殊方法的对象
itertools tee(it, n=2) 产出一个由 n 个生成器组成的元组,每个生成器用于单独产出输入的可迭代对象中的元素

成功抄吐了,更多详情请看文档,我都怀疑我为什么要写这没营养的读书笔记了

yield from

实现自己的chain生成器

1
2
3
4
5
6
7
8
9
def chain(*iterables):
    for it in iterables:
        for i in it:
            yield i

s = 'ABC'
t = list(range(3))
print(list(chain(s, t)))
>>> ['A', 'B', 'C', 0, 1, 2]

chain生成器函数把操作依次交给接收到的各个可迭代对象处理。为此,PEP 380 — Syntax for Delegating to a Subgenerator(https://www.python.org/dev/peps/pep-0380/)引入了一个新句法(翻译为委托给子生成器)

1
2
3
4
5
6
7
def chain(*iterables):
    for i in iterables:
        yield from i
s = 'ABC'
t = list(range(3))
print(list(chain(s, t)))
>>> ['A', 'B', 'C', 0, 1, 2]

可以看出, yield from i完全代替了内层的for循环. 除了代替循环之外, yield from还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供的值(16章再继续讲)

ps现在再看yield from, yield from i是把i自动生成了一个生成器?

深入分析iter函数

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

1
2
3
4
5
6
7
from random import randint
def d6():
    return randint(1, 6)

dg = iter(d6, 1)
for x in dg:
    print(x)

用随机数函数d6生成随机数生成器dg,不断的next(dg)直到遇到x=1抛出StopIteration(for循环会pass这个异常)

利用这一功能,这里有个实用的例子,这段代码逐行读取文件,直到遇到空行或者到达文件末尾为止:

1
2
3
with open('abc.txt') as fp:
    for line in iter(fp.readline, '\n'):
        process_line(line)

ps: 只是一种启发

把生成器当协程

__next__()方法一样,send()方法致使生成器前进到下一个yield语句。不同的是,send()方法还允许使用生成器的客户把数据发给自己,即不管传给send()方法什么参数,那个参数都会成为生成器函数定义体中对应的yield表达式的值。这意味着, send()方法允许在客户代码和生成器之间双向交换数据。而__next__()方法只允许客户从生成器中获取数据。这是一项重要的“改进”,甚至改变了生成器的本性:像这样使用的话,生成器就变身为协程

  • 生成器用于生成供迭代的数据
  • 协程是数据的消费者
  • 为了避免脑袋炸裂,不能把这两个概念混为一谈
  • 协程与迭代无关(注意,虽然在协程中会使用yield产出值,但这与迭代无关