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

StackOverflow https://stackoverflow.com/questions/661603

  •  20-08-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

Простой ответ на ваш вопрос:нет, простого способа не существует.Существует целая куча обходных путей.

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

Вы могли бы написать функцию 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.

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

iterable = some_generator()

_exhausted = object()

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

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

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

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

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

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)

Конечно, как, я уверен, укажут многие комментаторы, это халтура и вообще работает только в определенных ограниченных ситуациях (например, когда генераторы не имеют побочных эффектов).ИММВ.

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

Вот класс-оболочка, который можно добавить к существующему итератору, чтобы добавить __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 возникает, поскольку в вашем случае end достигается немедленно, возникает исключение. Но обычно вам не следует проверять наличие следующего значения.

еще одна вещь, которую вы можете сделать, это:

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

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

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

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]

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

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

Один из примеров, где это полезно, - в шаблонном коде , т. е.джинджа2

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

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

Бывший:

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

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

из itertools импортируется islice

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top