Як дізнатись, чи генератор порожній з самого початку?


146

Є простий спосіб перевірити , якщо генератор не має елементів, як peek, hasNext, isEmpty, що - то вздовж цих ліній?


Виправте мене, якщо я помиляюся, але якби ви могли зробити справді загальне рішення для будь-якого генератора, це було б еквівалентом встановлення точок прориву в заявах про вихід і можливість "крок назад". Чи означає це клонування кадру стека на врожайність та відновлення їх на StopIteration?

Я думаю, відновіть їх StopIteration чи ні, але принаймні StopIteration скаже вам, що вона порожня. Так, мені потрібно спати ...

4
Я думаю, я знаю, чому він цього хоче. Якщо ви робите веб-розробки за допомогою шаблонів і передаєте повернене значення в такий шаблон, як Гепард чи щось подібне, порожній список []зручно Фальсі, так що ви можете це зробити, якщо перевірити його та зробити особливу поведінку на щось чи нічого. Генератори вірні, навіть якщо вони не дають елементів.
jpsimons

Ось мій випадок використання ... Я використовую glob.iglob("filepattern")шаблон, що надається користувачем, і я хочу попередити користувача, якщо шаблон не відповідає жодним файлам. Звичайно, я можу обійти це різними способами, але корисно мати можливість чітко перевірити, чи з’явився ітератор порожнім чи ні.
LarsH

Може бути використовувати це рішення: stackoverflow.com/a/11467686/463758
Балки

Відповіді:


53

Проста відповідь на ваше запитання: ні, не існує простого способу. Є ціла маса робіт.

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

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


2
досить справедливо, це має сенс. Я знав, що немає можливості знайти довжину генератора, але подумав, що, можливо, я пропустив спосіб пошуку, якщо він спочатку збирається щось генерувати взагалі.
День

1
О, і для довідки, я спробував реалізувати власну пропозицію "фантазійного декоратора". ТВЕРДО. Мабуть copy.deepcopy не працює на генераторах.
Девід Бергер

47
Я не впевнений, що можу погодитися з "не повинно бути простого способу". У інформатиці є багато абстракцій, які призначені для виведення послідовності значень без утримання послідовності в пам'яті, але дозволяють програмісту запитати, чи є інше значення, не виймаючи його з "черги", якщо така є. Існує така річ, як одиночний вигляд вперед, не вимагаючи "зворотного обходу". Це не означає, що ітераторська конструкція повинна надавати таку функцію, але вона, безумовно, корисна. Можливо, ви заперечуєте, виходячи з того, що перше значення може змінитися після вигляду?
LarsH

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

6
@ S.Lott вам не потрібно генерувати всю послідовність, щоб знати, чи послідовність порожня чи ні. Одного елемента варто зберігати - дивіться мою відповідь.
Марк Викуп 11

99

Пропозиція:

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 "заглянути" - це операція, яка повертає значення верхньої частини колекції, не виймаючи значення з даних "
Джон Фугі

Це не спрацює, якщо генератор розроблений таким чином, щоб він не мав. 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
одноліткові

29

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

iterable = some_generator()

_exhausted = object()

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

Редагувати: виправлено проблему, вказану в коментарі mehtunguh.


1
Ні. Це невірно для будь-якого генератора, де перше значення не відповідає дійсності.
mehtunguh

7
Використовуйте object()замість , classщоб зробити це один рядок коротше: _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Месса

13

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?
Саїт

8
Можливо, тому, що це змушує вас фактично споживати генератор, а не просто тестувати, чи він порожній.
bfontaine

3
Це погано, тому що в момент, коли ви подзвоните далі (генератор, немає), ви пропустите 1 предмет, якщо він доступний
Nathan Do

Правильно, ви будете пропускати 1-й елемент свого роду, а також збираєтесь споживати свій рід, а не тестувати, якщо він порожній.
AJ

12

Найкращим підходом, IMHO, було б уникнути спеціального тесту. Найчастіше використання генератора є тестом:

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

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

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

1
@ ViktorStískala: Зрозумів. Однак моя думка така: як правило, ви дійсно хочете працювати на виході генератора. У моєму прикладі, якщо нічого не створюється, тепер ви це знаєте. В іншому випадку ви працюєте на створеному виході за призначенням - "Використання генератора - це тест". Немає необхідності в спеціальних випробуваннях або безглуздо споживати вихід генератора. Я відредагував свою відповідь, щоб уточнити це.
vezult

8

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

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.


1
Це подзвонить genгенератор лише один раз для кожного елемента, тому побічні ефекти не є занадто поганою проблемою. Але він буде зберігати копію всього, що було витягнуто з генератора через b, а не через a, тому наслідки пам'яті схожі з просто запущеним list(gen)і перевіреним.
Маттіас Фріпп

Це два питання. 1. Цей itertool може потребувати значного допоміжного зберігання (залежно від того, скільки тимчасових даних потрібно зберігати). Загалом, якщо один ітератор використовує більшість або всі дані до запуску іншого ітератора, швидше використовувати list () замість tee (). 2. Ітератори трійника не є безпечними для ниток. RuntimeError може бути підвищений при використанні одночасно ітераторів, що повертаються тим же викликом tee (), навіть якщо оригінальний ітерабельний файл є безпечним для потоків.
AJ

3

Вибачте за очевидний підхід, але найкращим способом було б це зробити:

for item in my_generator:
     print item

Тепер ви виявили, що генератор порожній під час його використання. Звичайно, елемент ніколи не відображатиметься, якщо генератор порожній.

Це може не зовсім відповідати вашому коду, але саме для цього ідіома генератора: ітерація, тому, можливо, ви можете трохи змінити свій підхід або взагалі не використовувати генератори.


Або ... запитуючий міг би дати деяку підказку, чому б спробувати виявити порожній генератор?
S.Lott

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

С.Лотт. Я згоден. Я не можу зрозуміти, чому. Але я думаю, навіть якби була причина, проблема може бути краще звернутись до використання кожного елемента.
Алі Афшар

1
Це не говорить програмі, чи генератор був порожній.
Етан Фурман

3

Все, що вам потрібно зробити, щоб побачити, чи генератор порожній, - спробувати отримати наступний результат. Звичайно, якщо ви не готові використати цей результат, вам доведеться зберегти його, щоб повернути його знову пізніше.

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

Зверніть увагу, що ви можете перевірити порожнечу в будь-який час, а не лише на початку ітерації.


Це направляється в правильному напрямку. Його слід змінити, щоб він міг зазирнути вперед, наскільки ви хочете, зберігаючи стільки результатів, скільки потрібно. В ідеалі це дозволило б натиснути на голову потоку довільних предметів. Ітератор, що висувається - це дуже корисна абстракція, яку я часто використовую.
sfkleach

@sfkleach Я не бачу необхідності ускладнювати це для кількох випереджуючих дій, це досить корисно, як є, і відповідає на питання. Незважаючи на те, що це давнє питання, воно все ще набуває випадкового вигляду, тому, якщо ви хочете залишити власну відповідь, хтось може вважати це корисним.
Марк Викуп

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

2

Ось запропонований Марком Рансом, ось клас, який ви можете використовувати, щоб обернути будь-який ітератор, щоб ви могли зазирнути вперед, відсунути значення назад на потік і перевірити, чи немає порожніх. Це проста ідея з простою реалізацією, яку я вважав дуже зручною в минулому.

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

Щойно впав на цю тему і зрозумів, що відсутня дуже проста і легка для читання відповідь:

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]

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

@bfontaine Хороша ідея
smac89

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


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, вам потрібно перевірити лише до першої ітерації, щоб виявити, чи вона порожня.

з itertools import islice

def isempty (ітерабельний):
    список повернення (islice (iterable, 1)) == []


Вибачте, це читаюче прочитання ... Доводиться робити спробу / ловити за допомогою StopIteration
Quin

0

Що з використанням будь-якого ()? Я використовую його з генераторами, і він працює чудово. Ось хлопець трохи пояснює це


2
Ми не можемо використовувати "any ()" для всього генератора. Просто спробував використати його з генератором, який містить кілька фреймів даних. Я отримав це повідомлення "Значення істинності DataFrame неоднозначне". у будь-якому (my_generator_of_df)
probitaille

any(generator)працює, коли ви знаєте, що генератор буде генерувати значення, на які можна передати bool- основні типи даних (наприклад, int, string). any(generator)буде помилковим, коли генератор порожній або коли генератор має лише помилкові значення - наприклад, якщо генератор генерує 0, '' (порожній рядок) та False, він все одно буде помилковим. Це може бути, а може і не бути цілеспрямованою поведінкою, до тих пір, поки ви це знаєте :)
Даніель,

0

Використовуйте функцію peek у цитоользі.

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

Я вирішив це за допомогою функції сума. Нижче див. Приклад, який я використав із 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.