流畅的Python 可迭代的对象、迭代器和生成器

典型的迭代器

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
37
38
39
import re
import reprlib

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


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

def __next__(self):
try:
word = self.words[self.index]
except IndexError as e:
raise StopIteration
self.index += 1
return word

def __iter__(self):
return self


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

def __repr__(self):
return f"{type(self).__name__}:{reprlib.repr(self.text)}"

def __iter__(self): # 明确表明这个类可以迭代
# 根据可迭代协议,__iter__ 方法实例化并返回一个迭代器。
return SentenceIterator(self.word)


# 在SentenceIterator中实现 __iter__ 可以让能让迭代器通过以下测试:
from collections import abc
print(issubclass(SentenceIterator, abc.Iterator))

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

除了 __iter__ 方法之外,有人可能还想在Sentence类中实现 __next__ 方法,让Sentence实例既是可迭代的对象,也是自身的迭代器。可是,这种想法非常糟糕的(原因不详述)。

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


生成器函数

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

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

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

def __repr__(self):
return f"{type(self).__name__}:{reprlib.repr(self.text)}"

def __iter__(self): # 生成器函数
for word in self.word:
yield word

生成器函数的工作原理:

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

使用准确的词语描述从生成器中获取结果的过程,有助于理解生成器。注意,产出生成值。如果说生成器“返回”值,就会让人难以理解。函数返回值;调用生成器函数返回生成器;生成器产出或生成值。生成器不会以常规的方式“返回”值:生成器函数定义体中的 return 语句会触发生成器对象抛出 StopIteration 异常。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def gen_AB():
print("start")
yield "A"
print("continue")
yield "B"
print("end")

for i in gen_AB(): # for机制会捕获异常
print("--> ",i)

# start
# --> A
# continue
# --> B
# end

Sentence类升级版:惰性实现

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

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

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

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

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

def __repr__(self):
return f"{type(self).__name__}:{reprlib.repr(self.text)}"

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

Sentence类终极版:生成器表达式

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

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

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

def __repr__(self):
return f"{type(self).__name__}:{reprlib.repr(self.text)}"

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

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

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

遇到简单的情况时,可以使用生成器表达式;如果生成器表达式要分成多行写,倾向于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用。