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

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

28 ответов

Решение

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

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

Если хотите, вы можете написать функцию 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')

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

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

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

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

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

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)

Просто попал в эту ветку и понял, что очень простой и легко читаемый ответ отсутствует:

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]

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

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)

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

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

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

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

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

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.

Я обнаружил, что только это решение работает и для пустых итераций.

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

Это старый вопрос, на который есть ответ, но, поскольку никто не показал его раньше, вот он:

for _ in generator:
    break
else:
    print('Empty')

Вы можете прочитать больше здесь

Используйте функцию просмотра в 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 центами», я собираюсь описать свой опыт:

У меня есть генератор, который мне нужно нарезать, используя 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

Возможно, это не лучший подход, в основном потому, что он потребляет генератор, но мне он подходит.

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

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

Просто оберните генератор с помощью 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

Проверка генератора перед его итерацией соответствует стилю кодирования LBYL .Другой подход (EAFP) состоял бы в том, чтобы перебрать его, а затем проверить, был ли он пустым или нет.

      is_empty = True

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

if is_empty:
    print('Generator is empty')

Этот подход также хорошо работает с бесконечными генераторами.

bool(генератор) вернет правильный результат

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

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

peekableотmore-itertoolsпозволяет проверить, исчерпан ли он, проверив его значение истинности. Демонстрация с одним пустым и одним непустым итератором:

      from more_itertools import peekable

for source in '', 'foobar':
    it = iter(source)
    
    if it := peekable(it):
        print('values:', *it)
    else:
        print('empty')

Выход:

      empty
values: f o o b a r

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

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

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

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

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

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

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

Есть очень простое решение: if next(generator,-1) == -1 значит генератор пустой!

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

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

* Это, вероятно, не будет работать для ОГРОМНЫХ генераторов, но должно работать хорошо для небольших списков

Другие вопросы по тегам