Python:生成器表达式 vs. yield

96

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

使用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)等。

有没有一个更好的选择呢?你有什么想法吗?


2
选择你认为最易读的那个。 - user238424
8个回答

78
这两者只有细微的差别。您可以使用dis模块自行检查此类内容。
编辑:我的第一个版本在交互提示符中反编译了生成器表达式。这与OP在函数内使用的版本略有不同。我已将其修改为与问题中实际情况匹配的版本。
如下所示,“yield”生成器(第一种情况)在设置中有三个额外的指令,但从第一个FOR_ITER开始,它们只有一个方面不同:在循环内部,“yield”方法使用LOAD_FAST代替LOAD_DEREF。相对于LOAD_FASTLOAD_DEREF速度"较慢",因此对于足够大的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

1
已接受 - 感谢使用 dis 进行详细说明的差异。 - cschol
1
我更新了,包括一个链接到一个声称LOAD_DEREF速度“相对较慢”的源代码,所以如果性能真的很重要,用timeit进行一些实际的计时会很好。理论分析只能走得这么远。 - Peter Hansen
这个问题13年后还是有效的吗?或者Python的最新性能改进是否改变了这种行为/速度差异? - undefined
@Paebbels 通过使用dis进行快速检查,可以发现Python 3.10.12中的基本差异与我上面展示的相同。无论LOAD_DEREF是否仍然(或曾经)是“相对较慢”,只有通过实际测量才能得到答案,而实际测量始终取决于主机平台(例如CPU、操作系统)和Python的具体版本。如果这对你很重要,你需要进行测量。 - undefined

37
在这个例子中,其实不需要。但是yield可以用于更复杂的结构-例如它还可以接受来自调用者的值并相应地修改流程。阅读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!")

8
+1 链接到 David Beazley 的演讲。他关于协程的演讲是我很久以来读过的最令人惊叹的东西。也许不像他关于生成器的演讲那么有用,但仍然令人惊叹。 - Robert Rossney

19

对于您可以适合生成器表达式的简单循环类型,没有区别。但是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]

5
+1,这超级酷……我从未见过如此简短而甜美的斐波那契数列实现,并且没有递归。 - JudoWill
貌似简单的代码片段——我认为斐波那契会很高兴看到它!! - user-asterix

11

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

生成器对象只能使用一次,而生成器函数可以每次调用时重新使用,因为它会返回一个全新的生成器对象。

通常情况下,生成器表达式通常是“原始”的,不需要将其包装在一个函数中,并且它们返回一个生成器对象。

例如:

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

8

如果表达式比嵌套循环更为复杂,使用yield是一种不错的选择。此外,您可以返回一个特殊的第一个或最后一个值。例如:

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

8

是的,两者有所不同。

对于生成器表达式(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)

while:

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,它会在__iter__中获取数据,而data = (f(x) for x in qs)可能需要很长时间,而def g(): for x in qs: yield f(x)后面跟着data=g()则会立即返回。
有关详细信息和正式定义,请参见PEP 289 -- Generator Expressions

5

当考虑迭代器时,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)]
>>> 

1
在某些情况下,有一个可能很重要但尚未指出的区别。使用yield会阻止您将return用于除隐式引发StopIteration(和相关协程)之外的其他内容。
这意味着这段代码格式不正确(将其提供给解释器将会产生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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接