Как я узнаю, что генератор пуст с самого начала?

есть ли простой способ тестирования, если генератор не имеет элементов, таких как peek, hasNext, isEmpty, что-то в этом роде?

20 ответов


простой ответ на ваш вопрос: нет, нет простого способа. Есть много обходных путей.

на самом деле не должно быть простого способа, из-за того, что генераторы: способ вывода последовательности значений не удерживая последовательность в памяти. Так что обратного хода нет.

вы можете написать функцию has_next или, возможно, даже шлепнуть ее на генератор в качестве метода с причудливым декоратором, если хотите.


предложение:

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.

простой способ-использовать необязательный параметр для next () который используется, если генератор исчерпан (или пуст). Например:

iterable = some_generator()

_exhausted = object()

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

Edit: Исправлена проблема, указанная в комментарии мехтунгуха.


лучшим подходом, ИМХО, было бы избежать специального теста. В большинстве случаев, использование генератора is тест:

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

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


Я ненавижу предлагать второе решение, особенно то, которое я бы не использовал сам, но, если вы абсолютно had сделать это и не потреблять генератор, как в других ответах:

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)

теперь мне очень не нравится это решение, потому что я считаю, что это не так, как Генераторы будут использоваться.


извините за очевидный подход, но лучшим способом будет сделать:

for item in my_generator:
     print item

теперь вы обнаружили, что генератор пуст, пока вы его используете. Конечно, элемент никогда не будет отображаться, если генератор пуст.

Это может не совсем соответствовать вашему коду, но это то, для чего предназначена идиома генератора: итерация, поэтому, возможно, вы можете немного изменить свой подход или вообще не использовать генераторы.


Я понимаю, что на данный момент этому сообщению 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)

конечно, как я уверен, многие комментаторы укажут, это хакерский и работает только в определенных ограниченных ситуациях (где генераторы свободны от побочных эффектов, например). YMMV.


все, что вам нужно сделать, чтобы увидеть, если генератор пуста, чтобы попытаться получить следующий результат. Конечно, если вы не готов чтобы использовать этот результат, вы должны сохранить его, чтобы вернуть его позже.

вот класс-оболочка, который можно добавить в существующий итератор, чтобы добавить __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'

обратите внимание, что вы можете проверить на пустоты в любое время, а не только в начале итерации.


>>> 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')

в моем случае мне нужно было знать, было ли заполнено множество генераторов, прежде чем я передал его функции, которая объединила элементы, т. е. 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"

моя конкретная проблема обладает тем свойством, что итераторы являются либо пустыми, либо такое же количество записей.


Если вам нужно знать до вы используете генератор, то нет, нет простого способа. Если вы можете подождать, пока после вы использовали генератор, есть простой способ:

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()

вот мой простой подход, который я использую, чтобы продолжать возвращать итератор, проверяя, было ли что-то получено Я просто проверяю, работает ли цикл:

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

вот простой декоратор, который обертывает генератор, поэтому он возвращает 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 %}

просто оберните генератор с itertools.цепь, поместите что-то, что будет представлять конец iterable как второй iterable, а затем просто проверьте это.

Ex:

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

используя islice, вам нужно только проверить до первой итерации, чтобы узнать, пуст ли он.

из itertools импортировать islice

def isempty (iterable):
    возвращаемый список (islice (iterable,1)) == []


Как насчет использования any ()? Я использую его с генератора и он работает нормально. здесь есть парень, объясняющий немного об этом


использовать peek функция в 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

итератор, возвращаемый этой функцией, будет эквивалентен исходному, переданному в качестве аргумента.


подсказано 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)

Я решил его с помощью функции sum. См. ниже пример, который я использовал с glob.iglob (который возвращает генератор).

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

*Это, вероятно, не будет работать для огромных генераторов, но должно хорошо работать для небольших списков