Как я узнаю, что генератор пуст с самого начала?
есть ли простой способ тестирования, если генератор не имеет элементов, таких как 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
*Это, вероятно, не будет работать для огромных генераторов, но должно хорошо работать для небольших списков