我如何知道发电机从一开始是否为空?


146

有没有测试,如果发电机组没有项目,如一个简单的方法peekhasNextisEmpty,类似的规定?


如果我错了,请纠正我,但是如果您可以为任何生成器提供真正通用的解决方案,那将等效于在yield语句上设置断点并具有“后退”功能。这是否意味着在收益上克隆堆栈框架并在StopIteration上恢复它们​​?

好吧,我想是否可以将它们还原为StopIteration,但至少StopIteration会告诉您它为空。是的,我需要睡觉...

4
我想我知道他为什么想要这个。如果您正在使用模板进行Web开发,并将返回值传递到诸如Cheetah之类的模板中,则空列表[]很容易成为Falsey,因此您可以对其进行if检查,并针对某些问题或不进行任何特殊行为。生成器是正确的,即使它们不产生任何元素。
jpsimons

这是我的用例...我正在使用glob.iglob("filepattern")用户提供的通配符模式,如果该模式与任何文件都不匹配,我想警告用户。当然,我可以通过各种方式解决此问题,但是能够干净地测试迭代器是否为空很有用。
LarsH

可以使用以下解决方案:stackoverflow.com/a/11467686/463758
balki 2015年

Answers:


53

您问题的简单答案:不,没有简单的方法。有很多解决方法。

实际上,不应该有一种简单的方法,因为生成器是什么:一种在不将序列保存在内存中的情况下输出值序列的方法。因此,没有向后遍历。

您可以编写has_next函数,甚至可以将其作为带有精美装饰器的方法添加到生成器上。


2
足够公平,这是有道理的。我知道无法找到发电机的长度,但以为我可能会错过寻找它最初是否会产生任何东西的方法。

1
哦,作为参考,我尝试实现自己的“花哨装饰器”建议。硬。显然copy.deepcopy在生成器上不起作用。
David Berger,2009年

47
我不确定我是否同意“不应该采用简单的方法”。在计算机科学中,有许多抽象设计用来输出一个值序列而不将其保存在内存中,但是允许程序员询问是否存在另一个值,而不将其从“队列”中删除。不需要“向后遍历”就可以进行单次偷看。这并不是说迭代器设计必须提供这样的功能,但是肯定有用。也许您是基于偷看之后第一个值可能会发生变化而反对?
LarsH 2013年

9
我反对的理由是,典型的实现甚至在需要它时才计算值。可以强制接口执行此操作,但是对于轻量级实现而言可能不是最佳选择。
David Berger 2013年

6
@ S.Lott您无需生成整个序列即可知道序列是否为空。一个元素的存储价值就足够了-请参阅我的答案。
Mark Ransom 2014年

98

建议:

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.

2
我不太明白在中两次返回第一个元素return first, itertools.chain([first], rest)
njzk2 2014年

6
@ njzk2我要进行“窥视”操作(因此,函数名)。Wiki “窥视是一种返回集合顶部的值而不会从数据中删除值的操作”
John Fouhy 2015年

如果生成器设计为不产生任何结果,则此方法将无效。 def gen(): for pony in range(4): yield None if pony == 2 else pony
保罗

4
@Paul仔细查看返回值。如果生成器完成(即不返回None而是提高)StopIteration,则函数的结果为None。否则,它是一个元组,不是None
基金莫妮卡的诉讼

这对我当前的项目很有帮助。我在python标准库模块'mailbox.py'的代码中找到了一个类似的示例。 This method is for backward compatibility only. def next(self): """Return the next message in a one-time iteration.""" if not hasattr(self, '_onetime_keys'): self._onetime_keys = self.iterkeys() while True: try: return self[next(self._onetime_keys)] except StopIteration: return None except KeyError: continue
同peer

29

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

iterable = some_generator()

_exhausted = object()

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

编辑:更正了mehtunguh注释中指出的问题。


1
否。这对于任何产生的第一个值不正确的生成器都是不正确的。
mehtunguh 2015年

7
使用的object(),而不是class让它一行短:_exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa

13

next(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():
        ...

也就是说,如果您的生成器来自函数,则此方法有效,如中所述generator()


4
为什么这不是最佳答案?万一发电机返回了None
2016年

8
可能是因为这迫使您实际上消耗了生成器,而不仅仅是测试它是否为空。
bfontaine

3
这很糟糕,因为在您呼叫next(generator,None)的那一刻,您将跳过一项(如果有)
Nathan Do

正确,您将错过gen的第一个元素,并且您还将消耗gen而不是测试gen是否为空。
AJ

12

最好的方法,恕我直言,将避免特殊的测试。大多数情况下,使用发电机一种测试:

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!"

3
该解决方案将尝试消耗整个发电机,从而使它无法用于无限发电机。
ViktorStískala'13

@ViktorStískala:我看不到你的意思。测试无限生成器是否产生任何结果将是愚蠢的。
vezult

我想指出,您的解决方案可能会在for循环中包含中断,因为您未在处理其他结果,并且生成这些结果也没有用。range(10000000)是有限生成器(Python 3),但您无需遍历所有项目即可确定它是否生成了某些东西。
维克多·斯特斯卡拉(ViktorStískala)2013年

1
@ViktorStískala:理解了。但是,我的意思是:通常,您实际上要对生成器输出进行操作。在我的示例中,如果什么也没有生成,那么您现在知道了。否则,您将按预期的方式对生成的输出进行操作-“测试使用生成器”。无需特殊测试,也无需消耗发电机输出。我已经编辑了答案以澄清这一点。
vezult 2013年

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)

现在我真的不喜欢这种解决方案,因为我认为这不是生成器的使用方式。


4

我意识到该帖子目前已有5年历史了,但是我在寻找惯用的方法时发现了它,并且没有看到我的解决方案发布。因此,对于后代:

import itertools

def get_generator():
    """
    Returns (bool, generator) where bool is true iff the generator is not empty.
    """
    gen = (i for i in [0, 1, 2, 3, 4])
    a, b = itertools.tee(gen)
    try:
        a.next()
    except StopIteration:
        return (False, b)
    return (True, b)

当然,正如我敢肯定的,很多评论员都会指出,这很hacky,并且仅在某些有限的情况下才起作用(例如,生成器是无副作用的)。YMMV。


1
这只会gen为每个项目调用一次生成器,因此副作用并不是一个太大的问题。但是它将存储已通过b,但不通过a,从生成器中拉出的所有内容的副本,因此内存含义类似于仅运行list(gen)和检查。
马提亚斯·弗里普

它有两个问题。1.此itertool可能需要大量辅助存储(取决于需要存储多少临时数据)。通常,如果一个迭代器在另一个迭代器启动之前使用了大部分或全部数据,则使用list()而不是tee()更快。2. tee迭代器不是线程安全的。当同时使用由相同tee()调用返回的迭代器时,即使原始的Iterable是线程安全的,也可能引发RuntimeError。
AJ

3

很抱歉使用明显的方法,但是最好的方法是:

for item in my_generator:
     print item

现在,您已经检测到生成器在使用时是空的。当然,如果生成器为空,则将永远不会显示项目。

这可能并不完全适合您的代码,但这是生成器的惯用法:迭代,因此也许您可能会稍微改变方法,或者根本不使用生成器。


或者...发问者可以提供一些暗示,为什么有人会尝试检测空发电机?
S.Lott,2009年

您的意思是“由于发生器为空,将不会显示任何内容”?
SilentGhost

洛特 我同意。我不明白为什么。但我认为,即使有原因,也最好改用每个项目。
阿里·阿夫沙尔

1
这不会告诉程序生成器是否为空。
伊桑·弗曼

3

您需要查看生成器是否为空的所有方法是尝试获取下一个结果。当然,如果您还没有准备好使用该结果,则必须将其存储起来,以便以后再次返回。

这是一个包装器类,可以将其添加到现有迭代器中以添加__nonzero__测试,因此您可以使用simple来查看生成器是否为空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'

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


朝着正确的方向前进。应该对其进行修改,以允许尽可能多地窥视,并根据需要存储尽可能多的结果。理想情况下,它将允许将任意项目推到流的顶部。pushable-iterator是我经常使用的非常有用的抽象。
sfkleach

@sfkleach我不认为需要提前多个来使其复杂化,它非常有用,可以回答问题。即使这是一个古老的问题,它仍然会偶尔出现,因此,如果您要留下自己的答案,则有人可能会发现它很有用。
Mark Ransom

马克说的很对,他的解决方案可以回答问题,这是关键所在。我本该说得更好。我的意思是,具有无限推回的pushable-iterator是我发现非常有用的一种习惯用法,而且实现可以说更简单。按照建议,我将发布变体代码。
sfkleach

2

在马克·兰瑟姆(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)

2

刚好落在这个线程上,并意识到缺少一个非常简单易读的答案:

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]

1
>>> gen = (i for i in [])
>>> next(gen)
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    next(gen)
StopIteration

在生成器结尾处StopIteration引发,因为在您的情况下立即到达结尾,所以引发异常。但通常您不应该检查是否存在下一个值。

您可以做的另一件事是:

>>> gen = (i for i in [])
>>> if not list(gen):
    print('empty generator')

2
实际上确实消耗了整个发电机。可悲的是,从这个问题尚不清楚,这是理想的还是不良的行为。
S.Lott

我想,就像“触摸”发电机的任何其他方式一样。
SilentGhost

我意识到这很旧,但是使用'list()'并不是最好的方法,如果生成的列表不是空的,但实际上很大,那么这是不必要的浪费
Chris_Rands

1

如果您使用发电机之前需要知道,那么没有,没有简单的方法。如果您可以等到使用发电机后再使用,则有一种简单的方法:

was_empty = True

for some_item in some_generator:
    was_empty = False
    do_something_with(some_item)

if was_empty:
    handle_already_empty_generator_case()

1

只需用itertools.chain包装生成器,然后将表示可迭代结束的内容作为第二个可迭代,然后进行简单检查即可。

例如:

import itertools

g = some_iterable
eog = object()
wrap_g = itertools.chain(g, [eog])

现在剩下的就是检查我们附加到iterable末尾的值,当您读取它时,它将表示末尾

for value in wrap_g:
    if value == eog: # DING DING! We just found the last element of the iterable
        pass # Do something

使用eog = object()而不是假设它float('-inf')永远不会在迭代中发生。
bfontaine

@bfontaine好主意
smac89'8

1

在我的情况下,我需要先了解是否填充了许多生成器,然后再将其传递给一个函数,该函数合并了各个项,即zip(...)。解决方案与接受的答案相似但足够不同:

定义:

def has_items(iterable):
    try:
        return True, itertools.chain([next(iterable)], iterable)
    except StopIteration:
        return False, []

用法:

def filter_empty(iterables):
    for iterable in iterables:
        itr_has_items, iterable = has_items(iterable)
        if itr_has_items:
            yield iterable


def merge_iterables(iterables):
    populated_iterables = filter_empty(iterables)
    for items in zip(*populated_iterables):
        # Use items for each "slice"

我的特定问题具有以下属性:可迭代项为空或具有完全相同的条目数。


1

我发现只有这种解决方案也可以用于空迭代。

def is_generator_empty(generator):
    a, b = itertools.tee(generator)
    try:
        next(a)
    except StopIteration:
        return True, b
    return False, b

is_empty, generator = is_generator_empty(generator)

或者,如果您不想为此使用异常,请尝试使用

def is_generator_empty(generator):
    a, b = itertools.tee(generator)
    for item in a:
        return False, b
    return True, b

is_empty, generator = is_generator_empty(generator)

标记的解决方案中,您无法将其用于空发生器,例如

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

这是我使用的简单方法,用于在检查是否产生某些结果时继续返回迭代器,而只是检查循环是否运行:

        n = 0
        for key, value in iterator:
            n+=1
            yield key, value
        if n == 0:
            print ("nothing found in iterator)
            break

0

这是一个包装生成器的简单装饰器,因此如果为空,则返回None。如果您的代码需要循环遍历之前知道生成器是否会生成任何东西这将很有用。

def generator_or_none(func):
    """Wrap a generator function, returning None if it's empty. """

    def inner(*args, **kwargs):
        # peek at the first item; return None if it doesn't exist
        try:
            next(func(*args, **kwargs))
        except StopIteration:
            return None

        # return original generator otherwise first item will be missing
        return func(*args, **kwargs)

    return inner

用法:

import random

@generator_or_none
def random_length_generator():
    for i in range(random.randint(0, 10)):
        yield i

gen = random_length_generator()
if gen is None:
    print('Generator is empty')

其中一个有用的示例是在模板代码中-即jinja2

{% if content_generator %}
  <section>
    <h4>Section title</h4>
    {% for item in content_generator %}
      {{ item }}
    {% endfor %
  </section>
{% endif %}

这两次调用了生成器函数,因此将产生两次生成器的启动成本。例如,如果生成器功能是数据库查询,那可能是很重要的。
伊恩·戈德比

0

使用islice,您只需要检查第一次迭代即可发现它是否为空。

从itertools导入islice

def isempty(iterable):
    返回列表(islice(iterable,1))== []


抱歉,这是一本消耗性的读物...必须使用StopIteration进行尝试/捕获
Quin

0

怎么样使用any()?我将其与发电机配合使用,并且工作正常。这里有人解释一下


2
我们不能对所有生成器都使用“ any()”。刚刚尝试将其与包含多个数据帧的生成器一起使用。我收到此消息“ DataFrame的真值不明确。” 在任何(my_generator_of_df)上
probitaille

any(generator)当您知道生成器将生成可以强制转换为的值时,此方法就起作用了bool-基本数据类型(例如,int,string)起作用。any(generator)当生成器为空时,或者当生成器仅具有假值时,它将为False-例如,如果生成器将生成0,''(空字符串)和False,则它仍将为False。只要您知道,这可能是或可能不是预期的行为:)
Daniel

0

在cytoolz中使用偷看功能。

from cytoolz import peek
from typing import Tuple, Iterable

def is_empty_iterator(g: Iterable) -> Tuple[Iterable, bool]:
    try:
        _, g = peek(g)
        return g, False
    except StopIteration:
        return g, True

此函数返回的迭代器将等效于作为参数传入的原始迭代器。


-2

我通过使用sum函数解决了它。请参阅下面的示例,我使用了glob.iglob(它返回一个生成器)。

def isEmpty():
    files = glob.iglob(search)
    if sum(1 for _ in files):
        return True
    return False

*这可能不适用于巨大的生成器,但对于较小的列表应该表现良好

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.