【问题标题】:What can you use generator functions for?你可以使用生成器函数做什么?
【发布时间】:2010-09-11 06:42:16
【问题描述】:

我开始学习 Python 并且遇到了生成器函数,它们中有一个 yield 语句。我想知道这些函数真正擅长解决哪些类型的问题。

【问题讨论】:

标签: python generator


【解决方案1】:

生成器给你懒惰的评估。您可以通过迭代它们来使用它们,可以使用 'for' 显式地使用它们,也可以通过将其传递给任何迭代的函数或构造来隐式地使用它们。您可以将生成器视为返回多个项目,就好像它们返回一个列表一样,但不是一次全部返回它们,而是一个接一个地返回它们,并且生成器函数暂停直到请求下一个项目。

生成器适用于计算大量结果(尤其是涉及循环本身的计算),您不知道是否需要所有结果,或者您不想为所有结果分配内存同时。或者对于生成器使用另一个生成器的情况,或者消耗一些其他资源的情况,如果发生越晚越好。

生成器的另一个用途(实际上是一样的)是用迭代替换回调。在某些情况下,您希望函数完成大量工作并偶尔向调用者报告。传统上,您会为此使用回调函数。您将此回调传递给工作函数,它会定期调用此回调。生成器方法是工作函数(现在是生成器)对回调一无所知,并且只在它想要报告某些内容时产生。调用者不是编写单独的回调并将其传递给工作函数,而是在生成器周围的一个小“for”循环中完成所有报告工作。

例如,假设您编写了一个“文件系统搜索”程序。您可以完整地执行搜索,收集结果,然后一次显示一个。在显示第一个结果之前,必须收集所有结果,并且所有结果都将同时存储在内存中。或者您可以在找到结果时显示结果,这样会更节省内存并且对用户更友好。后者可以通过将结果打印函数传递给文件系统搜索函数来完成,也可以通过将搜索函数作为生成器并迭代结果来完成。

如果您想查看后两种方法的示例,请参阅 os.path.walk()(带有回调的旧文件系统遍历函数)和 os.walk()(新的文件系统遍历生成器)。当然,如果你真的想将所有结果收集到一个列表中,生成器方法转换为大列表方法很简单:

big_list = list(the_generator)

【讨论】:

  • 生成文件系统列表的生成器是否与在循环中运行该生成器的代码并行执行操作?理想情况下,计算机将运行循环体(处理最后一个结果),同时执行生成器必须执行的任何操作以获得下一个值。
  • @StevenLu:除非在yieldjoin之前手动启动线程以获得下一个结果,否则它不会并行执行(并且没有标准库生成器这样做;秘密启动线程是不受欢迎的)。生成器在每个yield 处暂停,直到请求下一个值。如果生成器正在包装 I/O,则操作系统可能会主动缓存文件中的数据,假设它很快就会被请求,但那是操作系统,不涉及 Python。
  • stackoverflow.com/questions/19845101/… 可以在这里看到回调与生成器设计的示例
【解决方案2】:

使用生成器的原因之一是为了使某种解决方案的解决方案更清晰。

另一种方法是一次处理一个结果,避免构建庞大的结果列表,无论如何都要分开处理。

如果你有这样的斐波那契向上到 n 函数:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

您可以更轻松地编写函数:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

功能更清晰。如果你使用这样的功能:

for x in fibon(1000000):
    print x,

在此示例中,如果使用生成器版本,则根本不会创建整个 1000000 项列表,一次只会创建一个值。使用列表版本时不会出现这种情况,首先会创建一个列表。

【讨论】:

  • 如果你需要一份清单,你可以随时list(fibon(5))
  • 我想补充一点,如果您尝试在 n = 1,000,000 的情况下运行该函数,那么您的计算机将会非常困难。不过,使用生成器运行它非常好。
【解决方案3】:

我发现这个解释消除了我的疑问。因为有可能不知道Generators的人也不知道yield

返回

return 语句是销毁所有局部变量并将结果值返回(返回)给调用者的地方。如果稍后调用相同的函数,该函数将获得一组新的变量。

产量

但是,如果我们退出函数时没有丢弃局部变量怎么办?这意味着我们可以在我们离开的地方resume the function。这是引入generators 概念的地方,yield 语句从function 停止的地方继续。

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

这就是 Python 中 returnyield 语句之间的区别。

Yield 语句使函数成为生成器函数。

所以生成器是创建迭代器的简单而强大的工具。它们的编写方式与常规函数类似,但它们在想要返回数据时使用yield 语句。每次调用 next() 时,生成器都会从中断处继续(它会记住所有数据值以及最后执行的语句)。

【讨论】:

    【解决方案4】:

    请参阅PEP 255 中的“动机”部分。

    生成器的一个不明显的用途是创建可中断函数,它允许您在不使用线程的情况下执行更新 UI 或“同时”(实际上是交错)运行多个作业。

    【讨论】:

    • 动机部分很好,因为它有一个具体的例子:“当生产者函数的工作足够艰巨,需要维护产生的值之间的状态时,大多数编程语言都没有提供令人愉快和有效的解决方案。给生产者的参数列表添加回调函数……比如标准库中的tokenize.py就是采用这种方式”
    【解决方案5】:

    现实世界的例子

    假设您的 MySQL 表中有 1 亿个域,并且您想要更新每个域的 Alexa 排名。

    您需要做的第一件事是从数据库中选择您的域名。

    假设您的表名是domains,列名是domain

    如果您使用SELECT domain FROM domains,它将返回 1 亿行,这将消耗大量内存。所以你的服务器可能会崩溃。

    所以你决定分批运行程序。假设我们的批量大小是 1000。

    在我们的第一批中,我们将查询前 1000 行,检查每个域的 Alexa 排名并更新数据库行。

    在我们的第二批中,我们将处理接下来的 1000 行。在我们的第三批中,它将是从 2001 年到 3000 年等等。

    现在我们需要一个生成器函数来生成我们的批次。

    这是我们的生成器函数:

    def ResultGenerator(cursor, batchsize=1000):
        while True:
            results = cursor.fetchmany(batchsize)
            if not results:
                break
            for result in results:
                yield result
    

    如您所见,我们的函数保持yielding 结果。如果您使用关键字return 而不是yield,则整个函数将在返回时结束。

    return - returns only once
    yield - returns multiple times
    

    如果一个函数使用关键字yield,那么它就是一个生成器。

    现在你可以像这样迭代:

    db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
    cursor = db.cursor()
    cursor.execute("SELECT domain FROM domains")
    for result in ResultGenerator(cursor):
        doSomethingWith(result)
    db.close()
    

    【讨论】:

    • 如果yield可以用递归/动态编程来解释会更实用!
    【解决方案6】:

    缓冲。如果以大块的形式获取数据是有效的,但以小块的形式处理它,那么生成器可能会有所帮助:

    def bufferedFetch():
      while True:
         buffer = getBigChunkOfData()
         # insert some code to break on 'end of data'
         for i in buffer:    
              yield i
    

    以上内容可让您轻松地将缓冲与处理分开。消费者函数现在可以一个一个地获取值,而不必担心缓冲。

    【讨论】:

    • 如果 getBigChuckOfData 不是懒惰的,那么我不明白 yield 在这里有什么好处。这个函数的用例是什么?
    • 但关键是,IIUC,bufferedFetch 是 lazyfying 调用 getBigChunkOfData。如果 getBigChunkOfData 已经是惰性的,那么 bufferedFetch 将毫无用处。每次调用 bufferedFetch() 都会返回一个缓冲区元素,即使 BigChunk 已经被读入。而且您不需要显式地保留要返回的下一个元素的计数,因为 yield 的机制会隐含地做到这一点。跨度>
    【解决方案7】:

    我发现生成器在清理代码方面非常有帮助,它为您提供了一种非常独特的方式来封装和模块化代码。在您需要根据自己的内部处理不断吐出值的情况下,并且当需要从代码中的任何位置(而不仅仅是在循环或块中)调用该东西时,生成器是 要使用的功能。

    一个抽象的例子是斐波那契数生成器,它不存在于循环中,当从任何地方调用它时,总是会返回序列中的下一个数:

    def fib():
        first = 0
        second = 1
        yield first
        yield second
    
        while 1:
            next = first + second
            yield next
            first = second
            second = next
    
    fibgen1 = fib()
    fibgen2 = fib()
    

    现在您有两个斐波那契数生成器对象,您可以从代码中的任何位置调用它们,它们将始终按如下顺序返回更大的斐波那契数:

    >>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
    0
    1
    1
    2
    >>> fibgen2.next(); fibgen2.next()
    0
    1
    >>> fibgen1.next(); fibgen1.next()
    3
    5
    

    生成器的美妙之处在于它们封装了状态,而无需经历创建对象的过程。考虑它们的一种方式是将它们视为记住其内部状态的“函数”。

    我从 Python Generators - What are they? 获得了斐波那契示例,稍加想象,您可以想出许多其他情况,其中生成器可以很好地替代 for 循环和其他传统的迭代构造。

    【讨论】:

      【解决方案8】:

      简单的解释: 考虑一个for 声明

      for item in iterable:
         do_stuff()
      

      很多时候,iterable 中的所有项目不需要从一开始就存在,而是可以根据需要即时生成。这在两者中都可以更有效

      • 空间(您永远不需要同时存储所有项目)和
      • 时间(迭代可能在需要所有项目之前完成)。

      有时,您甚至无法提前了解所有项目。例如:

      for command in user_input():
         do_stuff_with(command)
      

      你无法事先知道所有用户的命令,但如果你有一个生成器来处理你的命令,你可以使用这样一个很好的循环:

      def user_input():
          while True:
              wait_for_command()
              cmd = get_command()
              yield cmd
      

      使用生成器,您还可以对无限序列进行迭代,这在迭代容器时当然是不可能的。

      【讨论】:

      • ...无限序列可能是通过在一个小列表上反复循环产生的,在到达结束后返回开始。我用它来选择图表中的颜色,或者在文本中产生忙碌的悸动者或微调器。
      • @mataap:有一个itertool ——见cycles
      【解决方案9】:

      我最喜欢的用途是“过滤”和“减少”操作。

      假设我们正在读取一个文件,并且只想要以“##”开头的行。

      def filter2sharps( aSequence ):
          for l in aSequence:
              if l.startswith("##"):
                  yield l
      

      然后我们可以在适当的循环中使用生成器函数

      source= file( ... )
      for line in filter2sharps( source.readlines() ):
          print line
      source.close()
      

      reduce 示例类似。假设我们有一个文件,我们需要在其中定位 &lt;Location&gt;...&lt;/Location&gt; 行的块。 [不是 HTML 标记,而是恰好看起来像标记的行。]

      def reduceLocation( aSequence ):
          keep= False
          block= None
          for line in aSequence:
              if line.startswith("</Location"):
                  block.append( line )
                  yield block
                  block= None
                  keep= False
              elif line.startsWith("<Location"):
                  block= [ line ]
                  keep= True
              elif keep:
                  block.append( line )
              else:
                  pass
          if block is not None:
              yield block # A partial block, icky
      

      同样,我们可以在适当的 for 循环中使用这个生成器。

      source = file( ... )
      for b in reduceLocation( source.readlines() ):
          print b
      source.close()
      

      这个想法是生成器函数允许我们过滤或减少一个序列,一次生成另一个序列一个值。

      【讨论】:

      • fileobj.readlines() 会将整个文件读取到内存中的列表中,从而违背了使用生成器的目的。由于文件对象已经是可迭代的,您可以使用 for b in your_generator(fileobject): 代替。这样您的文件将一次读取一行,以避免读取整个文件。
      • reduceLocation 产生一个列表很奇怪,为什么不只产生每一行呢?此外,filter 和 reduce 是具有预期行为的内置函数(参见 ipython 等中的帮助),“reduce”的用法与 filter 相同。
      • readlines() 的要点。在单元测试期间,我通常意识到文件是一流的行迭代器。
      • 实际上,“减少”是将多个单独的行组合成一个复合对象。好吧,这是一个列表,但它仍然是从源头上截取的。
      【解决方案10】:

      您可以使用生成器的一个实际示例是,如果您有某种形状,并且想要迭代它的角、边缘或其他任何东西。对于我自己的项目(源代码here),我有一个矩形:

      class Rect():
      
          def __init__(self, x, y, width, height):
              self.l_top  = (x, y)
              self.r_top  = (x+width, y)
              self.r_bot  = (x+width, y+height)
              self.l_bot  = (x, y+height)
      
          def __iter__(self):
              yield self.l_top
              yield self.r_top
              yield self.r_bot
              yield self.l_bot
      

      现在我可以创建一个矩形并在其角上循环:

      myrect=Rect(50, 50, 100, 100)
      for corner in myrect:
          print(corner)
      

      您可以使用iter_corners 方法代替__iter__,并使用for corner in myrect.iter_corners() 调用它。使用__iter__ 更加优雅,因为我们可以直接在for 表达式中使用类实例名称。

      【讨论】:

      • 我喜欢将类似的类字段作为生成器传递的想法
      【解决方案11】:

      在迭代输入维护状态时基本上避免了回调函数。

      请参阅 herehere 了解使用生成器可以完成的工作的概述。

      【讨论】:

        【解决方案12】:

        这里有一些很好的答案,但是,我还建议完整阅读 Python Functional Programming tutorial,它有助于解释一些更有效的生成器用例。

        【讨论】:

          【解决方案13】:

          由于没有提到生成器的发送方法,这里举个例子:

          def test():
              for i in xrange(5):
                  val = yield
                  print(val)
          
          t = test()
          
          # Proceed to 'yield' statement
          next(t)
          
          # Send value to yield
          t.send(1)
          t.send('2')
          t.send([3])
          

          它显示了向正在运行的生成器发送值的可能性。下面视频中关于生成器的更高级课程(包括来自解释的yield、用于并行处理的生成器、逃避递归限制等)

          David Beazley on generators at PyCon 2014

          【讨论】:

            【解决方案14】:

            当我们的网络服务器充当代理时,我使用生成器:

            1. 客户端向服务器请求代理 url
            2. 服务器开始加载目标url
            3. 服务器在得到结果后立即将结果返回给客户端

            【讨论】:

              【解决方案15】:

              一堆东西。任何时候您想生成一系列项目,但又不想一次将它们全部“物化”到一个列表中。例如,您可以有一个返回素数的简单生成器:

              def primes():
                  primes_found = set()
                  primes_found.add(2)
                  yield 2
                  for i in itertools.count(1):
                      candidate = i * 2 + 1
                      if not all(candidate % prime for prime in primes_found):
                          primes_found.add(candidate)
                          yield candidate
              

              然后您可以使用它来生成后续素数的乘积:

              def prime_products():
                  primeiter = primes()
                  prev = primeiter.next()
                  for prime in primeiter:
                      yield prime * prev
                      prev = prime
              

              这些都是相当琐碎的示例,但您可以看到它在处理大型(可能是无限的!)数据集时如何有用,而无需提前生成它们,这只是更明显的用途之一。

              【讨论】:

              • if not any(candidate % prime for primes_found) 应该是 if all(candidate % prime for primes_found)
              • 是的,我的意思是写“如果没有的话(candidate % prime == 0 for prime in primes_found)。不过,你的稍微整洁一些。:)
              • 我猜你忘了从 if not all (candidate % prime for primes_found) 中删除 'not'
              【解决方案16】:

              也适用于打印最多 n 的素数:

              def genprime(n=10):
                  for num in range(3, n+1):
                      for factor in range(2, num):
                          if num%factor == 0:
                              break
                      else:
                          yield(num)
              
              for prime_num in genprime(100):
                  print(prime_num)
              

              【讨论】:

                猜你喜欢
                • 2021-03-18
                • 1970-01-01
                • 2011-06-26
                • 2012-11-17
                • 2020-07-09
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多