Skip to content

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

此笔记记录于《流畅的python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为jupyter转的markdown,原始版jupyter笔记在这个仓库

当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深——这通常意味着自己正在手动完成的事情,本应该通过写代码来让宏的扩展自动实现。

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

所有生成器都是迭代器,因为生成器完全实现了迭代器接口。不过,根据《设计模式:可复用面向对象软件的基础》一书的定义,迭代器用于从集合中取出元素;而生成器用于“凭空”生成元素。

Sentence类第1版:单词序列

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 __getitem__(self, index):
        return self.words[index]

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

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)
python
s = Sentence('`The time has come, ` the Walrus said,')
s
Sentence('`The time ha... Walrus said,')
python
for word in s:
    print(word)
The
time
has
come
the
Walrus
said
python
list(s)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']

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

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

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

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

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

python
class Foo:
    def __iter__(self):
        pass
python
from collections import abc

issubclass(Foo, abc.Iterable)
True
python
f = Foo()
isinstance(f, abc.Iterable)
True

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

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

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

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

python
# 字符串'ABC'是可迭代的对象。背后是有迭代器的,只不过我们看不到
s = 'ABC'
for char in s:
    print(char)
A
B
C
python
s = 'ABC'
it = iter(s) # 使用可迭代的对象构建迭代器it
while True:
    try:
        print(next(it)) # 不断在迭代器上调用next函数,获取下一个字符
    except StopIteration:
        del it
        break
A
B
C

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

  • __next__: 返回下一个可用的元素,如果没有了,抛出StopIteration异常
  • __iter__: 返回self,以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中
python
# abc.Iterator类,摘自Lib/_collections_abc.py
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__方法,返回序列中的下一个元素;如果没有元素了,那么抛出StopIteration异常。Python中的迭代器还实现了__iter__方法,因此迭代器也可以迭代。

因为内置的iter(...)函数会对序列做特殊处理,所以第1版Sentence类可以迭代。

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

使用迭代器模式实现Sentence类

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):
        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

与前一版相比,这里只多了一个__iter__方法。这一版没有__getitem__方法,为的是明确表明这个类可以迭代,因为实现了__iter__方法。

把Sentence变成迭代器:坏主意

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

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

所以这里并没有直接在Sentence类上实现迭代器模式,而是在迭代器类SentenceIterator中实现了迭代器模式。

Sentence类第3版:生成器函数

实现相同功能,但却符合Python习惯的方式是,用生成器函数代替SentenceIterator类。

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 # 这个return语句不是必要的;这个函数可以直接“落空”,自动返回。不管有没有return语句,生成器函数都不会抛出StopIteration异常,而是在生成完全部值之后会直接退出
# 完成!➍

Sentence类第4版:惰性实现

只要使用的是Python 3,思索着做某件事有没有懒惰的方式,答案通常都是肯定的。

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

我们要使用这个函数让第4版Sentence类变得懒惰,即只在需要时才生成下一个单词。

python
import re
import reprlib
RE_WORD = re.compile('\w+')


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

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

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

Sentence类第5版:生成器表达式

python
def gen_AB():
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end.')
    
res2 = (x*3 for x in gen_AB())
res2 # 这种语法是可以产生生成器的,因此可以使用生成器表达式进一步减少Sentence类的代码
<generator object <genexpr> at 0x0000013742024D60>
python
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))

# 语法糖,可以不使用yield了

itertools模块提供了19个生成器函数,结合起来使用能实现很多有趣的用法。

python
import itertools
gen = itertools.count(1, .5)
python
next(gen)
1
python
next(gen)
1.5
python
next(gen)
2.0
python
next(gen)
2.5

标准库中的生成器函数

函数名描述
enumerate枚举,返回一个枚举对象。其__next__()方法返回一个元组,包含一个计数(从start开始)和通过迭代iterable得到的值。
iter返回一个迭代器对象。
next返回迭代器的下一个项目。
filter构造一个迭代器,从iterable中过滤出一些元素,其元素使得function返回true。
map返回一个迭代器,该迭代器通过对iterable中的每个元素应用function函数产生结果。
range虽然在Python 3.x中不是一个生成器函数,但range产生的是一个惰性序列,而不是列表。
zip使得多个iterables可以并行迭代,返回一个元组的迭代器。
reversed返回一个反向的迭代器。
sorted返回一个根据iterable中的项目排序的新列表,不是生成器函数,但生成的结果是迭代的。

Python 3.3中新出现的句法:yield from

如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的for循环。

python
def chain(*iterables):
    for it in iterables:
        for i in it:
            yield i
python
# 简化
def chain(*iterables):
    for i in iterables:
        yield from i

可迭代的归约函数

函数名描述
all如果iterable的所有元素都为真(或iterable为空),返回True。
any如果iterable中有任何元素为真,返回True。
sum对iterable中的项求和并返回总和。
max返回iterable中的最大值,或者两个及以上参数中的最大值。
min返回iterable中的最小值,或者两个及以上参数中的最小值。
reduce对iterable的元素累积应用两参数函数,从左到右,以减少iterable到单一值。

深入分析iter函数

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

python
from random import randint


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

d6_iter = iter(d6, 1)
d6_iter
<callable_iterator at 0x1374201b2e0>
python
for roll in d6_iter:
    print(roll)
4
2
4
3
python