提高你的Python:yield与generators解释

作者:Jeff Knupp

原文地址:https://jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/

在开始辅导之前,我要求新学生填写一份简短的自我评估,报告他们对各种Python概念的理解程度。即使在开始辅导之前,某些话题(“使用if/else的控制流”以及“定义及使用函数”)学生都理解。不过,有少数话题,几乎所有的学生报告不了解或知之甚少。在这些之中,“generators与关键字yield”是最大的罪魁祸首之一。我猜这是大多数Python新手的情况。

许多人报告说,即使在一起努力自学这个话题,也难以理解generatorsyield关键字。我希望改变这。在本文中,我将解释yield关键字的作用,为什么它是有用的,以及如何使用它。

Python生成器是什么(教科书定义)

Python generator是一个函数,它通过调用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中,这里描述为协程的一切仍然是一个generatorPython正式地定义术语generator;协程用在讨论中,但在Python里没有正式的定义。

例子:素数的乐趣

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

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

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

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加入了新的构造来解决它:generatorGenerator“产生值。通过同时引入的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)
1
2
3
>>> our_generator = simple_generator_function()
>>> next(our_generator)
1
>>> next(our_generator)
2
>>> next(our_generator)
3

神奇吗?

神奇的部分是什么?很高兴你这样问!当一个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_primeswhile True:循环出现的原因。如果不是这样,第一次调用next()时,我们要检查这个值是否是素数,也许会yield它。如果再次调用next(),我们将徒劳地向number1,并命中这个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
1

因此,这里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_10for循环中的get_primes时,观察前几个元素是如何创建的,是有帮助的。在for循环从get_primes请求第一个值时,我们像进入一个普通函数那样进入get_primes

  1. 我们进入第3行的while循环
  2. If条件成立(3是素数)
  3. 我们yield3,返回控制给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设置为这个值。”使用generatorsend方法,你可以把值“发送给”一个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,我希望现在所有困惑都清除了。

如常,如果如何部分是不清晰(或者,更重要的,包含错误),无论如何告诉我们。你可以在下面留言,通过[email protected]给我发邮件,或者在Twitter@jeffknupp找我。

猜你喜欢

转载自blog.csdn.net/wuhui_gdnt/article/details/88887664