Python中生成器和yield语句的用法详解

发表于 5年以前  | 总阅读数:620 次

在开始课程之前,我要求学生们填写一份调查表,这个调查表反映了它们对Python中一些概念的理解情况。一些话题("if/else控制流" 或者 "定义和使用函数")对于大多数学生是没有问题的。但是有一些话题,大多数学生只有很少,或者完全没有任何接触,尤其是"生成器和yield关键字"。我猜这对大多数新手Python程序员也是如此。

有事实表明,在我花了大功夫后,有些人仍然不能理解生成器和yield关键字。我想让这个问题有所改善。在这篇文章中,我将解释yield关键字到底是什么,为什么它是有用的,以及如何来使用它。

注意:最近几年,生成器的功能变得越来越强大,它已经被加入到了PEP。在我的下一篇文章中,我会通过协程(coroutine),协同式多任务处理(cooperative multitasking),以及异步IO(asynchronous I/O)(尤其是GvR正在研究的 "tulip" 原型的实现)来介绍yield的真正威力。但是在此之前,我们要对生成器和yield有一个扎实的理解.

协程与子例程

我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None)。一旦函数将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。

对于在计算机编程中所讨论的函数,这是很标准的流程。这样的函数只能返回一个值,不过,有时可以创建能产生一个序列的函数还是有帮助的。要做到这一点,这种函数需要能够"保存自己的工作"。

我说过,能够"产生一个序列"是因为我们的函数并没有像通常意义那样返回。return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。而"yield"的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。

在Python中,拥有这种能力的"函数"被称为生成器,它非常的有用。生成器(以及yield语句)最初的引入是为了让程序员可以更简单的编写用来产生值的序列的代码。 以前,要实现类似随机数生成器的东西,需要实现一个类或者一个模块,在生成数据的同时保持对每次调用之间状态的跟踪。引入生成器之后,这变得非常简单。

为了更好的理解生成器所解决的问题,让我们来看一个例子。在了解这个例子的过程中,请始终记住我们需要解决的问题:生成值的序列。

注意:在Python之外,最简单的生成器应该是被称为协程(coroutines)的东西。在本文中,我将使用这个术语。请记住,在Python的概念中,这里提到的协程就是生成器。Python正式的术语是生成器;协程只是便于讨论,在语言层面并没有正式定义。

例子:有趣的素数

假设你的老板让你写一个函数,输入参数是一个int的list,返回一个可以迭代的包含素数1 的结果。

记住,迭代器(Iterable) 只是对象每次返回特定成员的一种能力。

你肯定认为"这很简单",然后很快写出下面的代码:


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

      return result_list

    # 或者更好一些的...

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

    # 下面是 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

上面 is_prime 的实现完全满足了需求,所以我们告诉老板已经搞定了。她反馈说我们的函数工作正常,正是她想要的。

处理无限序列

噢,真是如此吗?过了几天,老板过来告诉我们她遇到了一些小问题:她打算把我们的get_primes函数用于一个很大的包含数字的list。实际上,这个list非常大,仅仅是创建这个list就会用完系统的所有内存。为此,她希望能够在调用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中,一上来就会碰到输入等于3的,并且在函数的第4行返回。与直接返回不同,我们需要的是在退出时可以为下一次请求准备一个值。

不过函数做不到这一点。当函数返回时,意味着全部完成。我们保证函数可以再次被调用,但是我们没法保证说,"呃,这次从上次退出时的第4行开始执行,而不是常规的从第一行开始"。函数只有一个单一的入口:函数的第1行代码。

走进生成器

这类问题极其常见以至于Python专门加入了一个结构来解决它:生成器。一个生成器会"生成"值。创建一个生成器几乎和生成器函数的原理一样简单。

一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。如果一个def的主体包含yield,这个函数会自动变成一个生成器(即使它包含一个return)。除了以上内容,创建一个生成器没有什么多余步骤了。

生成器函数返回生成器的迭代器。这可能是你最后一次见到"生成器的迭代器"这个术语了, 因为它们通常就被称作"生成器"。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法(method),其中一个就是next()。如同迭代器一样,我们可以使用next()函数来获取下一个值。

为了从生成器获取下一个值,我们使用next()函数,就像对付迭代器一样。

(next()会操心如何调用生成器的next()方法)。既然生成器是一个迭代器,它可以被用在for循环中。

每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作(例如yield 7)。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return(加上点小魔法)。**

yield就是专门给生成器用的return(加上点小魔法)。

下面是一个简单的生成器函数:


    >>> 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

魔法?

那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用yield,生成器函数的"状态"会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用next(),yield保存的状态就被无视了。

我们来重写get_primes()函数,这次我们把它写作一个生成器。注意我们不再需要magical_infinite_range函数了。使用一个简单的while循环,我们创造了自己的无穷串列。

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

如果生成器函数调用了return,或者执行到函数的末尾,会出现一个StopIteration异常。 这会通知next()的调用者这个生成器没有下一个值了(这就是普通迭代器的行为)。这也是这个while循环在我们的get_primes()函数出现的原因。如果没有这个while,当我们第二次调用next()的时候,生成器函数会执行到函数末尾,触发StopIteration异常。一旦生成器的值用完了,再调用next()就会出现错误,所以你只能将每个生成器的使用一次。下面的代码是错误的:


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

    >>> # 我们的生成器没有下一个值了...
    >>> print(next(our_generator))
    Traceback (most recent call last):
     File "<ipython-input-13-7e48a609051a>", line 1, in <module>
      next(our_generator)
    StopIteration

    >>> # 然而,我们总可以再创建一个生成器
    >>> # 只需再次调用生成器函数即可

    >>> new_generator = simple_generator_function()
    >>> print(next(new_generator)) # 工作正常
    1

因此,这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。

执行流程

让我们回到调用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,这时与进入普通函数没有区别。

  • 进入第三行的while循环
  • 停在if条件判断(3是素数)
  • 通过yield将3和执行控制权返回给solve_number_10

接下来,回到insolve_number_10:

  • for循环得到返回值3
  • for循环将其赋给next_prime
  • total加上next_prime
  • 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()的调用方,同时还会保存生成器函数的"状态"。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于2,000,000)。

更给力点

在PEP 342中加入了将值传给生成器的支持。PEP 342加入了新的特性,能让生成器在单一语句中实现,生成一个值(像从前一样),接受一个值,或同时生成一个值并接受一个值。

我们用前面那个关于素数的函数来展示如何将一个值传给生成器。这一次,我们不再简单地生成比某个数大的素数,而是找出比某个数的等比级数大的最小素数(例如10, 我们要生成比10,100,1000,10000 ... 大的最小素数)。我们从get_primes开始:


    def print_successive_primes(iterations, base=10):
      # 像普通函数一样,生成器函数可以接受一个参数

      prime_generator = get_primes(base)
      # 这里以后要加上点什么
      for power in range(iterations):
        # 这里以后要加上点什么

    def get_primes(number):
      while True:
        if is_prime(number):
        # 这里怎么写?
     get_primes的后几行需要着重解释。yield关键字返回number的值,而像 other = yield foo 这样的语句的意思是,"返回foo的值,这个值返回给调用者的同时,将other的值也设置为那个值"。你可以通过send方法来将一个值"发送"给生成器。

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

通过这种方式,我们可以在每次执行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在发送数据给生成器的同时还返回生成器通过yield生成的值(就如同生成器中yield语句做的那样)。

第二点,看一下prime_generator.send(None)这一行,当你用send来"启动"一个生成器时(就是从生成器函数的第一行代码执行到第一个yield语句的位置),你必须发送None。这不难理解,根据刚才的描述,生成器还没有走到第一个yield语句,如果我们发生一个真实的值,这时是没有人去"接收"它的。一旦生成器启动了,我们就可以像上面那样发送数据了。

综述

在本系列文章的后半部分,我们将讨论一些yield的高级用法及其效果。yield已经成为Python最强大的关键字之一。现在我们已经对yield是如何工作的有了充分的理解,我们已经有了必要的知识,可以去了解yield的一些更"费解"的应用场景。

不管你信不信,我们其实只是揭开了yield强大能力的一角。例如,send确实如前面说的那样工作,但是在像我们的例子这样,只是生成简单的序列的场景下,send几乎从来不会被用到。下面我贴一段代码,展示send通常的使用方式。对于这段代码如何工作以及为何可以这样工作,在此我并不打算多说,它将作为第二部分很不错的热身。


    import random

    def get_data():
      """返回0到9之间的3个随机数"""
      return random.sample(range(10), 3)

    def consume():
      """显示每次传入的整数列表的动态平均值"""
      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):
      """产生序列集合,传递给消费函数(consumer)"""
      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)

请谨记……

我希望您可以从本文的讨论中获得一些关键的思想:

  • generator是用来产生一系列值的
  • yield则像是generator函数的返回结果
  • yield唯一所做的另一件事就是保存一个generator函数的状态
  • generator就是一个特殊类型的迭代器(iterator)
  • 和迭代器相似,我们可以通过使用next()来从generator中获取下一个值
  • 通过隐式地调用next()来忽略一些值

我希望这篇文章是有益的。如果您还从来没有听说过generator,我希望现在您可以理解它是什么以及它为什么是有用的,并且理解如何使用它。如果您已经在某种程度上比较熟悉generator,我希望这篇文章现在可以让您扫清对generator的一些困惑。

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237276次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8110次阅读
 目录