【问题标题】:Python: generator expression vs. yieldPython:生成器表达式与产量
【发布时间】:2011-01-01 00:17:20
【问题描述】:

在 Python 中,通过 生成器表达式 与使用 yield 语句创建生成器对象有什么区别?

使用产量

def Generator(x, y):
    for i in xrange(x):
        for j in xrange(y):
            yield(i, j)

使用生成器表达式

def Generator(x, y):
    return ((i, j) for i in xrange(x) for j in xrange(y))

这两个函数都返回生成元组的生成器对象,例如(0,0), (0,1) 等

其中一个有什么优点吗?想法?

【问题讨论】:

  • 选择你认为最易读的那个。

标签: python python-3.x generator yield


【解决方案1】:

两者只有细微的差别。您可以使用dis 模块自己检查这类事情。

编辑:我的第一个版本在交互式提示中反编译了在模块范围内创建的生成器表达式。这与在函数内部使用的 OP 版本略有不同。我已对此进行了修改以匹配问题中的实际情况。

正如您在下面看到的,“yield”生成器(第一种情况)在设置中有三个额外的指令,但与第一个 FOR_ITER 相比,它们仅在一个方面有所不同:“yield”方法使用 LOAD_FAST代替循环内的LOAD_DEREFLOAD_DEREF"rather slower" 而不是 LOAD_FAST,因此对于足够大的 x 值(外循环),它使“yield”版本比生成器表达式稍快,因为 y 的值被轻微加载每次通过都更快。对于较小的 x 值,由于设置代码的额外开销,它会稍微慢一些。

可能还值得指出的是,生成器表达式通常会在代码中内联使用,而不是像这样用函数包装它。即使LOAD_FAST 为“yield”版本提供了优势,这也将消除一些设置开销,并使生成器表达式对于较小的循环值保持稍快。

在这两种情况下,性能差异都不足以证明在两者之间做出决定。可读性要重要得多,所以对于手头的情况,使用最易读的那个。

>>> def Generator(x, y):
...     for i in xrange(x):
...         for j in xrange(y):
...             yield(i, j)
...
>>> dis.dis(Generator)
  2           0 SETUP_LOOP              54 (to 57)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_FAST                0 (x)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                40 (to 56)
             16 STORE_FAST               2 (i)

  3          19 SETUP_LOOP              31 (to 53)
             22 LOAD_GLOBAL              0 (xrange)
             25 LOAD_FAST                1 (y)
             28 CALL_FUNCTION            1
             31 GET_ITER
        >>   32 FOR_ITER                17 (to 52)
             35 STORE_FAST               3 (j)

  4          38 LOAD_FAST                2 (i)
             41 LOAD_FAST                3 (j)
             44 BUILD_TUPLE              2
             47 YIELD_VALUE
             48 POP_TOP
             49 JUMP_ABSOLUTE           32
        >>   52 POP_BLOCK
        >>   53 JUMP_ABSOLUTE           13
        >>   56 POP_BLOCK
        >>   57 LOAD_CONST               0 (None)
             60 RETURN_VALUE
>>> def Generator_expr(x, y):
...    return ((i, j) for i in xrange(x) for j in xrange(y))
...
>>> dis.dis(Generator_expr.func_code.co_consts[1])
  2           0 SETUP_LOOP              47 (to 50)
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                40 (to 49)
              9 STORE_FAST               1 (i)
             12 SETUP_LOOP              31 (to 46)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_DEREF               0 (y)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                17 (to 45)
             28 STORE_FAST               2 (j)
             31 LOAD_FAST                1 (i)
             34 LOAD_FAST                2 (j)
             37 BUILD_TUPLE              2
             40 YIELD_VALUE
             41 POP_TOP
             42 JUMP_ABSOLUTE           25
        >>   45 POP_BLOCK
        >>   46 JUMP_ABSOLUTE            6
        >>   49 POP_BLOCK
        >>   50 LOAD_CONST               0 (None)
             53 RETURN_VALUE

【讨论】:

  • 已接受 - 用于详细解释使用 dis 的差异。谢谢!
  • 我更新为包含一个指向声称LOAD_DEREF“相当慢”的源的链接,所以如果性能真的很重要,timeit 的一些实际时间会很好。理论分析只到此为止。
【解决方案2】:

在这个例子中,并非如此。但是yield 可以用于更复杂的构造 - for example 它也可以接受来自调用者的值并因此修改流程。阅读PEP 342 了解更多详情(这是一个值得了解的有趣技术)。

无论如何,最好的建议是根据您的需要使用更清晰的方法

附:这是来自Dave Beazley的一个简单的协程示例:

def grep(pattern):
    print "Looking for %s" % pattern
    while True:
        line = (yield)
        if pattern in line:
            print line,

# Example use
if __name__ == '__main__':
    g = grep("python")
    g.next()
    g.send("Yeah, but no, but yeah, but no")
    g.send("A series of tubes")
    g.send("python generators rock!")

【讨论】:

  • +1 用于链接到 David Beazley。他关于协程的演讲是我很长时间以来读过的最令人兴奋的东西。也许不如他关于发电机的演讲有用,但仍然令人惊叹。
【解决方案3】:

对于可以放入生成器表达式的简单循环类型没有区别。然而,yield 可用于创建执行更复杂处理的生成器。下面是一个生成斐波那契数列的简单示例:

>>> def fibgen():
...    a = b = 1
...    while True:
...        yield a
...        a, b = b, a+b

>>> list(itertools.takewhile((lambda x: x<100), fibgen()))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

【讨论】:

  • +1 太酷了...不能说我见过如此短而甜美的不带递归的 fib 实现。
  • 看似简单的代码 sn-p - 我认为斐波那契会很高兴看到它!
【解决方案4】:

在使用中,请注意生成器对象与生成器函数之间的区别。

生成器对象只能使用一次,与生成器函数不同,生成器函数可以在您每次再次调用它时重复使用,因为它返回一个新的生成器对象。

生成器表达式在实践中通常使用“原始”,没有将它们包装在函数中,它们返回一个生成器对象。

例如:

def range_10_gen_func():
    x = 0
    while x < 10:
        yield x
        x = x + 1

print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))

哪个输出:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

与稍微不同的用法进行比较:

range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))

哪个输出:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

并与生成器表达式进行比较:

range_10_gen_expr = (x for x in range(10))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))

也输出:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

【讨论】:

    【解决方案5】:

    如果表达式比嵌套循环更复杂,使用yield 会很好。除其他外,您可以返回特殊的第一个或特殊的最后一个值。考虑:

    def Generator(x):
      for i in xrange(x):
        yield(i)
      yield(None)
    

    【讨论】:

      【解决方案6】:

      是的,有区别。

      对于生成器表达式(x for var in expr)iter(expr) 在表达式创建时被调用。

      使用defyield创建生成器时,如:

      def my_generator():
          for var in expr:
              yield x
      
      g = my_generator()
      

      iter(expr) 尚未调用。只有在 g 上迭代时才会调用它(并且可能根本不会被调用)。

      以这个迭代器为例:

      from __future__ import print_function
      
      
      class CountDown(object):
          def __init__(self, n):
              self.n = n
      
          def __iter__(self):
              print("ITER")
              return self
      
          def __next__(self):
              if self.n == 0:
                  raise StopIteration()
              self.n -= 1
              return self.n
      
          next = __next__  # for python2
      

      这段代码:

      g1 = (i ** 2 for i in CountDown(3))  # immediately prints "ITER"
      print("Go!")
      for x in g1:
          print(x)
      

      同时:

      def my_generator():
          for i in CountDown(3):
              yield i ** 2
      
      
      g2 = my_generator()
      print("Go!")
      for x in g2:  # "ITER" is only printed here
          print(x)
      

      由于大多数迭代器不会在__iter__ 中做很多事情,因此很容易错过这种行为。一个真实的例子是 Django 的QuerySet,其中fetch data in __iter__data = (f(x) for x in qs) 可能需要很长时间,而def g(): for x in qs: yield f(x) 后跟data=g() 会立即返回。

      更多信息和正式定义请参考PEP 289 -- Generator Expressions

      【讨论】:

        【解决方案7】:

        在考虑迭代器时,itertools 模块:

        ... 标准化一组核心的快速、高效的内存工具,这些工具本身或组合使用。它们共同构成了一个“迭代器代数”,使得在纯 Python 中简洁高效地构建专用工具成为可能。

        为了性能,考虑itertools.product(*iterables[, repeat])

        输入迭代的笛卡尔积。

        相当于生成器表达式中的嵌套 for 循环。例如,product(A, B) 返回的结果与 ((x,y) for x in A for y in B) 相同。

        >>> import itertools
        >>> def gen(x,y):
        ...     return itertools.product(xrange(x),xrange(y))
        ... 
        >>> [t for t in gen(3,2)]
        [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
        >>> 
        

        【讨论】:

          【解决方案8】:

          在某些尚未指出的情况下,存在可能很重要的差异。使用yield 可以防止您将return 用于implicitly raising StopIteration (and coroutines related stuff) 以外的其他内容。

          这意味着这段代码格式错误(将其提供给解释器会给你一个AttributeError):

          class Tea:
          
              """With a cloud of milk, please"""
          
              def __init__(self, temperature):
                  self.temperature = temperature
          
          def mary_poppins_purse(tea_time=False):
              """I would like to make one thing clear: I never explain anything."""
              if tea_time:
                  return Tea(355)
              else:
                  for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']:
                      yield item
          
          print(mary_poppins_purse(True).temperature)
          

          另一方面,这段代码就像一个魅力:

          class Tea:
          
              """With a cloud of milk, please"""
          
              def __init__(self, temperature):
                  self.temperature = temperature
          
          def mary_poppins_purse(tea_time=False):
              """I would like to make one thing clear: I never explain anything."""
              if tea_time:
                  return Tea(355)
              else:
                  return (item for item in ['lamp', 'mirror', 'coat rack',
                                            'tape measure', 'ficus'])
          
          print(mary_poppins_purse(True).temperature)
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-05-02
            • 2013-05-22
            • 2017-12-08
            • 2014-04-02
            • 2021-12-27
            • 2014-07-19
            • 2021-10-17
            • 2021-04-26
            相关资源
            最近更新 更多