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

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个回答

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)

当然,我相信许多评论员会指出,这种方法很粗糙,并且只适用于某些有限的情况(例如生成器是无副作用的)。结果因人而异。

4
这将仅针对每个项目调用gen生成器一次,因此副作用不是太严重的问题。但它将存储通过b而非通过a从生成器中提取的所有内容的副本,因此内存影响类似于只运行list(gen)并进行检查。 - Matthias Fripp
它有两个问题。
  1. 这个itertool可能需要大量的辅助存储(取决于需要存储多少临时数据)。通常,如果一个迭代器在另一个迭代器开始之前使用了大部分或全部数据,则使用list()而不是tee()更快。
  2. tee迭代器不是线程安全的。即使原始可迭代对象是线程安全的,在同时使用由同一tee()调用返回的迭代器时,也可能会引发RuntimeError异常。
- A.J.

4
我发现只有这种解决方案能够在空迭代中起作用。
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()

1
很好。这应该是被接受的答案。 - undefined

3
>>> 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
1
我知道这是老旧的,但使用'list()'肯定不是最好的方法,如果生成的列表不是空的,而实际上很大,那么这是不必要的浪费。 - Chris_Rands

1
使用cytoolz中的peek函数。
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

这个函数返回的迭代器将与作为参数传递的原始迭代器相等。

1

只需使用 itertools.chain 将生成器包装起来,将表示可迭代对象结尾的东西作为第二个可迭代对象,然后简单地检查即可。

例如:

import itertools

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

现在我们只需检查迭代器末尾所附加的值,当读取该值时,表示已到达末尾。
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

1

为了提供我的"2分钱"帮助,我将描述我的经验:

我有一个生成器,我需要使用itertools.islice对其进行切片,以生成小的生成器。然后,为了检查我的子生成器是否为空,我只需将它们转换/消耗成一个小列表,并检查该列表是否为空。

例如:

from itertools import islice

def generator(max_yield=10):
    a = 0

    while True:
        a += 1

        if a > max_yield:
            raise StopIteration()

        yield a

tg = generator()

label = 1

while True:
    itg = list(islice(tg, 3))

    if not itg:  # <-- I check if the list is empty or not
        break

    for i in itg:
        print(f'#{label} - {i}')

    label += 1

输出:

#1 - 1
#1 - 2
#1 - 3
#2 - 4
#2 - 5
#2 - 6
#3 - 7
#3 - 8
#3 - 9
#4 - 10

也许这不是最好的方法,主要是因为它会消耗生成器,但对我来说它有效。

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
在迭代之前检查生成器是否符合 LBYL 编码风格。另一种方法 (EAFP) 是先迭代它,然后检查它是否为空。
is_empty = True

for item in generator:
    is_empty = False
    do_something(item)

if is_empty:
    print('Generator is empty')

这种方法也很好地处理了无限生成器。

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"

我的问题特殊之处在于可迭代对象要么为空,要么具有完全相同数量的条目。


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 %}

这会调用生成器函数两次,因此将两次产生生成器的启动成本。如果生成器函数是数据库查询,那么这可能是相当大的开销。 - Ian Goldby

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