如何定义一个空的生成器函数?

147

一个生成器函数可以通过在函数体中放置yield关键字来定义:

def gen():
    for i in range(10):
        yield i

如何定义一个空的生成器函数?

以下代码无法工作,因为Python不能知道它应该是一个生成器函数而不是普通函数:

def empty():
    pass

我可以做这样的事情:

def empty():
    if False:
        yield

但是那样会很丑陋,有没有更好的方法呢?

10个回答

195
你可以在生成器中使用一次return,它会停止迭代但不产生任何值,因此提供了一个明确的替代方案来避免函数超出范围。因此,请使用yield将函数转换为生成器,但在产生任何值之前加上return以终止生成器。
>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

我不确定它比你现有的要好多少——它只是用一个无操作的yield语句替换了一个无操作的if语句。但它更符合惯用法。请注意,仅使用yield是行不通的。
>>> def f():
...     yield
... 
>>> list(f())
[None]

为什么不直接使用iter(())

这个问题特别在询问一个空的生成器函数。因此,我认为这是关于Python语法内部一致性的问题,而不是关于一般情况下创建空迭代器的最佳方式。

如果问题实际上是关于创建空迭代器的最佳方式,那么你可能会同意Zectbumo关于使用iter(())的观点。然而,重要的是要注意iter(())并没有返回一个函数!它直接返回一个空的可迭代对象。假设你正在使用一个期望每次调用时返回一个可迭代对象的可调用API,就像普通的生成器函数一样。你需要像这样做:

def empty():
    return iter(())

(Credit应归功于Unutbu,他给出了这个答案的第一个正确版本。)
现在,您可能会发现以上内容更清晰,但我可以想象有些情况下它可能不太清晰。考虑下面这个(人为制造的)生成器函数定义的长列表的例子:
def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

在那个漫长的列表结尾处,我更希望看到一些包含 yield 的东西,就像这样:
def empty():
    return
    yield

或者,在Python 3.3及以上版本中(如DSM所建议的),使用以下代码:
def empty():
    yield from ()

有了yield关键字,仅仅一眼就能清楚地看出这只是另一个生成器函数,与所有其他函数完全相同。需要更多时间才能看出iter(())版本正在执行相同的操作。

这是微妙的差别,但我真诚地认为基于yield的函数更易读和可维护。

另请参见user3840170的这个很棒的答案,它使用dis来显示为什么这种方法更可取:编译时发出最少的指令。


1
呃,return 后面有什么?我期望看到像 itertools.empty() 这样的东西。 - Grault
1
@Jesdisciple,嗯,在生成器内部,“return”意味着不同的东西。它更像是“break”。 - senderle
我喜欢这个解决方案,因为它相对简洁,并且不会做任何额外的工作,比如与“False”进行比较。 - Pi Marillion
Unutbu的答案并不是你提到的“真正的生成器函数”,因为它返回一个迭代器。 - Zectbumo
在函数前面可能需要添加 #pylint: disable = unreachable - OrenIshShalom
显示剩余2条评论

78
iter(())

你不需要一个发电机。加油伙计们!


4
我最喜欢这个答案。它快速简便易写,执行也很快。相比于使用iter([]),更吸引我的是()是一个常量,而每次调用[]时可能会在内存中实例化一个新的列表对象。 - Mumbleskates
2
回顾这个线程,我觉得有必要指出,如果你想要一个真正的生成器函数替代方案,你需要编写类似于 empty = lambda: iter(()) 或者 def empty(): return iter(()) 的代码。 - senderle
1
如果你必须使用生成器,那么你可以像其他人建议的那样使用(_ for _ in ())。 - Zectbumo
3
@Zectbumo,那仍然不是一个生成器函数。它只是一个生成器。生成器函数每次被调用时都会返回一个新的生成器。 - senderle
我确实是来找一个生成器的。我正在尝试将输出附加到os.walk()生成器上。所以,是的,我确实需要一个生成器。 - ingyhere
2
这将返回一个 tuple_iterator 而不是一个 generator。如果你的生成器需要返回空值,不要使用这个答案。 - user3064538

74

我选择 Python 3.3 (因为我对 yield from 感兴趣,而且 @senderle 抢走了我的第一个想法):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

但我不得不承认,我很难想出一个使用案例来证明 iter([]) 或者 (x)range(0) 无法同样有效地发挥作用。


4
我认为这种写法对于初学者来说比return; yield或者if False: yield None更易懂。 - abarnert
1
但我必须承认,我很难想出一个使用案例,其中iter([])(x)range(0)同样适用。不确定(x)range(0)是什么,但一个使用案例可以是一个方法,该方法旨在被一些继承类中的完整生成器覆盖。为了一致性,您希望即使是其他人继承的基本方法也返回生成器,就像覆盖它的那些方法一样。 - Vedran Šego

27

另一个选项是:

(_ for _ in ())

2
与其他选项不同,Pycharm认为这与用于生成器的标准类型提示是一致的,例如Generator[str, Any, None] - Michał Jabłoński

19

@senderle说的那样,使用这个:

def empty():
    return
    yield

我写这篇答案主要是为了分享另一个理由。

选择这种解决方案的原因之一是它从解释器的角度来看是最优的。

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

正如我们所看到的,empty_return与常规的空函数具有完全相同的字节码;其余函数执行一些不会改变行为的其他操作。 empty_returnnoop之间唯一的区别在于前者设置了生成器标志:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

上述反汇编在CPython 3.11已经过时,但empty_return仍然是最优的,与一个空操作函数相比只多了两个操作码(四个字节):

>>> dis.dis(empty_yield_from)
  1           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  2           6 LOAD_CONST               1 (())
              8 GET_YIELD_FROM_ITER
             10 LOAD_CONST               0 (None)
        >>   12 SEND                     3 (to 20)
             14 YIELD_VALUE
             16 RESUME                   2
             18 JUMP_BACKWARD_NO_INTERRUPT     4 (to 12)
        >>   20 POP_TOP
             22 LOAD_CONST               0 (None)
             24 RETURN_VALUE
>>> dis.dis(empty_iter)
  1           0 RESUME                   0

  2           2 LOAD_GLOBAL              1 (NULL + iter)
             14 LOAD_CONST               1 (())
             16 PRECALL                  1
             20 CALL                     1
             30 RETURN_VALUE
>>> dis.dis(empty_return)
  1           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  2           6 LOAD_CONST               0 (None)
              8 RETURN_VALUE
>>> dis.dis(noop)
  1           0 RESUME                   0

  2           2 LOAD_CONST               0 (None)
              4 RETURN_VALUE
当然,这个论点的强度非常依赖于Python在使用中的具体实现;一个足够聪明的替代解释器可能会注意到其他操作都没有用,并将它们优化掉。但是,即使存在这样的优化,也需要解释器花时间执行它们并保护免受优化假设的破坏,比如全局作用域中的iter标识符被重新绑定到其他东西(即使这实际上很可能表示一个错误) 。对于empty_return来说,根本没有什么可以优化的,因为在return语句后,字节码生成就停止了,所以即使是相对幼稚的CPython也不会浪费时间在任何虚假操作上。

哦,好的。你能加上 yield from () 的结果吗?(参见 DSM 的回答。) - senderle
1
很棒的答案。有点遗憾的是,“yield from ()”生成的代码不太优化,因为它的源代码看起来最直观和清晰。 - Dmitriy Sintsov

4
必须使用生成器函数吗?如果不是,那么怎么样呢?
def empty():
    return iter(())

或者更简单直接。
empty = ().__iter__

在某些情况下,使用 partial(iter, ()) (from functools import partial) 或 lambda: iter(()) 可能更可取。 - mtraceur

3

我想给出一个基于类的例子,因为我们还没有提出任何建议。 这是一个可调用的迭代器,不生成任何项。 我相信这是解决问题的一种简单明了的描述性方法。

class EmptyGenerator:
    def __iter__(self):
        return self
    def __next__(self):
        raise StopIteration

>>> list(EmptyGenerator())
[]

你能否添加一些解释说明为什么/如何使用这个方法来解决OP的问题? - SherylHohman
请不要仅仅发布代码作为答案,还要提供解释您的代码是如何解决问题的。带有解释的答案通常更有帮助和更高质量,并且更有可能吸引赞同。 - SherylHohman

2

创建一个空迭代器的“标准”方法似乎是iter([])。 我曾建议将[]作为iter()的默认参数;但这个建议被拒绝了,原因充分,详情请见http://bugs.python.org/issue25215 - Jurjen


2

还没有人提到,但是调用没有参数的内置函数zip会返回一个空迭代器:

>>> it = zip()
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

1
generator = (item for item in [])

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