Skąd mam wiedzieć, czy generator jest pusty od samego początku?


146

Czy istnieje prosty sposób na sprawdzenie, czy generator ma żadnych przedmiotów, jak peek, hasNext, isEmpty, coś w tym kierunku?


Popraw mnie, jeśli się mylę, ale gdybyś mógł stworzyć naprawdę ogólne rozwiązanie dla dowolnego generatora, byłoby to równoważne ustawieniu punktów przerwania w instrukcjach yield i posiadaniu możliwości „cofania się”. Czy oznaczałoby to sklonowanie ramki stosu na podstawie plonów i przywrócenie ich w StopIteration?

Cóż, myślę, że przywróć im StopIteration lub nie, ale przynajmniej StopIteration powie ci, że jest pusty. Tak, potrzebuję snu ...

4
Myślę, że wiem, dlaczego on tego chce. Jeśli tworzysz strony internetowe za pomocą szablonów i przekazujesz wartość zwracaną do szablonu takiego jak Cheetah lub coś w tym stylu, pusta lista []jest wygodnie Falsey, więc możesz ją sprawdzić i wykonać specjalne zachowanie dla czegoś lub niczego. Generatory są prawdziwe, nawet jeśli nie dostarczają żadnych elementów.
jpsimons

Oto mój przypadek użycia ... Używam glob.iglob("filepattern")wzorca wieloznacznego dostarczonego przez użytkownika i chcę ostrzec użytkownika, jeśli wzorzec nie pasuje do żadnych plików. Jasne, że mogę to obejść na różne sposoby, ale warto mieć możliwość dokładnego sprawdzenia, czy iterator wyszedł pusty, czy nie.
LarsH,

Można skorzystać z tego rozwiązania: stackoverflow.com/a/11467686/463758
balki

Odpowiedzi:


53

Prosta odpowiedź na twoje pytanie: nie, nie ma prostego sposobu. Istnieje wiele obejść.

Naprawdę nie powinno być prostego sposobu, z powodu tego, czym są generatory: sposobem na wyjście sekwencji wartości bez przechowywania sekwencji w pamięci . Nie ma więc przechodzenia wstecz.

Możesz napisać funkcję has_next, a może nawet wrzucić ją do generatora jako metodę z fantazyjnym dekoratorem, jeśli chcesz.


2
w porządku, to ma sens. Wiedziałem, że nie ma sposobu na ustalenie długości generatora, ale pomyślałem, że mogłem przegapić sposób znalezienia, czy początkowo ma on w ogóle coś wygenerować.
Dan,

1
Aha, i dla porównania, spróbowałem zaimplementować własną sugestię „fantazyjnego dekoratora”. CIĘŻKO. Najwyraźniej copy.deepcopy nie działa na generatorach.
David Berger,

47
Nie jestem pewien, czy mogę się zgodzić z „nie powinno być prostego sposobu”. W informatyce istnieje wiele abstrakcji, które mają na celu wyprowadzenie sekwencji wartości bez przechowywania sekwencji w pamięci, ale które pozwalają programiście zapytać, czy istnieje inna wartość bez usuwania jej z „kolejki”, jeśli istnieje. Istnieje coś takiego jak pojedyncze zajrzenie do przodu bez konieczności „przechodzenia wstecz”. Nie oznacza to, że projekt iteratora musi zapewniać taką funkcję, ale z pewnością jest przydatny. Może sprzeciwiasz się temu, że pierwsza wartość może się zmienić po peek'u?
LarsH

9
Sprzeciwiam się tym, że typowa implementacja nawet nie oblicza wartości, dopóki nie jest potrzebna. Można by zmusić interfejs do zrobienia tego, ale może to być nieoptymalne dla lekkich implementacji.
David Berger,

6
@ S.Lott nie musisz generować całej sekwencji, aby wiedzieć, czy sekwencja jest pusta, czy nie. Wystarczy przechowywać jeden element - zobacz moją odpowiedź.
Mark Ransom

99

Sugestia:

def peek(iterable):
    try:
        first = next(iterable)
    except StopIteration:
        return None
    return first, itertools.chain([first], iterable)

Stosowanie:

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
Nie do końca rozumiem sens zwracania pierwszego elementu dwukrotnie return first, itertools.chain([first], rest).
njzk2

6
@ njzk2 Chciałem wykonać operację „podglądu” (stąd nazwa funkcji). wiki "peek to operacja, która zwraca wartość wierzchołka kolekcji bez usuwania wartości z danych"
John Fouhy,

To nie zadziała, jeśli generator jest zaprojektowany tak, aby dawał Brak. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paweł

4
@Paul Przyjrzyj się uważnie zwracanym wartościom. Jeśli generator jest gotowy - tj. Nie zwraca None, ale podnosi StopIteration- wynikiem funkcji jest None. W przeciwnym razie jest to krotka, która nie jest None.
Załóż pozew Moniki

To bardzo mi pomogło w moim obecnym projekcie. Znalazłem podobny przykład w kodzie dla modułu standardowej biblioteki Pythona „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

29

Prostym sposobem jest użycie opcjonalnego parametru next (), który jest używany, gdy generator jest wyczerpany (lub pusty). Na przykład:

iterable = some_generator()

_exhausted = object()

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

Edycja: poprawiono problem wskazany w komentarzu mehtunguh.


1
Nie. Jest to niepoprawne dla każdego generatora, w którym pierwsza uzyskana wartość jest nieprawdziwa.
mehtunguh

7
Użyj object()zamiast classzrobić to jedna linia krótsza: _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa,

13

next(generator, None) is not None

Lub wymień, Noneale nie ma jej w generatorze jakąkolwiek znaną Ci wartość .

Edycja : Tak, spowoduje to pominięcie 1 elementu w generatorze. Często jednak sprawdzam, czy generator jest pusty tylko do celów walidacji, a potem tak naprawdę go nie używam. Lub inaczej robię coś takiego:

def foo(self):
    if next(self.my_generator(), None) is None:
        raise Exception("Not initiated")

    for x in self.my_generator():
        ...

Oznacza to, że działa to, jeśli twój generator pochodzi z funkcji , jak w generator().


4
Dlaczego to nie jest najlepsza odpowiedź? Na wypadek, gdyby generator zwrócił None?
Sait

8
Prawdopodobnie dlatego, że zmusza cię to do faktycznego zużycia generatora, zamiast po prostu testowania, czy jest pusty.
bfontaine

3
To jest złe, ponieważ w momencie, gdy zadzwonisz do następnego (generator, brak), pominiesz 1 element, jeśli jest dostępny
Nathan Do

Prawidłowo, przegapisz pierwszy element swojej generacji, a także zamierzasz konsumować gen, zamiast testować, czy jest pusty.
AJ

12

Najlepszym podejściem, IMHO, byłoby uniknięcie specjalnego testu. W większości przypadków użycie generatora jest testem:

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)

Jeśli to nie wystarczy, nadal możesz przeprowadzić wyraźny test. W tym momencie thingbędzie zawierać ostatnią wygenerowaną wartość. Jeśli nic nie zostało wygenerowane, będzie niezdefiniowane - chyba że zdefiniowałeś już zmienną. Możesz sprawdzić wartość thing, ale to trochę niewiarygodne. Zamiast tego po prostu ustaw flagę w bloku i sprawdź ją później:

if not thing_generated:
    print "Avast, ye scurvy dog!"

3
To rozwiązanie będzie próbowało pochłonąć cały generator, czyniąc go bezużytecznym dla nieskończonych generatorów.
Viktor Stískala

@ ViktorStískala: Nie widzę twojego punktu widzenia. Byłoby głupotą sprawdzanie, czy nieskończony generator daje jakiekolwiek wyniki.
vezult

Chciałem zwrócić uwagę, że Twoje rozwiązanie może zawierać przerwę w pętli for, ponieważ nie przetwarzasz innych wyników i ich generowanie jest bezużyteczne. range(10000000)jest generatorem skończonym (Python 3), ale nie musisz przeglądać wszystkich elementów, aby dowiedzieć się, czy coś generuje.
Viktor Stískala

1
@ ViktorStískala: Zrozumiano. Chodzi mi jednak o to: ogólnie rzecz biorąc, faktycznie chcesz działać na wyjściu generatora. W moim przykładzie, jeśli nic nie jest generowane, teraz to wiesz. W przeciwnym razie operujesz na wygenerowanym wyjściu zgodnie z przeznaczeniem - „Korzystanie z generatora jest testem”. Nie ma potrzeby przeprowadzania specjalnych testów ani bezcelowego zużywania mocy generatora. Zredagowałem moją odpowiedź, aby to wyjaśnić.
vezult

8

Nienawidzę oferować drugiego rozwiązania, szczególnie takiego, którego sam bym nie użył, ale gdybyś absolutnie musiał to zrobić i nie zużywać generatora, jak w innych odpowiedziach:

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)

Teraz bardzo mi się to rozwiązanie nie podoba, bo uważam, że nie tak mają być używane generatory.


4

Zdaję sobie sprawę, że ten post ma w tym momencie 5 lat, ale znalazłem go, szukając idiomatycznego sposobu na zrobienie tego, i nie widziałem opublikowanego mojego rozwiązania. Więc dla potomnych:

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)

Oczywiście, jak z pewnością zauważy wielu komentatorów, jest to zepsute i działa tylko w pewnych ograniczonych sytuacjach (na przykład, gdy generatory są wolne od efektów ubocznych). YMMV.


1
Spowoduje to wywołanie gengeneratora tylko raz dla każdego przedmiotu, więc skutki uboczne nie są problemem. Ale będzie przechowywać kopię wszystkiego, co zostało pobrane z generatora przez b, ale nie przez a, więc implikacje dotyczące pamięci są podobne do samego uruchomienia list(gen)i sprawdzenia tego.
Matthias Fripp

Ma dwa problemy. 1. To narzędzie itertool może wymagać znacznej ilości pamięci dyskowej (w zależności od tego, ile tymczasowych danych należy przechowywać). Ogólnie rzecz biorąc, jeśli jeden iterator używa większości lub wszystkich danych przed uruchomieniem innego iteratora, szybsze jest użycie list () zamiast tee (). 2. Iteratory tee nie są bezpieczne dla wątków. Błąd RuntimeError może zostać zgłoszony podczas jednoczesnego używania iteratorów zwracanych przez to samo wywołanie tee (), nawet jeśli oryginalna iteracja jest bezpieczna wątkowo.
AJ

3

Przepraszamy za oczywiste podejście, ale najlepiej byłoby zrobić:

for item in my_generator:
     print item

Teraz wykryłeś, że generator jest pusty, gdy go używasz. Oczywiście pozycja nigdy nie zostanie wyświetlona, ​​jeśli generator jest pusty.

Może to nie pasować do twojego kodu, ale do tego służy idiom generatora: iteracja, więc być może możesz nieznacznie zmienić swoje podejście lub w ogóle nie używać generatorów.


Albo ... osoba pytająca mogłaby podpowiedzieć, dlaczego ktoś miałby próbować wykryć pusty generator?
S.Lott,

czy chodziło Ci o „nic nie będzie wyświetlane, ponieważ generator jest pusty”?
SilentGhost

S.Lott. Zgadzam się. Nie rozumiem dlaczego. Ale myślę, że nawet gdyby istniał powód, lepiej byłoby rozwiązać problem, aby zamiast tego użyć każdego przedmiotu.
Ali Afshar,

1
To nie mówi programowi, czy generator był pusty.
Ethan Furman

3

Wszystko, co musisz zrobić, aby sprawdzić, czy generator jest pusty, to spróbować uzyskać następny wynik. Oczywiście, jeśli nie jesteś gotowy do użycia tego wyniku, musisz go zapisać, aby ponownie go później zwrócić.

Oto klasa opakowania, którą można dodać do istniejącego iteratora w celu dodania __nonzero__testu, dzięki czemu można sprawdzić, czy generator jest pusty za pomocą prostego if. Prawdopodobnie można go również przekształcić w dekoratora.

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)

Oto jak tego użyjesz:

with open(filename, 'r') as f:
    f = GenWrapper(f)
    if f:
        print 'Not empty'
    else:
        print 'Empty'

Zauważ, że możesz sprawdzić pustkę w dowolnym momencie, nie tylko na początku iteracji.


To zmierza we właściwym kierunku. Powinien zostać zmodyfikowany, aby umożliwić podglądanie przed siebie tak daleko, jak chcesz, przechowując tyle wyników, ile potrzeba. Idealnie pozwoliłoby na wypychanie dowolnych przedmiotów na czoło strumienia. Iterator typu pushable to bardzo przydatna abstrakcja, której często używam.
sfkleach

@sfkleach Nie widzę potrzeby komplikowania tego przy wielokrotnym zajrzeniu w przód, jest to całkiem przydatne i odpowiada na pytanie. Mimo że jest to stare pytanie, od czasu do czasu pojawia się ono, więc jeśli chcesz zostawić własną odpowiedź, ktoś może uznać ją za przydatną.
Mark Ransom

Mark ma rację, że jego rozwiązanie odpowiada na pytanie, które jest kluczowe. Powinienem był to sformułować lepiej. Chodziło mi o to, że pushable-iteratory z nieograniczonym pushback to idiom, który uznałem za niezwykle przydatny, a implementacja jest prawdopodobnie jeszcze prostsza. Zgodnie z sugestią opublikuję kod wariantu.
sfkleach

2

Poproszona przez Mark Ransom, oto klasa, której możesz użyć do zawinięcia dowolnego iteratora, abyś mógł zerknąć przed siebie, wrzucić wartości z powrotem do strumienia i sprawdzić, czy są puste. To prosty pomysł z prostą implementacją, która była dla mnie bardzo przydatna w przeszłości.

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

Właśnie wpadłem na ten wątek i zdałem sobie sprawę, że brakuje bardzo prostej i łatwej do odczytania odpowiedzi:

def is_empty(generator):
    for item in generator:
        return False
    return True

Jeśli nie zamierzamy konsumować żadnego przedmiotu, musimy ponownie wstrzyknąć pierwszy przedmiot do generatora:

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

Przykład:

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

Na końcu generator StopIterationjest podnoszony, ponieważ w twoim przypadku koniec jest osiągany natychmiast, zgłaszany jest wyjątek. Ale normalnie nie powinieneś sprawdzać istnienia następnej wartości.

inną rzeczą, którą możesz zrobić, jest:

>>> gen = (i for i in [])
>>> if not list(gen):
    print('empty generator')

2
Co faktycznie zużywa cały generator. Niestety, nie jest jasne, czy jest to pożądane czy niepożądane zachowanie.
S.Lott,

jak każdy inny sposób „dotknięcia” generatora.
SilentGhost

Zdaję sobie sprawę, że to jest stare, ale użycie metody 'list ()' nie może być najlepszym sposobem, jeśli wygenerowana lista nie jest pusta, ale w rzeczywistości duża, jest to niepotrzebnie marnotrawstwo
Chris_Rands,

1

Jeśli chcesz wiedzieć, zanim użyjesz generatora, to nie, nie ma prostego sposobu. Jeśli możesz poczekać, aż po użyłeś generatora, istnieje prosty sposób:

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

Po prostu zawiń generator za pomocą itertools.chain , umieść coś, co będzie reprezentować koniec iteracji jako drugą iterowalną, a następnie po prostu sprawdź to.

Dawny:

import itertools

g = some_iterable
eog = object()
wrap_g = itertools.chain(g, [eog])

Teraz pozostaje tylko sprawdzić tę wartość, którą dodaliśmy na końcu iterowalnej, kiedy ją przeczytasz, będzie to oznaczało koniec

for value in wrap_g:
    if value == eog: # DING DING! We just found the last element of the iterable
        pass # Do something

Użyj eog = object()zamiast zakładać, że float('-inf')nigdy nie wystąpi w iterowalnej.
bfontaine

@bfontaine Dobry pomysł
smac89

1

W moim przypadku musiałem wiedzieć, czy zapełniono wiele generatorów, zanim przekazałem je do funkcji, która scalała elementy, tj zip(...).. Rozwiązanie jest podobne, ale dostatecznie różne od przyjętej odpowiedzi:

Definicja:

def has_items(iterable):
    try:
        return True, itertools.chain([next(iterable)], iterable)
    except StopIteration:
        return False, []

Stosowanie:

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"

Mój szczególny problem ma tę właściwość, że elementy iteracyjne są albo puste, albo mają dokładnie taką samą liczbę wpisów.


1

Uważam, że tylko to rozwiązanie działa również dla pustych iteracji.

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)

Lub jeśli nie chcesz używać wyjątku do tego, spróbuj użyć

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)

W zaznaczonym rozwiązaniu nie ma możliwości zastosowania go do pustych generatorów typu

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

Oto moje proste podejście, którego używam, aby zwracać iterator podczas sprawdzania, czy coś zostało znalezione.Po prostu sprawdzam, czy pętla działa:

        n = 0
        for key, value in iterator:
            n+=1
            yield key, value
        if n == 0:
            print ("nothing found in iterator)
            break

0

Oto prosty dekorator, który otacza generator, więc zwraca None, jeśli jest pusty. Może to być przydatne, jeśli Twój kod musi wiedzieć, czy generator coś wyprodukuje, zanim przejdzie przez niego w pętli.

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

Stosowanie:

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

Jednym z przykładów, w którym jest to przydatne, jest kod szablonów - np. Jinja2

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

Spowoduje to dwukrotne wywołanie funkcji generatora, co spowoduje dwukrotne poniesienie kosztów uruchomienia generatora. Może to być istotne, jeśli na przykład funkcja generatora jest zapytaniem do bazy danych.
Ian Goldby,

0

używając islice, wystarczy sprawdzić do pierwszej iteracji, aby stwierdzić, czy jest pusty.

z itertools import islice

def isempty (iterowalne):
    lista zwrotów (islice (iterowalne, 1)) == []


Przepraszamy, to jest wyczerpująca lektura ... Muszę zrobić próbę / złapanie z StopIteration
Quin

0

A co z użyciem any ()? Używam go z generatorami i działa dobrze. Tutaj jest facet, który trochę to wyjaśnia


2
Nie możemy użyć "any ()" dla wszystkich generatorów. Właśnie próbowałem użyć go z generatorem, który zawiera wiele ramek danych. Otrzymałem wiadomość: „Wartość prawdziwości ramki DataFrame jest niejednoznaczna”. na dowolny (my_generator_of_df)
probitaille

any(generator)działa, gdy wiesz, że generator wygeneruje wartości, na które można rzutować bool- działają podstawowe typy danych (np. int, string). any(generator)będzie False, gdy generator jest pusty lub gdy generator ma tylko fałszywe wartości - na przykład, jeśli generator ma generować 0, '' (pusty ciąg) i False, to nadal będzie False. To może być zamierzone zachowanie lub nie, o ile jesteś tego świadomy :)
Daniel,

0

Użyj Peek funkcji w 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

Iterator zwracany przez tę funkcję będzie równoważny z oryginalnym przekazanym jako argument.


-2

Rozwiązałem to za pomocą funkcji sumy. Zobacz poniżej przykład, którego użyłem z glob.iglob (który zwraca generator).

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

* To prawdopodobnie nie zadziała dla OGROMNYCH generatorów, ale powinno dobrze działać dla mniejszych list

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.