Python yield

04 Sep 2014

1 说明

原文:http://www.jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/

原文的代码格式都用下划线替代了,打下划线把我的指头都打抽了…..

2 学习Python:"yield"和Generator的解释

在开新课程前,我都会让学生自我评估一下自己的对Python各概念的理解情况。一些内容(比如:if/else的控制流处理、函数的定义和使用)大部分学生在课程开始前都可以理解。但是,还是有一些内容几乎所有的学生都不理解或者理解有限。其中,"generators和yield"就是最大的问题之一,我估计这也是所有Python初学者的共同点。

经常有人说,哪怕十分努力学习这个问题,还是不怎么理解。所以我来了,本文我就要解释下yield的作用、为何其有用以及如何使用它。

注意:在最近这些年,PEP一直都在给generator增加特性,让它变的越来越强大。下篇文章,我会探索 yield 与协程、协同多任务、异步IO结合后的强大(特别是这些在GvR1的基础之一"tulip "的原型实现中的使用)。当然,首先我们需要很好的理解 yieldgenerator 的工作原理。

2.1 协程和子程序

当我们调用函数的时候,执行都是从函数的第一行开始,一直遇到 return 、出现 exception 或者到达函数的结尾(相当是一个隐式的 return None )。函数把控制权交给调用者后就彻底结束了。任何局部变量和函数的工作都会被丢弃。下一次新的调用又会重新开始。

这个过程对计算机编程中的函数(或者任何子程序 )来说都是很标准的。如果能够定义一个特殊的函数:不像普通函数那样简单的就这么返回一个值而是可以产生一系列的值。有些情况下就会对编程有好处。要实现这个结果,就需要函数能够保存自己的状态。

我之所以说”产生一系列的值“,因为我们假定中的这个函数不是像普通情况下的 返回return 意味着函数将控制流交回给调用自己的地方。而 产生 ,意味着控制的转移是临时而自愿的,我们函数还希望后续能够再重新获得控制权。

在Python里,这样的函数就被叫做 generator ,而且十分有用。 generator (还有 yield )的引入最初是为了让程序员可以有一个更加简单直接的方式来编写会产生一系列值的代码。之前,写一个随机数产生器需要引入一个类或者模块,这样才能实现生成多个值并且还能记录上次调用时的内部状态。有了 generator ,这一问题变得很简单。

为了更好的明白 generator 解决的问题,举个例子。在这个例子里,请注意被解决的核心问题: 生成一系列的值

注意:Python以外,除了最简单的 generator ,其他的就都被称作 协程 。这个词语我们后面再说。请注意,在Python里,本文有关 协程 的说明都是指 generator 。Python严格定义了 generator协程 虽然也在本文中出现了,但是在Python里没有严格定义。

2.1.1 例子:素数问题

假设我们的老大要我们写一个函数:输入是 intlist ,返回的是一个包含这一列表中是素数的元素的Iterable。 记住,Iterable 就是一个可以一次返回一个成员的对象。 ”没问题“,我们说完就搞定如下的代码:

def get_primes(input_list):
    result_list = list()
    for element in input_list:
        if is_prime(element):
            result_list.append()

    return result_list

# or better yet...

def get_primes(input_list):
    return (element for element in input_list if is_prime(element))

# not germane to the example, but here's a possible implementation of
# is_prime...

def is_prime(number):
    if number > 1:
        if number == 2:
            return True
        if number % 2 == 0:
            return False
        for current in range(3, int(math.sqrt(number) + 1), 2):
            if number % current == 0: 
                return False
        return True
    return False

上面的两个 get_primes 都搞定了需求,于是就交活给老大了。老大也说没问题就是她想要的。

  1. 处理无穷序列

    但是,世事难料。几天后,老大又来了,说遇到了个小问题:她想把这个函数用在一个非常大的列表上。而且,列表大到没法放入系统内存。为了解决这个问题,她希望能够给 get_primes 一个 start 值,然后获得所有比这个 start 大的素数(说不定她就是在解决欧拉问题10)。

    考虑下新需求,我们知道代码得大改了。显然,我们没法返回一个从 start 值到无穷大的所有素数的列表(但是对无穷序列的操作却有很多应用需求)。使用传统函数解决这个问题的机会渺茫。

    放弃之前,我们需要看下到底是什么核心原因阻碍了我们写一个满足老大需求的函数。考虑一下,问题就是这个: 函数只有一次返回机会,而且必须一次性返回所有值 。貌似这句话很废话。”函数不就是这么工作的么,“我们会想,但是如果我们能多问一句就能看到蹊跷了,”如果函数换个方式工作呢?“

    想象下如果 get_primes 可以返回下一个值,而非一次性返回所有值。就不再需要构建一个列表了。没有列表,就没有内存问题了。由于我们老板就是把结果拿来迭代的,所以无所谓返回一个列表还是一个一个结果返回。

    可惜的是,这个方法貌似无法实现。就算我们假设有一个函数可以允许我们从 n 一直循环到 无穷大 ,返回值的时候还是有问题。

    def get_primes(start):
        for element in magical_infinite_range(start):
            if is_prime(element):
                return element
    

    假设get_primes如此被调用:

    def solve_number_10():
        # She *is* working on Project Euler #10, I knew it!
        total = 2
        for next_prime in get_primes(3):
            if next_prime < 2000000:
                total += next_prime
            else:
                print(total)
                return
    

    显然,在 get_primes 函数里, number=3 的时候,我们很快就会直接从第四行返回。相对直接的 return ,就需要一种方法产生一个值,然后下次调用的时候再继续返回下一个值。

    函数就无法实现这一功能了。他们执行 return 的时候,自己的工作就结束了。哪怕我们可以让函数再调用一次,也没法贯彻自己这样的想法:"来吧宝贝,现在不像我们平时那样从头开始,咱们直接从第四行开始吧。"函数只有一个 入口 :第一行。

2.2 进入Generator

上面的这种问题是很常见的,所以Python里新加入了一个结构来解决这个问题: generatorGenerator 不断的产生值。建立 generator 也尽量依照 generator函数 的概念,十分方便,我们会一起介绍。

Generator函数 如同普通的函数,但是当需要一个值的时候,它会利用 yield 来产生,而不是直接 return 。如果一个函数的 def 代码里包括了 yield ,函数就自动成为了一个 generator函数 (即使同时还包含 return ), 不需要其他的额外工作。

generator函数 创建的是 generator迭代器generator迭代器 这个名字从此你就不会再看到了,因为这个名字几乎就是 generator 的别名。记住 generator 就是一种特殊形式的 迭代器 。作为 迭代器generator 必须定义一些方法,其中之一就是 _next__(). 。为了获得 _generator 中的下一个值,我们需要使用操作 迭代器 的内置函数: next()

再说明白点: 为了从 generator 获得下个值,我们使用 next() 这个内置函数,而这个函数也是用来操作 迭代器 。( next() 负责调用 generator 中的__next__()方法)。由于 generator迭代器 的一种,所以是可以直接用在 for 循环上的。

所以当 next() 调用 generator 的时候, generator 负责返回一个值给任何调用 next() 的方法。具体的方法就是利用 yield 来将值传回去(比如, yield 7)。最容易记忆的方法就是认为 yieldgenerator函数return 语句(只是稍微有点奇妙的不同)。 同样,我们再说明白点: yeild 就是 generator函数return 语句(稍微有点奇妙的不同) 。 来看看一个简单的 generator函数

>>> def simple_generator_function():
>>>    yield 1
>>>    yield 2
>>>    yield 3

还有调用它的两种方法:

>>> for value in simple_generator_function():
>>>     print(value)
1
2
3
>>> our_generator = simple_generator_function()
>>> next(our_generator)
1
>>> next(our_generator)
2
>>> next(our_generator)
3

2.2.1 奇妙的部分?

哪里是奇妙的部分呢?就等着你问这个问题,蛤蛤。 generator函数 调用 yield 的时候,函数的状态会被冻结;所有变量的状态都会保存下来,需要执行的下一行代码会被记录下来等待 next() 的下次调用。待到调用时, generator函数 又会回复如之前保存的状态。如果 next() 从此不再调用,保存的状态最终都会被丢弃。 我们现在就可以重写 get_primes 成一个 generator函数 了。注意,我们不再需要 magical_infinite_range 函数了。使用简单的 while 循环,我们就可以建立自己的无穷序列了:

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

如果 generator函数 执行 return 或者到达定义结尾的时候,会产生一个 StopIteration 异常。这个信号意味着 generator 的值都已经产生完了(这也是 迭代器 的正常行为)。这也是为什么在 get_primes 里面有个 while True: 死循环。如果没有的话,第一次调用 next() 的时候我们还可以检查这个数字是否是素数,然后用 yield 传出来。下一次调用的时候,我们会将 number 加1,然后就到达了函数的结尾(此时就会产生 StopIteration )。一旦 generator 用尽了,下一次再调用 next() 就会产生错误,所以你只能获得所有的 generator 的值一次。下面的代码就没法正常工作:

>>> our_generator = simple_generator_function()
>>> for value in our_generator:
>>>     print(value)

>>> # our_generator has been exhausted...
>>> print(next(our_generator))
Traceback (most recent call last):
  File "<ipython-input-13-7e48a609051a>", line 1, in <module>
    next(our_generator)
StopIteration

>>> # however, we can always create a new generator
>>> # by calling the generator function again...

>>> new_generator = simple_generator_function()
>>> print(next(new_generator)) # perfectly valid
1

所以这里的 while 循环就是保证我们永远不会到达 get_prime 函数的结尾。这样我们就可以让 next() 调用 generator 的时候一直输出。这也是处理无穷序列的一般方法(也是 generator 的常用方法)。

2.2.2 看看程序的控制流

我们回到调用 get_primes 的代码: solve_number_10

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

看看我们在 solve_number_10 函数 for 循环里调用 get_primes 时前面几个元素是如何产生的,这样也有助于我们更好的理解程序的流程。当 for 循环向 get_primes 请求第一个值的时候,我们就像普通函数一样的进入到 get_primes 里。

  1. 我们在第三行进入 while 循环
  2. 恰好 if 条件符合(3是素数)
  3. 我们就产生值3,同时将控制流交回给 solve_number_10

然后我们回到 solve_number_10 :

  1. 3被传回到了 for 循环中
  2. for 循环把值赋给了 next_prime
  3. next_prime 被加到了 total
  4. for 循环从 get_primes 获得下一个值

这次,我们不是从 get_primes 的最开始重新开始,而是直接从之前停下来的 第五行 继续。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1 # <<<<<<<<<<

更重要的是, number 还是保持我们之前调用 yield 时的值(也就是3) 。我们说过, yield 不仅仅是返回值给调用 next() 的方法,而且还会保存 generator函数 中的状态。显然,之后 number 就会增加到4,我们又到达 while 循环的开始,继续增加 number 直到我们到达下一个素数(5)。然后我们再一次的 yield ,将值传出到 solve_number_10for 循环里。如此循环一直持续下去,直到 for 循环结束(代码中就是第一个大于2000000的素数)。

2.3 更加强大的功能

PEP342 中,还进一步的加入了将值传入到 generator 的支持。PEP342generator 可以产生值(之前就可以),接受值,或者在一个语句中同时产生和接受(不同)值。 为了说明如何将值传给 generator ,我们回到素数例子上。这次,不是简单的依次返回比 number 大的数字,我们要找到比一个连续的幂大的最小的素数(假设是10,我们就需要找到比10大的最小素数,然后是100,然后是1000,等等)。我们同样从 get_primes 开始:

def print_successive_primes(iterations, base=10):
    # like normal functions, a generator function
    # can be assigned to a variable

    prime_generator = get_primes(base)
    # missing code...
    for power in range(iterations):
        # missing code...

def get_primes(number):
    while True:
        if is_prime(number):
        # ... what goes here?

get_primes 的下一行需要说明下, yield number 会将 number 的值传出, other = yield foo 的形式则是, yield foo ,同时如果有值传入给我,则将 other 设置为这个值。你可以通过 generatorsend 方法传入值。

def get_primes(number):
    while True:
        if is_prime(number):
            number = yield number
        number += 1

这样,我们就是在每次 generator 产生值的时候将 number 设置为另外一个值。完整的代码如下:

def print_successive_primes(iterations, base=10):
    prime_generator = get_primes(base)
    prime_generator.send(None)
    for power in range(iterations):
        print(prime_generator.send(base ** power))

记住两件事:第一,我们可以打印 generator.send 的返回值,因为 send 函数在传值给 generator 的同时也返回了 generator 产生的值(和 yieldgenerator 函数 中的表现类似)。

第二,注意 prime_generator.send(None) 这行。当使用 send 去启动一个 generator 时(也就是从 generator 的第一行一直执行到第一次遇到 yield ),需要传一次 None 。这是因为定义 generator 的时候代码还没有运行到第一个 yield ,所以如果我们这时候发送一个值就没有语句接收到这个值。等到 generator 启动后,就可以像上面那样直接发送值了。

2.4 复习与总结

在这个系列的下半场,我们会讨论各种加强 generator 的方法,还有这些方法带来的强大能力。 yield 也成为Python里最强大的关键字。现在我们对 yield 的工作原理已经有了很好的理解,我们现在已经可以理解更多 yield 的古怪用法。 信不信由你,我们其实也只是了解了 yield 强大能力的皮毛。比如,像上面那样利用 send 传递值的方法,一般都不会用在我们例子里面这么简单的序列上。下面,我们贴了一段传递值的常用方法。我不会再说明这段代码具体如何工作及其工作原理,你们就当是第二部分的热身吧。

import random

def get_data():
    """Return 3 random integers between 0 and 9"""
    return random.sample(range(10), 3)

def consume():
    """Displays a running average across lists of integers sent to it"""
    running_sum = 0
    data_items_seen = 0

    while True:
        data = yield
        data_items_seen += len(data)
        running_sum += sum(data)
        print('The running average is {}'.format(running_sum / float(data_items_seen)))

def produce(consumer):
    """Produces a set of values and forwards them to the pre-defined consumer
    function"""
    while True:
        data = get_data()
        print('Produced {}'.format(data))
        consumer.send(data)
        yield

if __name__ == '__main__':
    consumer = consume()
    consumer.send(None)
    producer = produce(consumer)

    for _ in range(10):
        print('Producing...')
        next(producer)

2.4.1 重点

在这个文章里面请记得这些重点:

  • generators 用来产生一系列的值
  • yield 就像 generator函数return
  • yield 唯一的不同就是会保存 generator函数 的状态
  • generator 只是一种特殊的 iterator
  • 如同 iterator ,我们可以通过调用 next() 获得 generator 的下一个值
    • for 会隐式的调用 next()

希望这篇文章能有所助益。如果你从没听说过 generator ,希望你能够明白他是什么、为什么有用还有如何使用。如果你已经熟悉了 generator ,希望这篇文章能为你解惑。 老样子,任何部分不清楚的(或者存在错误的),请不吝指教。你可以在下面留言,或者邮件给我[email protected], 或者twitter上找我@jeffknupp

Footnotes: