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


158

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


Поправьте меня, если я ошибаюсь, но если бы вы могли сделать действительно универсальное решение для любого генератора, это было бы эквивалентно установке точек останова в операторах yield и возможности «шагнуть назад». Означает ли это клонирование фрейма стека на выходах и их восстановление в StopIteration?

Ну, я думаю, восстановить их StopIteration или нет, но, по крайней мере, StopIteration скажет вам, что он пуст. Да, мне нужно поспать ...

4
Думаю, я знаю, почему он этого хочет. Если вы занимаетесь веб-разработкой с помощью шаблонов и передаете возвращаемое значение в шаблон, например Cheetah или что-то в этом роде, пустой список []удобно считать ложным, так что вы можете выполнить его проверку if и выполнить специальное поведение для чего-то или ничего. Генераторы верны, даже если они не дают никаких элементов.
jpsimons 08

1
Вот мой вариант использования ... Я использую glob.iglob("filepattern")предоставленный пользователем шаблон подстановки и хочу предупредить пользователя, если шаблон не соответствует ни одному файлу. Конечно, я могу обойти это разными способами, но полезно иметь возможность чисто проверить, оказался ли итератор пустым или нет.
LarsH

Можно использовать это решение: stackoverflow.com/a/11467686/463758
balki

Ответы:


56

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

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

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


2
честно говоря, это имеет смысл. Я знал, что нет способа определить длину генератора, но подумал, что, возможно, я пропустил способ узнать, будет ли он изначально генерировать что-либо вообще.
Дэн

1
Да, и для справки, я попытался реализовать свое собственное предложение «модного декоратора». ЖЕСТКИЙ. Видимо copy.deepcopy не работает на генераторах.
Дэвид Бергер,

51
Я не уверен, что могу согласиться с тем, что «простого пути быть не должно». В информатике существует множество абстракций, которые предназначены для вывода последовательности значений без сохранения последовательности в памяти, но которые позволяют программисту спрашивать, есть ли другое значение, не удаляя его из «очереди», если она есть. Существует такая вещь, как однократный просмотр вперед, не требующий «обратного просмотра». Это не значит, что дизайн итератора должен предоставлять такую ​​функцию, но она, безусловно, полезна. Может быть, вы возражаете на том основании, что первое значение может измениться после взгляда?
LarsH

10
Я возражаю на том основании, что типичная реализация даже не вычисляет значение, пока оно не понадобится. Можно заставить интерфейс делать это, но это может быть неоптимальным для облегченных реализаций.
Дэвид Бергер

6
@ S.Lott вам не нужно генерировать всю последовательность, чтобы знать, пуста она или нет. Достаточно памяти для одного элемента - см. Мой ответ.
Марк Рэнсом

104

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

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

6
@ njzk2 Я собирался выполнить операцию "взглянуть" (отсюда и название функции). wiki «peek - это операция, которая возвращает значение вершины коллекции без удаления значения из данных»
Джон Фухи

Это не сработает, если генератор предназначен для выдачи None. 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

31

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

iterable = some_generator()

_exhausted = object()

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

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


1
Нет. Это неверно для любого генератора, у которого первое полученное значение неверно.
mehtunguh 05

7
Используйте object()вместо , classчтобы сделать это одна строка короче: _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa

Почему предметы и все такое? Просто: if next(itreable,-1) == -1 тогда ген пустой!
Апостолос

@Apostolos Потому что next(iter([-1, -2, -3]), -1) == -1есть True. Другими словами, любая итерация с первым элементом, равным, -1будет отображаться как пустая с использованием вашего условия.
Jeyekomon

1
@Apostolos В простом случае да, это решение. Но это не удастся, если вы планируете создать общий инструмент для любой итерации без ограничений.
Джеэкомон,

16

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?
Sait

8
Вероятно, потому что это заставляет вас фактически потреблять генератор, а не просто проверять, пуст ли он.
bfontaine

5
Это плохо, потому что в тот момент, когда вы позвоните next (generator, None), вы пропустите 1 элемент, если он доступен
Натан До

Правильно, вы пропустите 1-й элемент вашего поколения, а также вы собираетесь потреблять свое поколение, а не тестировать его, если он пуст.
AJ,

13

Лучше всего, ИМХО, избежать специальной проверки. В большинстве случаев использование генератора - это проверка:

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
Это решение будет пытаться использовать весь генератор, что сделает его непригодным для бесконечных генераторов.
Виктор Стискала 02

@ ViktorStískala: Я не понимаю твоей точки зрения. Было бы глупо проверять, дает ли бесконечный генератор какие-либо результаты.
vezult 07

Я хотел указать, что ваше решение может содержать перерыв в цикле for, потому что вы не обрабатываете другие результаты, и их создание бесполезно. range(10000000)является конечным генератором (Python 3), но вам не нужно просматривать все элементы, чтобы узнать, генерирует ли он что-либо.
Виктор Стискала

1
@ ViktorStískala: Понятно. Однако я хочу сказать следующее: как правило, вы действительно хотите работать с выходом генератора. В моем примере, если ничего не сгенерировано, теперь вы это знаете. В противном случае вы работаете с сгенерированным выводом, как и предполагалось - «Использование генератора - это проверка». Нет необходимости в специальных тестах или бессмысленном потреблении мощности генератора. Я отредактировал свой ответ, чтобы прояснить это.
vezult

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)

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


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)

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


2
Это вызовет genгенератор только один раз для каждого элемента, поэтому побочные эффекты не являются большой проблемой. Но он будет хранить копию всего, что было получено из генератора через b, но не через a, поэтому последствия для памяти аналогичны простому запуску list(gen)и проверке этого.
Маттиас Фрипп

Здесь есть две проблемы. 1. Этот инструмент itertool может потребовать значительного объема вспомогательной памяти (в зависимости от того, сколько временных данных необходимо сохранить). Как правило, если один итератор использует большую часть или все данные до запуска другого итератора, быстрее использовать list () вместо tee (). 2. итераторы tee не являются потокобезопасными. Ошибка RuntimeError может возникнуть при одновременном использовании итераторов, возвращаемых одним и тем же вызовом tee (), даже если исходная итерация является потокобезопасной.
AJ,

4

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

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

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


Это движется в правильном направлении. Его следует изменить, чтобы можно было заглядывать вперед как можно дальше и сохранять столько результатов, сколько необходимо. В идеале это позволило бы помещать произвольные элементы в начало потока. Pushable-iterator - очень полезная абстракция, которую я часто использую.
sfkleach

@sfkleach Я не вижу необходимости усложнять это для многократного просмотра вперед, это довольно полезно как есть и отвечает на вопрос. Несмотря на то, что это старый вопрос, он все еще время от времени просматривается, поэтому, если вы хотите оставить свой собственный ответ, кто-то может счесть его полезным.
Марк Рэнсом

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

4

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

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]

3

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

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

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

for item in my_generator:
     print item

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

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


Или ... спрашивающий может подсказать, зачем пытаться обнаружить пустой генератор?
S.Lott

вы имели в виду "ничего не будет отображаться, поскольку генератор пуст"?
SilentGhost,

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

2
Это не сообщает программе, был ли генератор пуст.
Итан Фурман

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

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

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 09

@bfontaine Хорошая идея
smac89 09

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

1

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

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

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


Но чем это полезно, если вы действительно хотите поработать с элементами генератора? Просто вставка этого фрагмента перед основным кодом выглядит очень грязно WA
The Godfather

Вы бы заменили оператор break своим кодом.
Пауло Алвес,

Это явно не работает, если генератор производит более одного предмета.
Крестный отец

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

from itertools import islice

def isempty (iterable):
    return list (islice (iterable, 1)) == []


Извините, это чахоточное чтение ... Надо попробовать / уловить с StopIteration
Куин

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

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



-1

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


2
Мы не можем использовать any () для генератора всего. Просто пытался использовать его с генератором, который содержит несколько фреймов данных. Я получил это сообщение «Истинное значение DataFrame неоднозначно». на любом (my_generator_of_df)
пробитайл 08

any(generator)работает, когда вы знаете, что генератор будет генерировать значения, которые могут быть преобразованы в bool- основные типы данных (например, int, string) работают. any(generator)будет иметь значение False, когда генератор пуст, или когда генератор имеет только ложные значения - например, если генератор собирается генерировать 0, '' (пустая строка) и False, тогда он все равно будет False. Это может быть, а может и не быть предполагаемым поведением, если вы об этом знаете :)
Даниэль

anyпотреблял бы все предметы от генератора до (включительно) первого правдивого предмета, в дополнение к проблемам, упомянутым выше
Адам

-2

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.