[译]提高你的Python:yield与generators解释

用 yield 返回一个 generator 迭代器(只是一个我们可以遍历的对象)。可以使用一个值调用 yield ,其中这个值被处理为“生成的”值。下次在该 generator 迭代器上调用 next() (即例如在一个 for 循环中的下一步)时,生成器从它调用 yield 的地方恢复执行,而不是从该函数的开头。所有的状态被恢复,像局部变量的值, generators 继续执行,直到下一次调用 yield 。

如果你不能理解这,不要担心。我希望把教科书的定义拿开,这样我可以向你解释所有这些无意义之物实际意味着什么。

注意:近年来,随着通过 PEP 加入的特性, generators 变得更加强大。在我后面的博文里,我将探讨就协程( coroutine ),协作多任务及异步 I/O (特别是在 GvR 工作所在的 tulip 协议实现里的使用)而言, yield 的真实力量。不过,在我们达到目的前,我们需要透彻了解 yield 关键字与 generators 如何工作。

协程与子例程

在我们调用一个普通的 Python 函数时,执行从函数的第一行开始,直到遇到一条 return 语句,exception ,或函数末尾(这被视为一个隐含的 return None )。函数马上将控制返回给调用者,就是这样。任何由该函数完成并保存在局部变量的工作丢失了。对该函数新的调用将从头开始。

在计算机编程里,在讨论函数(更一般地称为 子例程 )时,这是非常标准的。不过,有时候能够创建一个不只是返回单个值,而是产生一系列值的函数,是有益的。可以说,要这样做,这样一个函数需要能够“保存其工作”。

我说,“产生一系列值”因为我们假设的函数不会在正常意义上“返回”。 Return 意味着该函数将执行控制返回给函数被调用的地方。不过, yield 意味着控制的转移是临时且自愿的,我们的函数期望在将来重新获得它。

在 Python 中,具有这些能力的函数称为 generators ,它们极其有用。最初引入 generators(以及 yield 语句)是为了给程序员更简单的方式来编写产生一系列值的代码。之前,创建像一个随机数生成器要求一个类或模块要生成值,同时记录调用间的状态。随着 generators 的引入,这变得简单得多。

为了更好地理解 generators 解决的问题,让我们看一下一个例子。贯穿这个例子,记住要解决的核心问题: 生成一系列值 。

注意: Python 以外,除了最简单的 generators 之外,所有的 generators 都被称为协程( coroutine )。在本文的后面我将使用协程这个术语。要记住的重要事情是,在 Python 中,这里描述为协程的一切仍然是一个 generator 。 Python 正式地定义术语 generator ;协程用在讨论中,但在 Python 里没有正式的定义。

例子:素数的乐趣

假设我们的老板要求我们编写一个函数,接受一个 int 的 list ,并返回包含元素是素数的某个可迭代对象。

记住,可迭代对象只是能够一次返回一个成员的对象。

扫描二维码关注公众号,回复: 5775271 查看本文章

“ 简单 ” ,我们说,我们写出以下代码:

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 实现满足了要求,因此我们告诉我们的老板我们做完了。她报告我们的函数能工作,正是她想要的。

处理无限序列

嗯,不太准确。几天后,我们老板回来并告诉我们她遇到了一个小问题:她想在一个非常大的数字列表上使用我们的 get_primes 函数。事实上,这个列表如此的大,仅创建它就耗尽了系统所有的内存。为了绕过这,她希望能够以一个 start 值调用 get_primes ,获取所有比 start 大的素数(可能她在解 Project Euler problem 10 )。

一旦我们思考这个新需求,我们会清楚地看到,对 get_primes 要求不只是简单的改动。显然,我们不能返回从 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 的情形,在第 4 行返回。不是 return ,我们需要一个方式来生成一个值,在要求下一个时,从我们离开的地方继续。

但是函数不能这样做。在它们 return 时,它们一劳永逸地完成了。即使我们保证函数将被再次调用,我们没有办法说,“ OK ,现在不是从我们通常做的那样从第一行开始,从我们离开的第 4行开始。”函数只有一个入口点:第一行。

进入generator

这类问题是如此普遍, Python 加入了新的构造来解决它: generator 。 Generator“ 产生 ” 值。通过同时引入的 generator 函数的概念, generator 的创建尽可能简单。

一个 generator 函数像普通函数那样定义,但一旦它需要生成一个值,它通过 yield 关键字而不是 return 来做到。如果一个 def 的主体包含 yield ,该函数自动成为一个 generator 函数(即使它还包含一个 return 语句)。除此之外,不需要别的。

Generator 函数创建 generator 迭代器。但是这是你最后一次看到术语 generator 迭代器,因为它们几乎总是被称为 generators 。记住一个 generator 是一种特殊的迭代器。要被视为迭代器,generator 必须定义几个方法,其中一个是 __next__() 。为了从一个 generator 得到下一个值,我们使用与迭代器相同的内置函数: next() 。

这一点还是值得重复: 为了从一个 generator 获取下一个值,我们使用与迭代器相同的内置函数: next() 

( next() 负责调用 generator 的 __next()__ 方法)。因为 generator 是一种迭代器,它可以在一个 for 循环里使用。

因此,一旦在一个 generator 上调用 next() 时,这个 generator 负责将一个值传回 next() 的调用者。这通过连同要传回的值调用 yield (比如 yield 7 )来完成。记住 yield 做什么最简单的方式是把它视为 generator 函数的 return (加上一点魔法)。

再次,这值得重复:对 generator 函数 yield 只是 return (加上一点魔术)。

下面是一个简单的 generator 函数:

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

下面是两个使用它的简单方式:

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

神奇吗?

神奇的部分是什么?很高兴你这样问!当一个 generator 函数调用 yield 时,这个 generator 函数的状态冻结;所有变量的值被保存,下一行要执行的代码行被记录,直到再次调用 next() 。一旦是,这个 generator 函数只是在离开的地方重新开始。如果 next() 不再调用,在 yield 调用期间记录的状态(最终)被丢弃。

让我们把 get_primes 重写为 generator 函数。注意,我们不再需要 magical_infinite_range 函数。使用一个简单的 while 循环,我们可以创建我们自己的无限序列:

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

如果 generator 函数调用 return 或者到达定义的末尾时,会抛出一个 StopIteration 异常。它通知 next() 的调用者, generator 被耗尽(这是正常的迭代器行为)。它也是在 get_primes 里 while True: 循环出现的原因。如果不是这样,第一次调用 next() 时,我们要检查这个值是否是素数,也许会 yield 它。如果再次调用 next() ,我们将徒劳地向 number 加 1 ,并命中这个 generator 函数的末尾(导致抛出 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

因此,这里 while 循环是确保我们不会到达 get_primes 的末尾。只要在这个 generator 上调用next() ,它允许我们生成一个值。在处理无限序列(一般来说 generators )时,这是一个常用的惯用语法。

观察流程

让我们回到调用 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. 我们进入第 3 行的while循环
  2. If 条件成立( 3 是素数)
  3. 我们 yield 值 3 ,返回控制给 solve_number_10 。

然后,回到 solve_number_10 :

  1. 值 3 被传回 for 循环
  2. For 循环将 next_prime 赋给这个值
  3. Next_prime 加到 total 上
  4. For 循环从 get_primes 请求下一个元素

但是这次不是从头进入 get_primes ,我们从第 5 行开始,我们离开的地方。

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 )。我们再次在 solve_number_10 里的 for 循环中 yield number 的值。这个循环持续,直到 for 循环停止(在第一个大于2,000,000 的素数处)。

更多能力

在 PEP 342 中,增加了将值传递入 generators 的支持。 PEP 342 给予 generator yield 一个值(如前),接受一个值,或者在单条语句里同时 yield 一个值及接受一个(可能不同的)值的能力。

为了展示值如何发送到一个 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 设置为这个值。”使用 generator 的 send 方法,你可以把值“发送给”一个 generator 。

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

这样,每次这个 generator yield 时,我们可以将 number 设置为不同的值。现在我们可以在 print_successive_primes 里填入缺失的代码:

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 yield 的值(反映了在 generator 函数内部 yield 如何工作)。

其次,注意 prime_generator.send(None) 一行。在你使用 send 来“启动”一个 generator (即,执行从这个 generator 第一行到第一个 yield 语句的代码)时,你必须 send None 。这很合理,因为根据定义这个 generator 还没到达第一条 yield 语句,因此如果我们发送一个真实值,将没有东西来“接收”它。一旦启动了这个 generator ,我们可以像我们上面做的那样发送值。

总结

在本系列的下半部分,我们将讨论增强 generators 的各种方法,以及作为结果它们获得的能力。 Yield 已经变成 Python 里最强大的关键字。现在,我们已经奠定了对 yield 如何工作一个坚实的理解,我们有了理解某些使用 yield 、更加烧脑事物的必要知识。

信不信由你,我们仅触及了 yield 能力的皮毛。例如,虽然 send 确实如上描述那样工作,在生成像我们例子那样简单的序列时,几乎从不使用它。下面,我贴出了 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)

记住……

我希望你们能从这次讨论中得到一些关键的思想:

  • Generators 用于生成一系列值
  • Yield 就像 generator 函数的 return
  • Yield 做的唯一的其他事情是保存 generator 函数的状态
  • 一个 generator 只是一个特殊类型的迭代器
  • 就像迭代器,使用 next() ,我们可以从一个 generator 获取下一个值
    • For 通过隐含地调用 next() 获取值

我希望本文是有帮助的。如果你从未听过 generator ,我希望你现在理解它们是什么,为什么它们是有用的,以及如何使用它们。如果你有点熟悉 generators ,我希望现在所有困惑都清除了

猜你喜欢

转载自blog.csdn.net/zhoulei124/article/details/88993797