如何判断一个生成器是否从一开始就为空?

235

有没有一种简单的方法来测试生成器是否没有任何项,例如peekhasNextisEmpty等类似方法?


请纠正我,但如果您能够为任何生成器创建一个真正通用的解决方案,那么它将相当于在yield语句上设置断点并具有“向后步进”的能力。这是否意味着在yield时克隆堆栈帧并在StopIteration上恢复它们? - user44484
好吧,我猜恢复它们StopIteration或不恢复,但至少StopIteration会告诉你它是空的。是啊,我需要睡觉了... - user44484
4
我想我知道他为什么想要这样做。如果你使用模板进行web开发,并将返回值传递到像 Cheetah 之类的模板中,那么空列表 [] 是方便的 Falsey 值,所以你可以对其进行 if 检查,并为某些特殊情况或无内容设置特殊行为。即使生成器没有生成任何元素,它们也是真实的。 - jpsimons
1
这是我的使用案例...我正在使用glob.iglob("filepattern")来处理用户提供的通配符模式,并且如果该模式没有匹配到任何文件,我想要警告用户。当然,我可以通过各种方式解决这个问题,但是能够干净地测试迭代器是否为空是很有用的。 - LarsH
可以尝试使用这个解决方案:https://dev59.com/MGgu5IYBdhLWcg3wP01m#11467686 - balki
显示剩余2条评论
25个回答

146
建议:
def peek(iterable):
    try:
        first = next(iterable)
    except StopIteration:
        return None
    return first, itertools.chain([first], iterable)

用法:

res = peek(mysequence)
if res is None:
    # sequence is empty.  Do stuff.
else:
    first, mysequence = res
    # Do something with first, maybe?
    # Then iterate over the sequence:
    for element in mysequence:
        # etc.

3
return first, itertools.chain([first], rest)这行代码中,我不太明白为什么要将第一个元素返回两次。 - njzk2
11
@njzk2 我正在进行一个“窥视”操作(因此函数名称)。维基百科 “窥视是一种操作,它返回集合顶部的值,而不从数据中删除该值”。 - John Fouhy
2
如果生成器旨在产生None,则这不起作用。def gen(): for pony in range(4): yield None if pony == 2 else pony - Paul
9
仔细看返回值。如果生成器完成了——即不返回None,而是引发StopIteration——函数的结果为None。否则,它是一个元组,不是None - anon
2
大量的peek调用会不会创建一个无休止的itertools.chain对象链,其中包含对其他itertools.chain对象的引用? - Mateen Ulhaq
显示剩余3条评论

78

对于你的问题,简单回答:没有简单的方法。有很多变通方法。

实际上,不应该有简单的方法,因为生成器的作用是输出一个值序列而无需在内存中保存整个序列。所以无法进行反向遍历。

如果需要,您可以编写has_next函数,或者甚至通过使用装饰器将其附加到生成器作为方法。


2
很公平,那很有道理。我知道没有办法找到生成器的长度,但我想可能会错过一种判断它是否会生成任何内容的方法。 - Dan
94
我不确定能否同意“不应该有简单的方法”的说法。在计算机科学中,有许多抽象概念被设计为在不将序列存储在内存中的情况下输出序列的值,并允许程序员询问是否有另一个值,而不需要从“队列”中删除它。存在这样的单个预读取功能,而不需要“向后遍历”。这并不意味着迭代器设计必须提供这样的功能,但它确实很有用。也许你反对的是第一个值在预读后可能会改变的情况? - LarsH
12
我反对的理由是,典型的实现直到需要计算值时才进行计算。虽然可以强制接口这样做,但对于轻量级实现来说可能不是最优选择。 - David Berger
11
@S.Lott,你不需要生成整个序列来确定序列是否为空。一个元素的存储空间就足够了-请参考我的回答。 - Mark Ransom
2
描述过于复杂,而且没有可见的解决方案,即使获得了55个赞!!! - Apostolos
显示剩余7条评论

49

一个简单的方法是使用 next() 的可选参数,如果生成器已经用尽(或为空),则使用该参数。例如:

_exhausted  = object()

if next(some_generator, _exhausted) is _exhausted:
    print('generator is empty')

9
因为next(iter([-1, -2, -3]), -1) == -1是正确的。换句话说,任何第一个元素等于-1的可迭代对象都将在使用您的条件时显示为空。 - Jeyekomon
2
@Apostolos 在简单情况下,是的,那就是解决方案。但是如果您计划创建一个没有约束条件的通用可迭代工具,则会失败。 - Jeyekomon
3
@Apostolos object()是一个非常特殊的值,它不会被包含在生成器中。 - Mikko Koho
11
注意:这仍然是一个“窥视”函数,并将从生成器中取出一个元素。 - phlaxyr
1
编辑以检查身份(由于“_exhausted”是一个对象,因此这相当于==但更快) - Mr_and_Mrs_D
显示剩余5条评论

31

快速脏解决方案:

next(my_generator(), None) is not None

或者将 None 替换为您知道在生成器中不存在的任何值。

编辑:是的,这将跳过生成器中的1个项目。但有时,我仅出于验证目的检查生成器是否为空,然后不会真正使用它。否则,我会做类似以下的事情:

def foo(self):
    if next(self.my_generator(), None) is None:
        raise Exception("Not initiated")

    for x in self.my_generator():
        ...

那就是说,只有当你的生成器来自于一个函数,例如my_generator()时,这个方法才会生效。

5
如果生成器返回“None”,为什么这不是最佳答案? - Sait
11
可能是因为这样强制你实际消耗生成器而不仅仅测试它是否为空。 - bfontaine
6
因为当你调用next(generator, None)的时候,如果有一个可用的项,你将会跳过一个项,所以这是不好的。 - Nathan Do
2
正确,您将会错过您的生成器的第一个元素,而且您也将耗尽您的生成器,而不是测试它是否为空。 - A.J.
这不是通用解决方案,因为它仅适用于那些我们事先知道生成器永远不会返回的值的生成器,例如None - Mikko Koho
1
@MikkoKoho,原始问题(或者至少是截至本评论时的“当前原始问题”)要求的是_一个_发电机,而不是_所有_发电机。这个答案适用于所有可能的发电机的一个子集,并且有一个适当的警告,使其适用于个人在实践中使用的_所有_发电机。 - undefined

23

在我看来,最好的方法是避免特殊的测试。大多数情况下,使用生成器本身就是一种测试:

thing_generated = False

# Nothing is lost here. if nothing is generated, 
# the for block is not executed. Often, that's the only check
# you need to do. This can be done in the course of doing
# the work you wanted to do anyway on the generated output.
for thing in my_generator():
    thing_generated = True
    do_work(thing)

如果那还不够好,您仍然可以执行显式测试。此时,thing将包含生成的最后一个值。如果没有生成任何内容,则它将是未定义的 - 除非您已经定义了该变量。您可以检查thing的值,但这有点不可靠。相反,在块内设置一个标志,然后在之后检查它:

if not thing_generated:
    print "Avast, ye scurvy dog!"

4
此解决方案将尝试消耗整个生成器,因此使其对于无限生成器不可用。 - Viktor Stískala
2
@ViktorStískala:我不明白你的意思。测试无限生成器是否产生任何结果是愚蠢的。 - vezult
我想指出你的解决方案可能在 for 循环中包含 break,因为你没有处理其他结果,而它们生成是无用的。range(10000000) 是一个有限生成器(Python 3),但你不需要遍历所有项才能找出它是否会生成某些东西。 - Viktor Stískala
3
@ViktorStískala:明白了。然而,我的观点是:通常情况下,您实际上希望对生成器输出进行操作。在我的示例中,如果没有生成任何内容,那么现在您就知道了。否则,您将按预期对生成的输出进行操作-“使用生成器就是测试”。不需要特殊测试或毫无意义地消耗生成器输出。我已经编辑了我的答案以澄清这一点。 - vezult

9
由Mark Ransom提出,这里有一个可以包装任何迭代器的类,您可以使用它来预览,将值推回流中并检查是否为空。这是一个简单的想法,有着简单的实现,过去我发现它非常方便。
class Pushable:

    def __init__(self, iter):
        self.source = iter
        self.stored = []

    def __iter__(self):
        return self

    def __bool__(self):
        if self.stored:
            return True
        try:
            self.stored.append(next(self.source))
        except StopIteration:
            return False
        return True

    def push(self, value):
        self.stored.append(value)

    def peek(self):
        if self.stored:
            return self.stored[-1]
        value = next(self.source)
        self.stored.append(value)
        return value

    def __next__(self):
        if self.stored:
            return self.stored.pop()
        return next(self.source)

更新:我认为将其转化为PyPI包是值得的,因为我过去已经使用了它很多次。请在此处找到稍微详细一些的版本-https://pypi.org/project/pushable/ - sfkleach

9

我刚看到这个帖子,发现一个非常简单易懂的答案还未出现:

def is_empty(generator):
    for item in generator:
        return False
    return True

如果我们不应该消耗任何项目,那么我们需要将第一个项目重新注入生成器中:
def is_empty_no_side_effects(generator):
    try:
        item = next(generator)
        def my_generator():
            yield item
            yield from generator
        return my_generator(), False
    except StopIteration:
        return (_ for _ in []), True

例子:

>>> g=(i for i in [])
>>> g,empty=is_empty_no_side_effects(g)
>>> empty
True
>>> g=(i for i in range(10))
>>> g,empty=is_empty_no_side_effects(g)
>>> empty
False
>>> list(g)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

8

我不太愿意提供第二种解决方案,特别是一种我自己不会使用的方案,但是如果你绝对必须这样做,并且不想像其他答案那样消耗生成器:

def do_something_with_item(item):
    print item

empty_marker = object()

try:
     first_item = my_generator.next()     
except StopIteration:
     print 'The generator was empty'
     first_item = empty_marker

if first_item is not empty_marker:
    do_something_with_item(first_item)
    for item in my_generator:
        do_something_with_item(item)

我真的不喜欢这个解决方案,因为我认为这不是生成器的正确使用方式。


6

要检查一个生成器是否为空,您只需要尝试获取下一个结果。当然,如果您还没有准备好使用该结果,则必须将其存储以便稍后返回。

以下是一个包装器类,可以添加到现有迭代器中以添加一个__nonzero__测试,因此您可以使用简单的if语句查看生成器是否为空。它可能也可以转换成一个装饰器。

class GenWrapper:
    def __init__(self, iter):
        self.source = iter
        self.stored = False

    def __iter__(self):
        return self

    def __nonzero__(self):
        if self.stored:
            return True
        try:
            self.value = next(self.source)
            self.stored = True
        except StopIteration:
            return False
        return True

    def __next__(self):  # use "next" (without underscores) for Python 2.x
        if self.stored:
            self.stored = False
            return self.value
        return next(self.source)

以下是如何使用它的方法:

with open(filename, 'r') as f:
    f = GenWrapper(f)
    if f:
        print 'Not empty'
    else:
        print 'Empty'

请注意,您可以随时检查是否为空,而不仅仅是在迭代开始时。

这是朝着正确的方向前进。它应该被修改为允许向前预览任意数量的数据,并存储所需的所有结果。理想情况下,它将允许推送任意项目到流的头部。可推送迭代器是一种非常有用的抽象,我经常使用。 - sfkleach
1
@sfkleach 我认为没有必要为了多个预览而使这个问题变得复杂,它已经很有用并且回答了这个问题。虽然这是一个旧问题,但仍然会偶尔被查看,所以如果你想留下自己的答案,可能会有人觉得有用。 - Mark Ransom
马克说得很对,他的解决方案回答了问题,这是关键点。我应该表达得更好。我的意思是,具有无限推回的可推送迭代器是我发现非常有用的习语,而且实现甚至更简单。如建议所示,我将发布变体代码。 - sfkleach

5

抱歉这种方法显而易见,但是最好的方法是这样做:

for item in my_generator:
     print item

现在您检测到在使用生成器时它为空。当然,如果生成器是空的话,就不会显示任何项目。

这可能不完全符合您的代码,但这就是生成器习语的作用:通过迭代来实现,因此也许您需要稍微改变一下自己的方法,或者干脆不使用生成器。


或者...提问者可以给出一些提示,为什么要尝试检测空生成器? - S.Lott
你的意思是“由于生成器为空,将不会显示任何内容”吗? - SilentGhost
S.Lott。我同意。我不明白为什么。但是我认为即使有原因,问题最好转而使用每个项目。 - Ali Afshar
4
这并没有告诉程序生成器是否为空。 - Ethan Furman

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