Python中的“迭代”详解

迭代器模式:一种惰性获取数据项的方式,即按需一次获取一个数据项。

关于迭代器和生成器的区别:

在Python社区中,大多数时候把迭代器和生成器视为同一概念。

如果需要细分的话,那么迭代器用于从集合中取出数据,而生成器用于凭空生成元素。

一、序列

所有序列都是可以迭代的。我们接下来要实现一个 Sentence(句子)类,我们向这个类的构造方法传入包含一些文本的字符串,然后可以逐个单词迭代。

sentence.py

import re
import reprlib

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


class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)  # re.findall 函数返回一个字符串列表

    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)  # reprlib.repr 函数用于生成大型数据结构的简略字符串表示形式

接下来测试 Sentence 实例能否迭代

In [11]: s = sentence.Sentence("'winter is coming!', he said.")

In [12]: s
Out[12]: Sentence("'winter is c...g!', he said.")

In [13]: for word in s:
    ...:     print(word)
    ...:     
winter
is
coming
he
said

In [14]: list(s)
Out[14]: ['winter', 'is', 'coming', 'he', 'said']

序列可以迭代的原因:

iter()

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

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

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

由于序列都实现了__getitem__方法,所以都可以迭代。

从 Python3.4 开始,检查对象 x 能否迭代,最准确的方式是调用 iter(x),如果不可迭代,再处理 TypeError 异常,这比使用 isistance(x, abc.Iterable)更准确,因为iter(x)函数会考虑__getitem__方法。

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

可迭代对象:使用内置函数 iter() 可以获取迭代器的对象。

与迭代器的关系:Python 从可迭代对象中获取迭代器。

下面用for循环迭代一个字符串,这里字符串 'abc' 是可迭代的对象,用 for 循环迭代时是有生成器,只是 Python 隐藏了。

In [19]: s = 'abc'

In [20]: for char in s:
    ...:     print(char)
    ...:     
    ...:     
a
b
c

如果没有 for 语句,使用 while 循环模拟,要写成下面这样:

In [21]: s = 'abc'

In [22]: it = iter(s)  # 使用可迭代对象构建迭代器

In [23]: while True:
    ...:     try:
    ...:         print(next(it)) # 在迭代器上不断调用 next() 获取下一个字符
    ...:     except StopIteration: # 如果没有字符了,迭代器会抛出 StopIteration 异常
    ...:         del it # 释放对 it 的引用,即废弃迭代器对象
    ...:         break
    ...:     
a
b
c

Python 内部会处理 for 循环和其他迭代上下文(如列表推导,元组拆包等等)中的 StopIteration 异常。

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

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

__iter__:返回 self,以便在需要使用可迭代对象的地方使用迭代器,如 for 循环中。

迭代器:实现了无参数的__next__方法,返回序列中的下一个元素;如果没有元素了,那么抛出 StopIteration 异常。Python 中的迭代器还实现了__iter__方法,因此迭代器也可以迭代。

接下来使用迭代器模式实现 Sentence 类:

sentence_iter.py

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

    # 实现 __next__ 返回单个元素
    def __next__(self):
        try:
            word = self.words[self.index]
        except IndexError:
            raise StopIteration()
        self.index += 1
        return word
    
    # 实现 __iter__ 返回自身
    def __iter__(self):
        return self

注意,不要在 Sentence 类中实现 __next__ 方法,让 Sentence 实例既是可迭代对象,也是自身的迭代器。

迭代器模式可用来:

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

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

所以总结下来就是:

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

三、生成器

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

sentence_gen.py

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)
    
    # 是一个生成器函数,调用时会构建一个实现了迭代器接口的生成器对象,因此不用再定义 SentenceIterator类了。
    def __iter__(self):
        for word in self.words:
            yield word

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

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

注意,我说的是产出或生成值。调用生成器函数返回生成器,生成器产出或生成值。生成器不会以常规的方式“返回”值,生成器函数定义体中的 return 语句会触发生成器对象抛出 StopIteration 异常。(Python3.3 之前有 return 语句会报错)

关于 for 循环:

for 循环会隐式的调用 next(),而且在使用 for 循环到生成器函数定义体的末尾时,for 循环会捕获异常,因此循环终止时没有报错。

如今这一版 Sentence 类相较之前简短多了,但是还不够慵懒。惰性,是如今人们认为最好的特质。惰性实现是指尽可能延后生成值,这样做能节省内存,或许还能避免做无用的处理。

四、惰性实现

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

re.finditer 函数是 re.findall函数的惰性版本,返回的是一个生成器,按需生成 re.MatchObject 实例。我们可以使用这个函数来让 Sentence 类变得懒惰,即只在需要时才生成下一个单词。

sentence_gen2.py


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() # match.group()方法从 MatchObject 实例中提取匹配正则表达式的具体文本。

五、标准库中的生成器函数

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

第一组是用于过滤的生成器函数:从输入的可迭代对象中产出元素的子集,而且不修改元素本身。这种函数大多数都接受一个断言参数(predicate),这个参数是个布尔函数,有一个参数,会应用到输入中的每个元素上,用于判断元素是否包含在输出中。

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

以下为这些函数的演示:

>>> def vowel(c):
...     return c.lower() in 'aeiou'
... 
>>> list(filter(vowel, 'Aardvark'))
['A', 'a', 'a']
>>> import itertools
>>> list(itertools.filterfalse(vowel, 'Aardvark'))
['r', 'd', 'v', 'r', 'k']
>>> list(itertools.dropwhile(vowel, 'Aardvark'))
['r', 'd', 'v', 'a', 'r', 'k'] # 不再进一步检查,即相同真值只跳过一次
>>> list(itertools.takewhile(vowel, 'Aardvark'))
['A', 'a'] # 同上,不再进一步检查
>>> list(itertools.compress('Aardvark', (1, 0, 1, 1, 0, 1)))
['A', 'r', 'd', 'a'] # selector_it 是(1, 0, 1, 1, 0, 1),所以'Aardvark'中位置对应1的生成出来了。
>>> list(itertools.islice('Aardvark', 4))
['A', 'a', 'r', 'd']
>>> list(itertools.islice('Aardvark', 4, 7))
['v', 'a', 'r']
>>> list(itertools.islice('Aardvark', 1, 7, 2))
['a', 'd', 'a']

第二组是用于映射的生成器函数:在输入的单个/多个可迭代对象中的各个元素上做计算,然后返回结果。

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

以下为这些函数的用法:

>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> import itertools
>>> list(itertools.accumulate(sample))
[5, 9, 11, 19, 26, 32, 35, 35, 44, 45]
>>> import operator
>>> list(itertools.accumulate(range(1, 11), operator.mul)) # 1! 到 10!的阶乘
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] 
>>> list(map(operator.mul, range(11), range(11)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> list(map(operator.mul, range(11), [2, 4, 8]))
[0, 4, 16]
>>> list(itertools.starmap(operator.mul, enumerate('albatroz', 1))) # 从 1 开始,根据字母所在位置,把字母重复相应的次数
['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']

第三组是用于合并的生成器函数,这些函数都可以从输入的多个可迭代对象中产出元素。

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

以下为演示:

>>> list(itertools.chain('ABC', range(2)))
['A', 'B', 'C', 0, 1]
>>> list(itertools.chain.from_iterable(enumerate('ABC')))
[0, 'A', 1, 'B', 2, 'C']
>>> list(zip('ABC', range(5)))
[('A', 0), ('B', 1), ('C', 2)]
>>> list(itertools.zip_longest('ABC', range(5), fillvalue='?'))
[('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
>>> list(itertools.product('ABC', range(2)))
[('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]

第四组是从一个元素中产出多个值,扩展输入的可迭代对象。

模块 函数 说明
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_len 个 it 产出的元素排列在一起,然后产出这些排列,out_len 的默认值等于 len(list(it))
itertools repeat(item, [times]) 重复不断的产出指定的元素,除非提供 times,指定次数

以下为演示:

>>> ct = itertools.count()
>>> next(ct), next(ct), next(ct)
(0, 1, 2)
>>> list(itertools.islice(cy, 7))
['A', 'B', 'C', 'A', 'B', 'C', 'A']
>>> list(itertools.repeat(7, 4))
[7, 7, 7, 7]
>>> list(map(operator.mul, range(11), itertools.repeat(5))) # repeat 常见用途:为 map 函数提供固定参数
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
>>> list(itertools.combinations('ABC', 2)) # 'ABC'中每两个元素的各种组合,元素顺序无意义,不含相同元素的组合
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(itertools.combinations_with_replacement('ABC', 2)) # 'ABC'中每两个元素的各种组合,元素顺序无意义,含相同元素的组合
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
>>> list(itertools.permutations('ABC',2)) # 'ABC'中每两个元素的各种排列,元素的顺序有意义
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

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

模块 函数 说明
itertools groupby(it, key=None) 产出由两个元素组成的元素,形式为(key, group),其中 key 是分组标准,group 是生成器,用于产出分组里的元素
内置 reversed(seq) 从后向前,倒序产出seq中的元素,seq必须是序列,或者是实现了 __reversed__特殊方法的对象
itertools tee(it, n=2) 产出一个由 n 个生成器组成的元组,每个生成器用于单独产出输入的可迭代对象中的元素
>>> import itertools
>>> list(itertools.groupby('LLLAAAXXXX'))
[('L', <itertools._grouper object at 0x7f467ea946a0>), ('A', <itertools._grouper object at 0x7f467ea946d8>), ('X', <itertools._grouper object at 0x7f467ea94710>)]
>>> for char, group in itertools.groupby('LLLAAAXXXX'):
...     print(char, '->', list(group))
... 
L -> ['L', 'L', 'L']
A -> ['A', 'A', 'A']
X -> ['X', 'X', 'X', 'X']
>>> words = ['x','ss','zzz','dsas','xs']
>>> words.sort(key=len) # 使用 groupby 时需要先进行排序或进行分组
>>> words
['x', 'ss', 'xs', 'zzz', 'dsas']
>>> for length, group in itertools.groupby(words, len):
...     print(length, '->', list(group))
... 
1 -> ['x']
2 -> ['ss', 'xs']
3 -> ['zzz']
4 -> ['dsas']

>>> g1, g2 = itertools.tee('ABC')
>>> next(g1)
'A'
>>> next(g1)
'B'
>>> next(g2)
'A'
>>> list(g2)
['B', 'C']

六、可迭代的归约函数

下面的函数都接受一个可迭代的对象,然后返回单个结果,这种函数叫“归约函数”,“合拢函数”或“累加函数”,其实,这些内置函数都可以用 functools.reduce 函数实现,但内置更加方便,而且还有一些优点。

模块 函数 说明
内置 all(it) it 中的所有元素都为真值时返回 True,否则返回 False;all([]) 返回 True
内置 any(it) 只要 it 中有元素为真值就返回 True,否则返回 False;any([]) 返回 False
内置 max(it, [key=], [default=]) 返回 it 中值最大的元素,key 是排序函数,与 sorted 函数中一样;如果可迭代的对象为空,返回 default
内置 min(it, [key=], [default=]) 返回 it 中值最小的元素,key 是排序函数,与 sorted 函数中一样;如果可迭代的对象为空,返回 default
functools reduce(func, it, [initial]) 把前两个元素传给 func,然后把计算结果和第三个元素传给 func,以此类推,返回最后的结果,如果提供了 initial ,把它当做第一个元素传入。
内置 sum(it, start=0) it 中所有元素的总和,如果提供可选的 start,会把它加上(计算浮点数的加法时,可以使用 math.fsum 函数提高精度

参考教程:
《流畅的python》 P330 - 363

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343