python生成器详解

前言

  • 作为python程序员,生成器以及协程是必不可少的话题。你可能在面试中会经常遇到这样的问题:说一说生成器和迭代器的区别?使用了哪些异步插件?讲一讲asyncio的用法以及原理?等等。当然,能回答出这些问题只是初级目标,重要的是,我们是否深入掌握了这些内容,是否在实际中能够找到合适的方法处理异步问题。我依照《fluent pyhton》中的经典例子,结合我自己的理解,由浅入深讲解。本来只打算写一篇文章的,但是写着写着发现内容过多,只好拆分出来。

iter(...) 函数如何把序列变得可以迭代

import re
import reprlib

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


class Sentence:

    def __init__(self, text):
        self.text = text
        #  re.findall 函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配。 
        self.words = RE_WORD.findall(text)

    def __getitem__(self, item):
        return self.words[item]

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

    def __repr__(self):
        #  reprlib.repr 这个实用函数用于生成大型数据结构的简略字符串表示形式
        #  默认情况下,reprlib.repr 函数生成的字符串最多有 30 个字符
        return "Sentence({})".format(reprlib.repr(self.text))

Sentence 实例测试

if __name__ == '__main__':
    s = Sentence('"The time has come," the Walrus said,')
    print(s)
    for word in s:
        print(word)
    print(list(s))

output

Sentence('"The time ha... Walrus said,')
The
time
has
come
the
Walrus
said
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
  • 通过测试说明Sentence实例可迭代,实现了序列协议,但是为什么可迭代呢?
序列可以迭代的原因:iter函数
  • 解释器需要迭代对象x时,会自动调用iter(x)。
  • 内置的 iter 函数有以下作用
    • 1.检查对象是否实现了 __iter__ 方法,如果实现了就调用它,获取一个迭代器。
    • 2.如果没有实现 __iter__ 方法,但是实现了 __getitem__ 方法,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。
    • 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“x object is not iterable”
可迭代的对象与迭代器的对比
  • 使用 iter 内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的 __iter__ 方法,那么对象就是可迭代的。
  • 所以任何 Python 序列都可迭代的原因是,它们都实现了 __getitem__ 方法,标准的序列也都实现了 __iter__ 方法。
  • 从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法是:调用 iter(x) 函数,如果不可迭代,再处理 TypeError 异常
  • 标准的迭代器接口有两个方法
    • __next__:返回下一个可用的元素,如果没有元素了,抛出 StopIteration异常。
    • __iter__:返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中。
  • 下面使用前面的Sentence类来说明如何使用 iter(...) 函数构建迭代器,以及如何使用 next(...)函数使用迭代器
if __name__ == '__main__':
    s = Sentence('hello world')
    it = iter(s)  # 构建迭代器
    print(it)
    print(next(it))
    print(next(it))
    print(next(it))
  • output
<iterator object at 0x0BC82230>
hello
world
Traceback (most recent call last):
  File "xxx.py", line 33, in <module>
    print(next(it))
StopIteration
  • 可知next方法会不断拿出迭代器中的元素,如果没有元素,返回StopIteration异常。如果使用next迭代完成后想再次迭代,必须重新构建迭代器,因为next会拿出迭代器中的元素而不放回:
if __name__ == '__main__':
    s = Sentence('hello world')
    it = iter(s)  # 构建迭代器  
    print(it)  # <iterator object at 0x0C242230>
    print(next(it))  # hello
    print(next(it))  # world
    # print(next(it))
    print(list(it))  # []
    print(list(iter(s)))  # 重新构建迭代器生成的列表并打印  #  ['hello', 'world']
  • 根据以上可以总结迭代器的定义:实现了无参数的 __next__ 方法,返回序列中的下一个元素;如果没有元素了,那么抛出 StopIteration 异常。Python 中的迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。
  • 下面根据Sentence类来实现标准的迭代器:
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 __iter__(self):
        return SentenceIterator(self.words)  # 返回迭代器

    def __repr__(self):
        return "Sentence({})".format(reprlib.repr(self.text))


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__ 两个方法。可迭代的对象有个 __iter__ 方法,每次都实例化一个新的迭代器;而迭代器要实现 __next__ 方法,返回单个元素,此外还要实现 __iter__ 方法,返回迭代器本身。
  • 因此,迭代器可以迭代,但是可迭代的对象不是迭代器。
  • 各个迭代器要能维护自身的内部状态, 每次调用 iter(my_iterable) 都新建一个独立的迭代器。这就是为什么这个示例需要定义 SentenceIterator 类。由此,可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现 __iter__ 方法,但不能实现 __next__ 方法。
  • 你可能会想,难道我们定义一个可迭代的对象必须还得新增个迭代器对象吗,也就是说,我想要去掉SentenceIterator迭代器类,有没有更好的实现方式?这样,生成器就出来了。下面改写__iter__方法,使用生成器如下:
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 __iter__(self):
        """生成器函数"""
        for word in self.words:
            yield word  # yield 可以简单理解为return
        return

    def __repr__(self):
        return "Sentence({})".format(reprlib.repr(self.text))
  • 这里的 __iter__ 方法是生成器函数, 每次调用 __iter__ 方法都会自动创建迭代器,所以迭代器其实是生成器对象。这里可能有点绕,下面来详细解释下生成器的原理。
  • 生成器函数的工作原理:只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂。下面通过一个示例来说明生成器的行为。
def gen():
    for i in range(3):
        yield i

if __name__ == '__main__':
    g = gen()
    print(g)
    for item in g:
        print(item)
    print(next(g))
  • output
<generator object gen at 0x0C8ED300>
0
1
2
Traceback (most recent call last):
  File "xxx.py", line 56, in <module>
    print(next(g))
StopIteration
  • gen函数在调用是返回一个生成器对象(generator ),这个生成器对象时迭代器,会生成传给 yield 关键字的表达式的值,由于这里的g是迭代器,所以迭代完成后使用next方法获取不到元素而报错,除非重新生成一个迭代器才可以进行迭代。
  • __iter__ 方法是生成器函数,调用时会构建一个实现了迭代器接口的生成器对象,因此不用再定义额外的迭代器类了。
  • 遍历列表会消耗不少内存,特别是在列表比较大时,而且如果我们只需要某几个元素,重复遍历列表显然有点杀鸡用牛刀。Sentence类中findall返回的是列表,能否直接返回迭代器呢?re.finditer就考虑到了这点,re.finditer返回的不是列表,而是一个生成器,按需生成 re.MatchObject 实例。这样__iter__无需遍历列表就可以直接获取生成器实例,显然能节省大量内存。如下示例:
class Sentence:
    """可迭代的对象"""

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

    def __iter__(self):
        for match in RE_WORD.finditer(self.text):  # finditer 函数构建一个迭代器
            yield match.group()

    def __repr__(self):
        return "Sentence({})".format(reprlib.repr(self.text))
  • 想必python程序员经常会用到列表推导式,但是生成器表达式可能不太常用,生成器表达式构建一个生成器,但是会大大简化代码。下面通过一个例子先来看看列表推导式和生成器表达式的区别:
def gen():
    for i in range(3):
        print(i)
        yield str(i)


if __name__ == '__main__':
    res1 = [x*3 for x in gen()]  # 列表推导式
    print('---------------------------')
    res2 = (x*3 for x in gen())  # 生成器表达式
    for item in res1:
        print(item)
    print('---------------------------')
    for item in res2:
        print(item)
  • output
0
1
2
---------------------------
000
111
222
---------------------------
0
000
1
111
2
222
  • 可以看出,列表推导式会直接迭代完迭代器,返回一个列表,而生成器表达式返回一个生成器对象(res2),只有迭代这个生成器(res2)时才会执行迭代器函数。for 循环迭代 res2 时,实际上每次迭代时会隐式调用 next(res2),前进到 gen 函数中的下一个 yield 语句。
  • 由于生成器表达式返回一个生成器对象,所以可以进一步简化Sentence类:
class Sentence:
    """可迭代的对象"""

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

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

    def __repr__(self):
        """使用生成器表达式返回生成器对象"""
        return "Sentence({})".format(reprlib.repr(self.text))
  • 生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利。
  • 那么何时使用生成器表达式呢?和列表推导式一样,如果生成器表达式要分成多行写,建议定义生成器函数,以便提高可读性。在比较简单的情况下,生成器表达式可以代替列表推导式使用,这样做不用立即返回列表从而大大减少内存,在遇到大文件时尤其有用。
  • 不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用(后面说明)。如果一个类只是为了构建生成器而去实现__iter__ 方法,那还不如使用生成器函数。

yield from

  • 如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的 for 循环:
def chain(*iterables):
    for it in iterables:
        for i in it:
            yield i


if __name__ == '__main__':
    a = "abc"
    b = range(4)
    print(list(chain(a, b)))  # ['a', 'b', 'c', 0, 1, 2, 3]

python3.3引入了yield from,使用yield from可以改进 chain 生成器函数:

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

深入iter

  • 在 Python 中迭代对象 x 时会调用 iter(x)。可是,iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。看下面的例子就知道了:
def d1():
    return randint(1, 6)


if __name__ == '__main__':
    a = iter(d1, 1)
    for i in a:
        print(i)
  • 无论怎样运行,都不打印1,当可调用的对象返回为1时,和第二个参数(哨符)相同,就会触发StopIteration 异常,不产出哨符;也就是说,当随机数产出数字为哨符1 时,for循环终止。
  • 这样的思想有很大用处,比如定时任务的终止回调、特定匹配回调等场景。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,562评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,209评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,908评论 2 7