生成器进化到协程 Part 1

发布时间:2019-09-28 08:37:58编辑:auto阅读(1790)

    前言

    这篇文章大部分来自 David Beazley 在 PyCon 2014 的 PPT 《Generators: The Final Frontier》。这个PPT很长而且非常烧脑,建议在阅读前应了解 Python 的生成器与携程相关知识,推荐《流畅的 Python》。

    生成器(generator)

    使用 yield 来定义一个生成器

    def countdown(n):
        while n > 0:
            yield n
            n -= 1
        
    c = countdown(10)
    c
    <generator object countdown at 0x0000021F5EAB9F10>

    生成器可用于迭代

    for x in countdown(10):
        print('倒数:', x)
    倒数: 10
    倒数: 9
    倒数: 8
    倒数: 7
    倒数: 6
    倒数: 5
    倒数: 4
    倒数: 3
    倒数: 2
    倒数: 1

    可以通过 next() 来一步步地让生成器 yield 一个值,直到函数迭代器结束并抛出 StopIteration。如果你对这一头雾水,建议阅读《Fluent Python》14.4 章。
    这里 for 其实相当于不断地调用 next 并处理 StopIteration

    c = countdown(3)
    
    # next(c)
    3
    # next(c)
    2
    # next(c)
    1

    把生成器当作管道

    你可以嵌套生成器,这会导致类似于 Unix 命令行管道的效果

    def add_A(seq):
        for item in seq:
            yield item + '-A'
    
    def add_B(seq):
        for item in seq:
            yield item + '-B'
    
    def add_C(seq):
        for item in seq:
            yield item + '-C'
            
    seq = ['apple', 'banana', 'orange']
    
    stacked_generator = add_C(add_B(add_A(seq)))
    
    for item in stacked_generator:
        print(item)
    apple-A-B-C
    banana-A-B-C
    orange-A-B-C

    可以看到,我们为 seq 里的每项都依次添加了一个 tag。

    yield 可以接受传值

    yield 的作用是向调用者返回一个值,调用者其实也可以向生成器传值。

    def receiver():
        while True:
            received_item = yield
            print('收到:', received_item)
    
    def caller():
        recv = receiver()
        next(recv) # 使生成器前进到 yield
        for i in 'abcd':
            recv.send(i)
            
    caller()
    收到: a
    收到: b
    收到: c
    收到: d

    send 函数的返回值是什么呢?

    def receiver():
        call_times = 0
        while True:
            item = yield call_times
            print('收到:', item)
            call_times += 1
            
    def caller():
        recv = receiver()
        next(recv)
        for i in 'abcd':
            ret_value = recv.send(i)
            print('返回值: ', ret_value)
            
    caller()
    收到: a
    返回值:  1
    收到: b
    返回值:  2
    收到: c
    返回值:  3
    收到: d
    返回值:  4

    所以 send 可以向生成器传值的同时会让生成器前进到下一个 yield 语句,并将 yield 右侧的值作为返回值。

    生成器 101

    • yield 用于定义生成器函数
    • 只要 yield 存在该函数必定是一个生成器
    • 调用该函数返回一个生成器

    让一个生成器前进

    使用 next 使一个生成器前进到下一个 yield 语句处,并将产出值(yielded value)作为其返回值。使用 gen.__next__()效果相同。

    注意:这是一个新创建的生成器唯一允许的操作。

    def generator():
        yield 'a'
        yield 'b'
        
    gen = generator()
    
    # next(gen)
    'a'
    # next(gen)
    'b'

    给生成器传值

    可以通过调用生成器的 send 方法来向生成器传值,这将让生成器从上次暂停的 yield 前进到下个 yield,并将产出值作为 send 的返回值。

    def generator():
        item = yield 'a'
        print(item)
        another_item = yield 'b'
        
    gen = generator()
    print(next(gen))
    print(gen.send(1))
    a
    1
    b

    关闭一个生成器

    通过调用生成器 close 方法可以生成器在 yield 语句处抛出 GeneratorExit。这时仅允许 return,如果没有捕捉这个错误,生成器会静默关闭,不抛出错误。

    def generator():
        times = 0
        while True:
            yield times
            times += 1
                
    gen = generator()
    print(next(gen))
    print(next(gen))
    gen.close() # 不会抛出错误
    0
    1

    抛出错误

    调用生成器的 throw 方法可以在 yield 处抛出某个特定类型的错误,如果生成器内部可以捕捉这个错误,那生成器将前进到下个 yield 语句处,并将产出值作为 throw 的返回值,否则中止生成器。
    throw 的函数签名如下:

    throw(typ, [,val, [,tb]])

    其中 tyb 是某错误类型,val是错误信息,tb 为 traceback。更多信息可以参考官方的PEP0342

    def generator():
        try:
            yield 'apple'
        except RuntimeError as e:
            print('捕捉到:', e)
        yield 'banana'
        
    
    gen = generator()
    print(next(gen))
    print(gen.throw(RuntimeError, '运行错误'))
    apple
    捕捉到: 运行错误
    banana

    生成器返回值

    如果在生成器函数中加上 return 那在运行到 return 时将会把返回值作为 StopIteration 的值传递出去。这个是 Python3 的特性,Python2 生成器不能返回某个值。

    def generator():
        yield
        return 'apple'
        
    g = generator()
    next(g)
    try:
        next(g)
    except StopIteration as e:
        print(e)
    apple

    生成器委托

    使用 yield from 可以帮你对一个生成器不断调用 next 函数,并返回生成器的返回值。言下之意是你可以在生成器里调用生成器

    def generator():
        yield 'a'
        yield 'b'
        return 'c'
        
    def func():
        result = yield from generator()
        print(result)

    调用 func 结果是返回一个生成器

    # func()
    <generator object func at 0x0000021F5EB0F990>
    
    # next(func())
    'a'

    另外一个例子

    def chain(x, y):
        yield from x
        yield from y
        
    a = [1, 2, 3]
    b = [4, 5, 6]
    
    for i in chain(a, b):
        print(i, end=' ')
    1 2 3 4 5 6 
    c = [7, 8, 9]
    for i in chain(a, chain(b, c)):
        print(i, end=' ')
    1 2 3 4 5 6 7 8 9 

    Part 1总结

    生成器定义

    def generator():
        ...
        yield
        ...
        return result

    生成器操作

    gen = generator()
    
    # 使一个生成器前进
    next(gen)
    
    # 传递一个值
    gen.send(item)
    
    # 中止生成器
    gen.close()
    
    # 抛出错误
    gen.throw(exc, val, tb)
    
    # 委托
    result = yield from gen

关键字