所有生成器都是迭代器,因为生成器完全实现了迭代器接口,不过迭代器一般用于从集合取出元素,生成器用于 “凭空” 创造元素。斐波那契数列例子可以很好的说明两者区别:斐波那契数列中的数有无穷个,在一个集合里放不下。

在 Python 3 中,生成器有广泛用途。现在即使是内置的 range() 函数也要返回一个类似生成器的对象,而以前返回完整列表。如果一定让 range() 函数返回列表,必须明确指明(例如,list(range(100)))。

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

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

本章探讨以下话题:

  • 语言内部使用 iter(...) 内置函数处理可迭代对象的方式
  • 如何使用 Python 经典的迭代器模式
  • 详细说明生成器函数的工作原理
  • 如何使用生成器函数或生成器表达式代替经典的迭代器
  • 如何使用标准库中通用的生成器函数
  • 如何使用 yield from 语句合并生成器
  • 案例分析: 在一个数据库转换工具中使用生成器处理大型数据集
  • 为什么生成器和协程看似相同,其实差别很大,不能混淆

Sentence 类第 1 版:单词序列

我们创建一个类,并向它传入一些包含文本的字符串,然后可以逐个单词迭代,第 1 版要实现序列协议,这个类的对象可以迭代,因为所有序列都可以迭代 -- 这一点前面已经说过,现在说明真正的原因

下面展示了一个可以通过索引从文本提取单词的类:

In [3]:
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]
    
    # 为了完善序列协议,我们实现了 __len__ 方法,不过,为了让对象可迭代,没必要实现这个方法
    def __len__(self):
        return len(self.words)
    
    def __repr__(self):
        # 下面这个函数用于生成大型数据结构的简略字符串表示形式
        return 'Sentence(%s)' % reprlib.repr(self.text)
In [4]:
s = Sentence('"The time has come,", the Walrus said')
s
Out[4]:
Sentence('"The time ha...e Walrus said')
In [5]:
for word in s:
    print(word)
The
time
has
come
the
Walrus
said
In [6]:
list(s)
Out[6]:
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
In [7]:
s[0], s[-1]
Out[7]:
('The', 'said')

我们都知道,序列可以迭代,下面说明具体原因: iter 函数

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

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

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

  2. 如果没有实现 __iter__ 方法,但是实现了 __getitem__ 方法,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素

  3. 如果尝试失败,Python 抛出 TypeError 异常,通常提示 C object is not iterable,其中 C 是目标对象所属的类

任何 Pytho 序列都可迭代的原因是实现了 __getitem__ 方法。其实标准的序列也都实现了 __iter__ 方法,因此我们也应该这么做。之所以对 __getitem__ 方法特殊处理,是为了向后兼容,未来可能不会再这么做

11 章提到过,这是鸭子类型的极端形式,不仅要实现特殊的 __iter__ 方法,还要实现 __getitem__ 方法,而且 __getitem__ 方法的参数是从 0 开始的整数(int),这样才认为对象是可迭代的。

在白鹅类型理论中,可迭代对象定义的简单一些,不过没那么灵活,如果实现了 __iter__ 方法,那么就认为对象是可迭代的。此时,不需要创建子类,也不需要注册,因为 abc.Iterable 类实现了 __subclasshook__ 方法,下面举个例子:

In [1]:
from collections import abc

class Foo:
    def __iter__(self):
        pass
    
issubclass(Foo, abc.Iterable)
Out[1]:
True
In [2]:
f = Foo()
isinstance(f, abc.Iterable)
Out[2]:
True

不过要注意,前面定义的 Sentence 类是可迭代的,却无法通过 issubclass(Sentence, abc.Iterable) 测试

从 Python 3.4 开始,检测对象 x 是否可迭代,最准确的方法是调用 iter(x) 函数,如果不可迭代,再处理 TypeError 异常,这回比使用 isinstance(x, abc.Iterable) 更准确,因为 iter(x) 会考虑到 __getitem__ 方法

迭代对象之前显式检查或许没必要,因为试图迭代不可迭代对象时,抛出的错误很明显。如果除了跑出 TypeError 异常之外还要进一步处理,可以使用 try/except 块,无需显式检查。如果要保存对象,等以后迭代,或许可以显式检查,因为这种情况需要尽早捕捉错误

可迭代对象与迭代器对比

可迭代对象:

使用 iter 内置函数可以获取迭代器对象。如果对象实现了能返回迭代器的 __iter__ 方法,那么对象可迭代。序列都可以迭代:实现了 __getitem__ 方法,而且其参数是从 0 开始的索引,这种对象也可以迭代。

我们要明确可迭代对象和迭代器之间的关系: Python 从可迭代的对象中获取迭代器

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

In [10]:
s = 'ABC'
for char in s:
    print(char)
A
B
C

如果用 while 循环,要像下面这样:

In [11]:
s = 'ABC'
it = iter(s)
while True:
    try:
        print(next(it))
    except StopIteration: # 这个异常表示迭代器到头了
        del it
        break
A
B
C

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

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

__iter__ 返回 self,以便在应该使用可迭代对象的地方使用迭代器,比如 for 循环

这个接口在 collections.abc.Iterator 抽象基类中,这个类定义了 __next__ 抽象方法,而且继承自 Iterable 类: __iter__ 抽象方法则在 Iterable 类中定义

abc.Iterator 抽象基类中 __subclasshook__ 的方法作用就是检查有没有 __iter____next__ 属性

检查对象 x 是否为 迭代器 的最好方式是调用 isinstance(x, abc.Iterator)。得益于 Iterator.__subclasshook__ 方法,即使对象 x 所属的类不是 Iterator 类的真实子类或虚拟子类,也能这样检查

下面可以看到 Sentence 类如何使用 iter 函数构建迭代器,和如何使用 next 函数使用迭代器

In [13]:
s3 = Sentence('Pig and Pepper')
it = iter(s3)
it
Out[13]:
<iterator at 0x7f0a04fc5080>
In [14]:
next(it)
Out[14]:
'Pig'
In [15]:
next(it)
Out[15]:
'and'
In [16]:
next(it)
Out[16]:
'Pepper'
In [17]:
next(it)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-17-2cdb14c0d4d6> in <module>()
----> 1 next(it)

StopIteration: 
In [18]:
list(it) # 到头后,迭代器没用了
Out[18]:
[]
In [20]:
list(s3) # 如果想再次迭代,要重新构建迭代器
Out[20]:
['Pig', 'and', 'Pepper']

因为迭代器只需要 __next____iter__ 两个方法,所以除了调用 next() 方法,以及捕获 StopIteration 异常之外,没有办法检查是否还有遗留元素。此外,也没有办法 ”还原“ 迭代器。如果想再次迭代,那就要调用 iter(...) 传入之前构造迭代器传入的可迭代对象。传入迭代器本身没用,因为前面说过 Iterator.__iter__ 方法实现方式是返回实例本身,所以传入迭代器无法还原已经耗尽的迭代器

我们可以得出迭代器定义如下:实现了无参数的 __next__ 方法,返回序列中的下一个元素,如果没有元素了,那么抛出 StopIteration 异常。Python 中迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。因为内置的 iter(...) 函数会对序列做特殊处理,所以第 1 版 的 Sentence 类可以迭代。

Sentence 类第 2 版:典型的迭代器

这一版根据《设计模式:可复用面向对象软件的基础》一书给出的模型,实现典型的迭代器设计模式。注意,这不符合 Python 的习惯做法,后面重构时候会说明原因。不过,通过这一版能明确可迭代集合和迭代器对象之间的区别

下面的类可以迭代,因为实现了 __iter__ 方法,构建并返回一个 SentenceIterator 实例,《设计模式:可复用面向对象软件的基础》一书就是这样描述迭代器设计模式的。

这里之所以这么做,是为了清楚的说明可迭代的对象和迭代器之间的重要区别,以及二者间的联系。

In [2]:
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

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

注意 SentenceIterator 类的大多数代码在处理迭代器内部状态,稍后会说明如何简化,不过我们先讨论一个看似合理实则错误的实现捷径

把 Sentence 变成迭代器:坏主意

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

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

除了 __iter__ 方法之外,你可能还想在 Sentence 类中实现 __next__ 方法,让 Sentence 实例既是可迭代对象,也是自身迭代器,可是这种想法非常糟糕,这也是常见的反模式

迭代器模式可以用来:

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

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

可迭代对象一定不能是自身的迭代器,也就是说,可迭代对象必须实现 __iter__ 方法,但不能实现 __next__ 方法。另一方面,迭代器应该可以一直迭代,迭代器的 __iter__ 应该返回自身

Sentence 类第 3 版:生成器函数

实现同样功能,却符合 Python 习惯的方式是,用生成器函数替代 SentenceIterator 类。先看下面的例子:

In [13]:
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 不是必要的,生成器函数不会抛出 StopIteration 异常,
        #而是在生成全部值之后直接退出
        return 
In [21]:
a = Sentence('hello world')
one = iter(a)
print(next(one))
two = iter(a)
print(next(two)) # 两个迭代器之间不会互相干扰
hello
hello

在这个例子中,迭代器其实是生成器对象,每次调用 __iter__ 方法都会自动创建,因为这里的 __iter__ 方法是生成器函数

生成器函数的工作原理

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

下面用一个特别简单的函数说明生成器行为:

In [22]:
def gen_123():
    yield 1
    yield 2
    yield 3
    
gen_123
Out[22]:
<function __main__.gen_123>
In [23]:
gen_123()
Out[23]:
<generator object gen_123 at 0x7f2d2014d3b8>
In [24]:
for i in gen_123():
    print(i)
1
2
3
In [25]:
g = gen_123()
next(g)
Out[25]:
1
In [26]:
next(g)
Out[26]:
2
In [27]:
next(g)
Out[27]:
3
In [28]:
next(g) # 生成器函数定义体执行完毕后,跑出 StopIteration 异常
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-28-42ec62f89883> in <module>()
----> 1 next(g) # 生成器函数定义体执行完毕后,跑出 StopIteration 异常

StopIteration: 

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

下面例子更清楚的说明了生成器函数定义体的执行过程:

In [29]:
def gen_AB():
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end')
    
for c in gen_AB():
    print('-->', c)
start
--> A
continue
--> B
end

现在在我们应该知道 Sentence.__iter__ 作用了: __iter__ 方法是生成器函数,调用时会构建一个实现了迭代器接口的生成器对象,因此不用再定义 SentenceIterator 类了。

这一版 Sentence 类比之前简短多了,但还不够懒惰,懒惰实现是指尽可能延后生成值,这样能节省内存,或许还可以避免做无用的处理

Sentence 类第 4 版:惰性实现

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

目前的 Sentence 类不具有惰性,因为 __init__ 方法急迫的构建好了文本中的单词列表,然后绑定到 self.words 属性上。这样就得到处理后的整个文本,列表使用的内存量可能与文本本身一样多(获取更多,这取决于文本中有多少非单词字符)。如果只需迭代前几个单词,大多数工作都是白费力气。

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

In [30]:
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() # 从 MatchObject 实例中提取匹配正则表达式的具体文本

生成器表达式

简单的生成器函数,如前面的例子中使用的那个,可以替换成生成器表达式

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

下面展示了一个生成器表达式,并与列表推导式对比:

In [3]:
def gen_AB():
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end')
    
res1 = [x * 3 for x in gen_AB()]
start
continue
end
In [4]:
for i in res1:
    print('-->', i)
--> AAA
--> BBB
In [5]:
res2 = (x * 3 for x in gen_AB())
res2
Out[5]:
<generator object <genexpr> at 0x7f571b179990>
In [6]:
for i in res2:
    print('-->', i)
start
--> AAA
continue
--> BBB
end

可以看出,生成器表达式会产出生成器,因此可以使用生成器表达式进一步减少 Sentence 类的代码:

In [7]:
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):
        return (match.group() for match in RE_WORD.finditer(self.text))

这里用的是生成器表达式构建生成器,然后将其返回,不过最终效果一样:调用 __iter__ 方法会得到一个生成器对象

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

何时使用生成器表达式

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

如果生成器表达式要分成多行,最好使用生成器函数,提高可读性

如果函数或构造方法只有一个参数,传入生成器表达式时不用写一堆调用函数的括号,再写一堆括号围住生成器表达式,只写一对括号就行,如果生成器表达式后面还有其他参数,那么必须使用括号围住,否则会抛出 SynataxError 异常

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

In [10]:
class ArithmeticProgression:
    
    def __init__(self, begin, step, end=None):
        self.begin = begin
        self.step = step
        self.end = end # 无穷数列
        
    def __iter__(self): 
         # self 赋值给 result,不过要先强制转成前面加法表达式类型(两个支持加法的对象返回一个对象)
        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
In [11]:
ap = ArithmeticProgression(0, 1, 3)
list(ap)
Out[11]:
[0, 1, 2]
In [12]:
ap = ArithmeticProgression(1, 5, 3)
list(ap)
Out[12]:
[1]
In [13]:
ap = ArithmeticProgression(0, 1 / 3, 1)
list(ap)
Out[13]:
[0.0, 0.3333333333333333, 0.6666666666666666]

上面的类完全可以用一个生成器函数代替

In [14]:
def aritprog_gen(begin, step, end=None):
    result = type(begin + step)(begin)
    forever = end is None
    index = 0
    while forever or result < end:
        yield result
        index += 1
        result = begin + step * index

上面的实现很棒,但是要记住,标准库中有很多现成的生成器,下面会用 itertools 模块实现,这个版本更棒

使用 itertools 生成等差数列

itertools 提供了 19 个生成器函数,结合起来很有意思。

例如 itertools.count 函数返回的生成器能生成多个数。如果不传入参数,itertools.count 函数会生成从 0 开始的整数数列。不过,我们可以提供 start 和 step 值,这样实现的作用与 aritprog_gen 函数相似

In [18]:
import itertools
gen = itertools.count(1, .5)
next(gen)
Out[18]:
1
In [19]:
next(gen)
Out[19]:
1.5
In [20]:
next(gen)
Out[20]:
2.0
In [21]:
next(gen)
Out[21]:
2.5

然而 itertools.count 函数从不停止,因此,调用 list(count())) 会产生一个特别大的列表,超出可用的内存

不过,itertools.takewhile 函数不同,他会生成一个使用另一个生成器的生成器,在指定条件计算结果为 False 时候停止,因此,可以把这两个函数结合:

In [24]:
gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
list(gen)
Out[24]:
[1, 1.5, 2.0, 2.5]

所以,我们可以将等差数列写成这样:

In [26]:
import itertools

def aritprog_gen(begin, step, end=None):
    first = type(begin+step)(begin)
    ap_gen = itertools.count(first, step)
    if end is not None:
        ap_gen = itertools.takewhile(lambda n: n < end, ap_gen)
    return ap_gen

注意, aritprog_gen 不是生成器函数,因为没有 yield 关键字,但是会返回一个生成器,因此它和其他的生成器函数一样,是一个生成器工厂函数

标准库中的生成器函数

标准库中有很多生成器,有用于逐行迭代文本文件的对象,还有出色的 os.walk 函数,不过本节专注于通用的函数:参数为任意可迭代对象,返回值是生成器,用于生成选中的,计算出的和重新排列的元素。

第一组是过滤生成器函数,如下:

In [29]:
def vowel(c):
    return c.lower() in 'aeiou'

# 字符串各个元素传给 vowel 函数,为真则返回对应元素
list(filter(vowel, 'Aardvark'))
Out[29]:
['A', 'a', 'a']
In [32]:
import itertools
# 与上面相反
list(itertools.filterfalse(vowel, 'Aardvark'))
Out[32]:
['r', 'd', 'v', 'r', 'k']
In [33]:
# 处理 字符串,跳过 vowel 为真的元素,然后产出剩余的元素,不再检查
list(itertools.dropwhile(vowel, 'Aardvark'))
Out[33]:
['r', 'd', 'v', 'a', 'r', 'k']
In [35]:
#返回真值对应的元素,立即停止,不再检查
list(itertools.takewhile(vowel, 'Aardvark')) 
Out[35]:
['A', 'a']
In [37]:
# 并行处理两个迭代对象,如果第二个是真值,则返回第一个
list(itertools.compress('Aardvark', (1, 0, 1, 1, 0, 1)))
Out[37]:
['A', 'r', 'd', 'a']
In [38]:
list(itertools.islice('Aardvark', 4))
Out[38]:
['A', 'a', 'r', 'd']
In [39]:
list(itertools.islice('Aardvark', 4, 7))
Out[39]:
['v', 'a', 'r']
In [40]:
list(itertools.islice('Aardvark', 1, 7, 2))
Out[40]:
['a', 'd', 'a']

下面是映射生成器函数:

In [43]:
sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
import itertools
# 产出累计的总和
list(itertools.accumulate(sample))
Out[43]:
[5, 9, 11, 19, 26, 32, 35, 35, 44, 45]
In [45]:
# 如果提供了函数,那么把前两个元素给他,然后把计算结果和下一个元素给它,以此类推
list(itertools.accumulate(sample, min))
Out[45]:
[5, 4, 2, 2, 2, 2, 2, 0, 0, 0]
In [46]:
list(itertools.accumulate(sample, max))
Out[46]:
[5, 5, 5, 8, 8, 8, 8, 8, 9, 9]
In [47]:
import operator
list(itertools.accumulate(sample, operator.mul)) # 计算乘积
Out[47]:
[5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0]
In [48]:
list(itertools.accumulate(range(1, 11), operator.mul))
Out[48]:
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
In [49]:
list(enumerate('albatroz', 1)) #从 1 开始,为字母编号
Out[49]:
[(1, 'a'),
 (2, 'l'),
 (3, 'b'),
 (4, 'a'),
 (5, 't'),
 (6, 'r'),
 (7, 'o'),
 (8, 'z')]
In [50]:
import operator
list(map(operator.mul, range(11), range(11)))
Out[50]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [52]:
# 计算两个可迭代对象中对应位置的两个之和,元素最少的迭代完毕就停止
list(map(operator.mul, range(11), [2, 4, 8]))
Out[52]:
[0, 4, 16]
In [57]:
list(map(lambda a, b: (a, b), range(11), [2, 4, 8]))
Out[57]:
[(0, 2), (1, 4), (2, 8)]
In [58]:
import itertools
# starmap 把第二个参数的每个元素传给第一个函数 func,产出结果,
# 输入的可迭代对象应该产出可迭代对象 iit,
# 然后以(func(*iit) 这种形式调用 func)
list(itertools.starmap(operator.mul, enumerate('albatroz', 1)))
Out[58]:
['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']
In [59]:
sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
# 计算平均值
list(itertools.starmap(lambda a, b: b / a, 
                       enumerate(itertools.accumulate(sample), 1)))
Out[59]:
[5.0,
 4.5,
 3.6666666666666665,
 4.75,
 5.2,
 5.333333333333333,
 5.0,
 4.375,
 4.888888888888889,
 4.5]

接下来是用于合并的生成器函数:

In [63]:
# 先产生第一个元素,然后产生第二个参数的所有元素,以此类推,无缝连接到一起
list(itertools.chain('ABC', range(2)))
Out[63]:
['A', 'B', 'C', 0, 1]
In [64]:
list(itertools.chain(enumerate('ABC')))
Out[64]:
[(0, 'A'), (1, 'B'), (2, 'C')]
In [66]:
# chain.from_iterable 函数从可迭代对象中获取每个元素,
# 然后按顺序把元素连接起来,前提是各个元素本身也是可迭代对象
list(itertools.chain.from_iterable(enumerate('ABC')))
Out[66]:
[0, 'A', 1, 'B', 2, 'C']
In [67]:
list(zip('ABC', range(5), [10, 20, 30, 40])) #只要有一个生成器到头,就停止
Out[67]:
[('A', 0, 10), ('B', 1, 20), ('C', 2, 30)]
In [68]:
# 处理到最长的迭代器到头,短的会填充 None
list(itertools.zip_longest('ABC', range(5)))
Out[68]:
[('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
In [70]:
list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # 填充问号
Out[70]:
[('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]

itertools.product 生成器是计算笛卡尔积的惰性方式,从输入的各个迭代对象中获取元素,合并成由 N 个元素构成的元组,与嵌套的 for 循环效果一样。repeat指明重复处理多少次可迭代对象。下面演示 itertools.product 的用法

In [79]:
list(itertools.product('ABC', range(2)))
Out[79]:
[('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]
In [80]:
suits = 'spades hearts diamonds clubs'.split()
list(itertools.product('AK', suits))
Out[80]:
[('A', 'spades'),
 ('A', 'hearts'),
 ('A', 'diamonds'),
 ('A', 'clubs'),
 ('K', 'spades'),
 ('K', 'hearts'),
 ('K', 'diamonds'),
 ('K', 'clubs')]
In [81]:
# 传入一个可迭代对象,产生一系列只有一个元素的元祖,不是特别有用
list(itertools.product('ABC'))
Out[81]:
[('A',), ('B',), ('C',)]
In [82]:
# repeat = N 重复 N 次处理各个可迭代对象
list(itertools.product('ABC', repeat=2))
Out[82]:
[('A', 'A'),
 ('A', 'B'),
 ('A', 'C'),
 ('B', 'A'),
 ('B', 'B'),
 ('B', 'C'),
 ('C', 'A'),
 ('C', 'B'),
 ('C', 'C')]
In [83]:
list(itertools.product(range(2), repeat=3))
Out[83]:
[(0, 0, 0),
 (0, 0, 1),
 (0, 1, 0),
 (0, 1, 1),
 (1, 0, 0),
 (1, 0, 1),
 (1, 1, 0),
 (1, 1, 1)]
In [86]:
rows = itertools.product('AB', range(2), repeat=2)
for row in rows: print(row)
('A', 0, 'A', 0)
('A', 0, 'A', 1)
('A', 0, 'B', 0)
('A', 0, 'B', 1)
('A', 1, 'A', 0)
('A', 1, 'A', 1)
('A', 1, 'B', 0)
('A', 1, 'B', 1)
('B', 0, 'A', 0)
('B', 0, 'A', 1)
('B', 0, 'B', 0)
('B', 0, 'B', 1)
('B', 1, 'A', 0)
('B', 1, 'A', 1)
('B', 1, 'B', 0)
('B', 1, 'B', 1)

把输入的各个元素扩展成多个输出元素的生成器函数:

In [88]:
ct = itertools.count()
next(ct) # 不能构建 ct 列表,因为 ct 是无穷的
Out[88]:
0
In [89]:
next(ct), next(ct), next(ct)
Out[89]:
(1, 2, 3)
In [90]:
list(itertools.islice(itertools.count(1, .3), 3))
Out[90]:
[1, 1.3, 1.6]
In [91]:
cy = itertools.cycle('ABC')
next(cy)
Out[91]:
'A'
In [92]:
list(itertools.islice(cy, 7))
Out[92]:
['B', 'C', 'A', 'B', 'C', 'A', 'B']
In [93]:
rp = itertools.repeat(7) # 重复出现指定元素
next(rp), next(rp)
Out[93]:
(7, 7)
In [94]:
list(itertools.repeat(8, 4)) # 4 次数字 8
Out[94]:
[8, 8, 8, 8]
In [95]:
list(map(operator.mul, range(11), itertools.repeat(5)))
Out[95]:
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

itertools 中 combinations, comb 和 permutations 生成器函数,连同 product 函数称为组合生成器。itertool.product 和其余组合学函数有紧密关系,如下:

In [96]:
# 'ABC' 中每两个元素 len() == 2 的各种组合
list(itertools.combinations('ABC', 2))
Out[96]:
[('A', 'B'), ('A', 'C'), ('B', 'C')]
In [97]:
# 包括相同元素的每两个元素的各种组合
list(itertools.combinations_with_replacement('ABC', 2))
Out[97]:
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
In [98]:
# 每两个元素的各种排列
list(itertools.permutations('ABC', 2))
Out[98]:
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
In [99]:
list(itertools.product('ABC', repeat=2))
Out[99]:
[('A', 'A'),
 ('A', 'B'),
 ('A', 'C'),
 ('B', 'A'),
 ('B', 'B'),
 ('B', 'C'),
 ('C', 'A'),
 ('C', 'B'),
 ('C', 'C')]

用于重新排列元素的生成器函数:

In [101]:
# 产出由两个元素组成的元素,形式为 (key, group),其中 key 是分组标准,
#group 是生成器,用于产出分组里的元素
list(itertools.groupby('LLLAAGGG'))
Out[101]:
[('L', <itertools._grouper at 0x7f57144b7cc0>),
 ('A', <itertools._grouper at 0x7f57143595c0>),
 ('G', <itertools._grouper at 0x7f5714359128>)]
In [102]:
for char, group in itertools.groupby('LLLLAAAGG'):
    print(char, '->', list(group))
L -> ['L', 'L', 'L', 'L']
A -> ['A', 'A', 'A']
G -> ['G', 'G']
In [104]:
animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear',
           'bat', 'dolphin', 'shark', 'lion']
animals.sort(key=len)
animals
Out[104]:
['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin']
In [105]:
for length, group in itertools.groupby(animals, len):
    print(length, '->', list(group))
3 -> ['rat', 'bat']
4 -> ['duck', 'bear', 'lion']
5 -> ['eagle', 'shark']
7 -> ['giraffe', 'dolphin']
In [106]:
# 使用 reverse 生成器从右往左迭代 animals
for length, group in itertools.groupby(reversed(animals), len):
    print(length, '->', list(group))
7 -> ['dolphin', 'giraffe']
5 -> ['shark', 'eagle']
4 -> ['lion', 'bear', 'duck']
3 -> ['bat', 'rat']
In [107]:
# itertools 产生多个生成器,每个生成器都产出输入的各个元素
list(itertools.tee('abc'))
Out[107]:
[<itertools._tee at 0x7f5714502088>, <itertools._tee at 0x7f5714502448>]
In [108]:
g1, g2 = itertools.tee('abc')
next(g1)
Out[108]:
'a'
In [109]:
next(g2)
Out[109]:
'a'
In [110]:
next(g2)
Out[110]:
'b'
In [111]:
list(g1)
Out[111]:
['b', 'c']
In [112]:
list(g2)
Out[112]:
['c']
In [113]:
list(zip(*itertools.tee('ABC')))
Out[113]:
[('A', 'A'), ('B', 'B'), ('C', 'C')]

Python 3.3 中新语法 yield from

如果生成器函数需要产生两一个生成器生成的值,传统方法是使用 for 循环

In [115]:
def chain(*iterables): # 自己写的 chain 函数,标准库中的 chain 是用 C 写的
    for it in iterables:
        for i in it:
            yield i
            
s = 'ABC'
t = tuple(range(3))
list(chain(s, t))
Out[115]:
['A', 'B', 'C', 0, 1, 2]

chain 生成器函数把操作依次交给接收到的各个可迭代对象处理。为此 Python 3.3 引入了新语法,如下:

In [116]:
def chain(*iterables):
    for i in iterables:
        yield from i  # 详细语法在 16 章讲
        
list(chain(s, t))
Out[116]:
['A', 'B', 'C', 0, 1, 2]

可迭代的归约函数

接受可迭代对象,然后返回单个结果,叫归约函数。

In [118]:
all([1, 2, 3]) # 所有元素为真返回 True
Out[118]:
True
In [119]:
all([1, 0, 3])
Out[119]:
False
In [120]:
any([1, 2, 3]) # 有元素为真就返回 True
Out[120]:
True
In [121]:
any([1, 0, 3])
Out[121]:
True
In [122]:
any([0, 0, 0])
Out[122]:
False
In [123]:
any([])
Out[123]:
False
In [129]:
g = (n for n in [0, 0.0, 7, 8])
any(g) 
Out[129]:
True
In [130]:
next(g) # any 碰到一个为真就不往下判断了
Out[130]:
8

还有一个内置的函数接受一个可迭代对象,返回不同的值 -- sorted,reversed 是生成器函数,与此不同,sorted 会构建并返回真正的列表,毕竟要读取每一个元素才能排序。它返回的是一个排好序的列表。这里提到 sorted,是因为它可以处理任何可迭代对象

当然,sorted 和这些归约函数只能处理最终会停止的可迭代对象,这些函数会一直收集元素,永远无法返回结果

深入分析 iter 函数

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

下面是掷骰子,直到掷出 1

In [139]:
from random import randint

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

d6_iter = iter(d6, 1)
d6_iter
Out[139]:
<callable_iterator at 0x7f571457cbe0>
In [140]:
for roll in d6_iter:
    print(roll)
6
4
6
4
4
5
3
3
2
2
6
5
6
5
2
2
4
2
2
2
3
6
2
4

内置函数 iter 的文档有一个实用的例子,逐行读取文件,直到遇到空行或者到达文件末尾为止:

In [143]:
# for line in iter(fp.readline, '\n'):
#     process_line(line)

把生成器当成协程

Python 2.2 引入了 yield 关键字实现的生成器函数,Python 2.5 为生成器对象添加了额外的方法和功能,其中最引人关注的是 .send() 方法

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

这是一项重要的 “改进”,甚至改变了生成器本性,这样使用的话,生成器就变成了协程。所以要提醒一下:

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

延伸阅读

有个简单的生成器函数例子

In [147]:
def f(): 
    x=0
    while True:
        x += 1
        yield x

我们无法通过函数调用抽象产出这个过程,下面似乎能抽象产出这个过程:

In [153]:
def f():
    def do_yield(n):
        yield n
    x = 0
    while True:
        x += 1
        do_yield(x)

调用 f() 会得到一个死循环,而不是生成器,因为 yield 只能将最近的外层函数变成生成器函数。虽然生成器函数看起来像函数,可是我们不能通过简单的函数调用把职责委托给另一个生成器函数。

Python 新引入的 yield from 语法允许生成器或协程把工作委托给第三方完成,这样就无需嵌套 for 循环作为变通了。在函数调用前面加上 yield from 能 ”解决“ 上面的问题,如下:

In [155]:
def f():
    def do_yield(n):
        yield n
    x = 0
    while True:
        x += 1
        yield from do_yield(x)