Как я узнаю, что генератор пуст с самого начала?
Есть ли простой способ проверки, если в генераторе нет элементов, таких как 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')
Этот подход также хорошо работает с бесконечными генераторами.
Если вам нужно знать, прежде чем использовать генератор, то нет, простого способа не существует. Если вы можете подождать, пока вы не воспользуетесь генератором, есть простой способ:
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
* Это, вероятно, не будет работать для ОГРОМНЫХ генераторов, но должно работать хорошо для небольших списков