Bir jeneratörün başlangıçtan itibaren boş olup olmadığını nasıl anlarım?


146

Jeneratör hiç ürün gibi varsa test basit bir yolu var mı peek, hasNext, isEmptybu doğrultuda bir şey?


Yanılıyorsam beni düzeltin, ancak herhangi bir jeneratöre gerçekten genel bir çözüm yapabiliyorsanız , verim ifadelerinde kesme noktaları ayarlamanın ve "geri adım atma" yeteneğine sahip olmanın karşılığı olacaktır. Bu, yığın çerçevesinin verim üzerine klonlanması ve StopIteration'da geri yüklenmesi anlamına mı gelir?

Sanırım onları StopIteration'ı geri yükle ya da geri yükle, ama en azından StopIteration size boş olduğunu söylerdi. Evet uykuya ihtiyacım var ...

4
Sanırım bunu neden istediğini biliyorum. Şablonlarla web geliştirme yapıyorsanız ve dönüş değerini Çita veya benzeri bir şablona []geçiriyorsanız , boş liste uygun bir şekilde Falsey'dir, böylece bir if kontrolü yapabilir ve bir şey veya hiçbir şey için özel davranışlar yapabilirsiniz. Jeneratörler hiçbir eleman vermese bile doğrudur.
jpsimons

İşte kullanım durumum ... glob.iglob("filepattern")Kullanıcı tarafından sağlanan joker karakter desenini kullanıyorum ve desen herhangi bir dosyayla eşleşmezse kullanıcıyı uyarmak istiyorum. Elbette bu konuda çeşitli şekillerde çalışabilirim, ancak yineleyicinin boş olup olmadığını temiz bir şekilde test edebilmek yararlıdır.
LarsH

Bu çözümü kullanıyor olabilir: stackoverflow.com/a/11467686/463758
balki

Yanıtlar:


53

Sorunuzun basit cevabı: hayır, basit bir yolu yok. Bir sürü çözüm var.

Jeneratörlerin ne olduğu için gerçekten basit bir yol olmamalı: diziyi bellekte tutmadan bir değer dizisi çıkarmanın bir yolu . Yani geriye doğru geçiş yok.

Bir has_next işlevi yazabilir veya isterseniz bir süslü dekoratörle bir yöntem olarak bir jeneratöre bile tokatlayabilirsiniz.


2
yeterince adil, bu mantıklı. bir jeneratörün uzunluğunu bulmanın bir yolu olmadığını biliyordum, ama başlangıçta hiç bir şey üretecekse bulmanın bir yolunu kaçırmış olabileceğimi düşündüm.
Dan

1
Ve referans olarak, kendi "süslü dekoratör" öneriyi uygulamaya çalıştım. ZOR. Görünüşe göre copy.deepcopy jeneratörlerde çalışmaz.
David Berger

47
"Basit bir yol olmamalı" konusunda hemfikir olabileceğimden emin değilim. Bilgisayar biliminde, sekansı bellekte tutmadan bir değer dizisi çıkarmak için tasarlanmış, ancak programcıya "kuyruktan" çıkarmadan başka bir değer olup olmadığını sormasına izin veren birçok soyutlama vardır. "Geriye doğru çaprazlama" gerektirmeden tek bir ileri gitmek gibi bir şey vardır. Bu, bir yineleyici tasarımının böyle bir özellik sağlaması gerektiği anlamına gelmez, ancak kesinlikle yararlıdır. Belki ilk bakışta gözetlemeden sonra değişebileceğine itiraz ediyorsun?
LarsH

9
Ben tipik bir uygulama gerekene kadar bir değer bile hesaplamıyor gerekçesiyle itiraz ediyorum. Arayüz bunu yapmaya zorlayabilir, ancak bu, hafif uygulamalar için optimalin altında olabilir.
David Berger

6
Dizinin boş olup olmadığını bilmek için dizinin tamamını oluşturmanıza gerek yoktur. Bir elemanın depolama alanı yeterli - cevabımı görün.
Mark Ransom

99

Öneri:

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

Kullanımı:

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
İlk öğeyi iki kez geri döndürme noktasını tam olarak anlamıyorum return first, itertools.chain([first], rest).
njzk2

6
@ njzk2 Bir "gözetleme" işlemi için gidiyordum (dolayısıyla işlev adı). wiki "peek, verilerden değeri kaldırmadan koleksiyonun en üst değerini döndüren bir işlemdir"
John Fouhy

Jeneratör Yok üretecek şekilde tasarlanmışsa bu çalışmaz. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul

4
@Paul Dönüş değerlerine yakından bakın. Jeneratör yapıldıysa - yani geri dönmüyor None, ama yükseltiyorsa StopIteration- fonksiyonun sonucu None. Aksi takdirde, bir demettir, öyle değildir None.
Monica'nın Davası

Mevcut projemde bu bana çok yardımcı oldu. Python'un standart kütüphane modülü 'mailbox.py' için de benzer bir örnek buldum. 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
akran

29

Basit bir yol, jeneratör tükendiğinde (veya boşsa) kullanılan next () için isteğe bağlı parametreyi kullanmaktır . Örneğin:

iterable = some_generator()

_exhausted = object()

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

Düzenleme: Sorun mehtunguh yorumunda işaret düzeltildi.


1
Hayır. Bu, verilen ilk değerin doğru olmadığı tüm jeneratörler için yanlıştır.
mehtunguh

7
Bir satır daha kısa yapmak için object()yerine kullanın :; class_exhausted = object()if next(iterable, _exhausted) is _exhausted:
Messa

13

next(generator, None) is not None

Ya da değiştirin Noneama bildiğiniz her değer jeneratörünüzde değil .

Düzenleme : Evet, bu jeneratördeki 1 öğeyi atlayacaktır. Ancak, genellikle, bir jeneratörün sadece doğrulama amacıyla boş olup olmadığını kontrol ederim, o zaman gerçekten kullanmayın. Yoksa başka bir şey gibi:

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

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

Yani, jeneratörünüz olduğu gibi bir işlevden geliyorsa bu çalışır generator().


4
Neden bu en iyi cevap değil? Jeneratör geri dönerse None?
Sait

8
Muhtemelen bu sizi sadece boş olup olmadığını test etmek yerine jeneratörü tüketmeye zorlar.
bfontaine

3
Kötü çünkü sonraki aradığınızda (jeneratör, Yok) varsa 1 öğeyi atlayacaksınız
Nathan Do

Doğru, geninizin 1. elementini kaçıracaksınız ve ayrıca geniniz boşsa test etmek yerine tüketeceksiniz.
AJ

12

En iyi yaklaşım olan IMHO, özel bir testten kaçınmak olacaktır. Çoğu kez, bir jeneratör kullanımı olan testi:

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)

Bu yeterince iyi değilse, yine de açık bir test yapabilirsiniz. Bu noktada, thingüretilen son değeri içerecektir. Hiçbir şey oluşturulmadıysa, değişkeni önceden tanımlamadığınız sürece tanımsız olacaktır. Değerini kontrol edebilirsiniz thing, ama bu biraz güvenilmez. Bunun yerine, blok içinde bir bayrak ayarlayın ve daha sonra kontrol edin:

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

3
Bu çözüm tüm jeneratörü tüketmeye çalışacak ve böylece sonsuz jeneratörler için kullanılamaz hale gelecektir.
Viktor Stískala

@ ViktorStískala: Ne demek istediğini anlamıyorum. Sonsuz bir jeneratörün herhangi bir sonuç üretip üretmediğini test etmek aptalca olurdu.
vezult

Çözümünüzün for döngüsünde bir kırılma içerebileceğini belirtmek istedim, çünkü diğer sonuçları işlemiyorsunuz ve üretilmeleri işe yaramaz. range(10000000)sonlu bir jeneratördür (Python 3), ancak bir şey üretip üretmediğini öğrenmek için tüm öğeleri gözden geçirmeniz gerekmez.
Viktor Stískala

1
@ ViktorStískala: Anlaşıldı. Ancak, benim açımdan şu: Genelde jeneratör çıkışında çalışmak istiyorsunuz. Örneğimde, hiçbir şey üretilmezse, şimdi biliyorsunuz. Aksi takdirde, üretilen çıktı üzerinde amaçlandığı gibi çalışırsınız - "Jeneratörün kullanımı testtir". Özel testlere veya jeneratör çıkışını gereksiz yere tüketmeye gerek yoktur. Bunu netleştirmek için cevabımı düzenledim.
vezult

8

İkinci bir çözüm sunmaktan, özellikle kendimi kullanmayacağım bir çözüm sunmaktan nefret ediyorum, ancak, bunu kesinlikle yapmak ve jeneratörü tüketmemek zorundaysanız , diğer cevaplarda olduğu gibi:

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)

Şimdi bu çözümü gerçekten sevmiyorum çünkü jeneratörlerin bu şekilde kullanılmayacağına inanıyorum.


4

Bu yazının şu anda 5 yaşında olduğunun farkındayım, ancak bunu yapmanın deyimsel bir yolunu ararken buldum ve çözümümün yayınlanmadığını görmedim. Yani gelecek nesiller için:

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)

Tabii ki, birçok yorumcunun işaret edeceğinden eminim, bu çılgın ve sadece belirli sınırlı durumlarda (örneğin jeneratörlerin yan etkisiz olduğu yerlerde) çalışıyor. YMMV.


1
Bu, genjeneratörü her öğe için sadece bir kez çağıracaktır , bu nedenle yan etkiler çok kötü bir sorun değildir. Ancak, jeneratörden çekilen bancak üzerinden değil , her şeyin bir kopyasını saklayacaktır a, bu nedenle bellek sonuçları sadece koşmaya list(gen)ve kontrol etmeye benzer .
Matthias Fripp

İki sorunu var. 1. Bu itertool önemli miktarda yardımcı depolama gerektirebilir (ne kadar geçici verinin saklanmasına bağlı olarak). Genel olarak, bir yineleyici başka bir yineleyici başlamadan önce verilerin çoğunu veya tamamını kullanırsa, tee () yerine list () kullanmak daha hızlıdır. 2. tee yineleyiciler güvenli değildir. Aynı tee () çağrısı tarafından döndürülen eşzamanlı olarak yineleyiciler kullanıldığında, orijinal yinelenebilir iş parçacığı güvenli olsa bile bir RuntimeError öğesi yükseltilebilir.
AJ

3

Bariz yaklaşım için özür dilerim, ama en iyi yol:

for item in my_generator:
     print item

Şimdi jeneratörü kullanırken boş olduğunu tespit ettiniz. Tabii ki, jeneratör boşsa, öğe asla görüntülenmeyecektir.

Bu, kodunuza tam olarak uymayabilir, ancak jeneratörün deyimi bunun içindir: yineleme, bu yüzden belki yaklaşımınızı biraz değiştirebilir veya jeneratörleri kullanmayabilirsiniz.


Veya ... soru soran, neden boş bir jeneratörü tespit etmeye çalışacağına dair bir ipucu verebilir mi?
S.Lott

"jeneratör boş olduğundan hiçbir şey görüntülenmeyecek" mi demek istediniz?
SilentGhost

S.Lott. Katılıyorum. Nedenini göremiyorum. Ama bence bir sebep olsa bile, sorun her öğeyi kullanmak için daha iyi olabilir.
Ali Afshar

1
Bu programa jeneratörün boş olup olmadığını söylemez.
Ethan Furman

3

Bir jeneratörün boş olup olmadığını görmek için tek yapmanız gereken bir sonraki sonucu almaya çalışmaktır. Tabii bu sonucu kullanmaya hazır değilseniz, daha sonra tekrar döndürmek için saklamanız gerekir.

İşte bir __nonzero__test eklemek için mevcut bir yineleyiciye eklenebilen bir sarmalayıcı sınıfı , böylece jeneratörün basit olup olmadığını görebilirsiniz if. Muhtemelen bir dekoratöre dönüştürülebilir.

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)

Nasıl kullanacağınız aşağıda açıklanmıştır:

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

Sadece yinelemenin başlangıcında değil, istediğiniz zaman boşluğu kontrol edebileceğinizi unutmayın.


Bu doğru yönde ilerliyor. İstediğiniz kadar ileriye bakabilmeniz ve gerektiğinde çok sayıda sonucu saklayabilmeniz için değiştirilmelidir. İdeal olarak, keyfi öğelerin akışın başına itilmesine izin verecektir. Bir itilebilir iteratör, sıklıkla kullandığım çok kullanışlı bir soyutlamadır.
sfkleach

@sfkleach Birden fazla gözetleme için bunu karmaşıklaştırma gereği görmüyorum, olduğu gibi oldukça kullanışlı ve soruyu cevaplıyor. Bu eski bir soru olmasına rağmen, hala ara sıra bir görünüm kazanıyor, bu yüzden kendi cevabınızı bırakmak isterseniz birileri faydalı olabilir.
Mark Ransom

Mark, çözümünün kilit nokta olan soruyu cevaplaması konusunda oldukça haklı. Daha iyi ifade etmeliydim. Demek istediğim sınırsız geri itme ile itilebilir iteratörler son derece yararlı bulduğum bir deyim ve uygulama tartışmasız daha basitti. Önerildiği gibi ben varyant kodu göndereceğiz.
sfkleach

2

Mark Ransom tarafından istendiğinde, ileriye bakabilmeniz, değerleri tekrar akışa aktarabilmeniz ve boş olup olmadığını kontrol edebilmeniz için herhangi bir yineleyiciyi sarmak için kullanabileceğiniz bir sınıf. Geçmişte çok kullanışlı bulduğum basit bir uygulama ile basit bir fikir.

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

Sadece bu konuya düştüm ve çok basit ve okunması kolay bir cevabın eksik olduğunu fark ettim:

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

Herhangi bir öğeyi tüketmememiz gerekiyorsa, ilk öğeyi jeneratöre yeniden enjekte etmemiz gerekir:

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

Misal:

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

Jeneratörün StopIterationsonunda, durumunuza hemen ulaşıldığı için istisna yükselir. Ancak normalde bir sonraki değerin varlığını kontrol etmemelisiniz.

yapabileceğiniz başka bir şey:

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

2
Aslında tüm jeneratörü tüketir. Ne yazık ki, bu istenen veya istenmeyen davranış olup olmadığı sorudan net değildir.
S.Lott

sanırım jeneratöre "dokunmanın" başka bir yolu gibi.
SilentGhost

Bunun eski olduğunu anlıyorum, ancak 'list ()' kullanmak en iyi yol olamaz, eğer oluşturulan liste boş değil ama aslında büyükse bu gereksiz bir
israftır

1

Jeneratörü kullanmadan önce bilmeniz gerekiyorsa , hayır, basit bir yol yoktur. Eğer beklemek durumunda sonra Jeneratörü kullanmış, basit bir yolu var:

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

Jeneratörü itertools.chain ile sarın, ikinci tekrarlanabilir olarak tekrarlanabilir sonunu temsil edecek bir şey koyun, sonra bunu kontrol edin.

Ör:

import itertools

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

Şimdi geriye kalan tek şey, yinelemenin sonuna eklediğimiz değeri kontrol etmektir, okuduğunuzda bu son anlamına gelir

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

Bunun asla tekrarlanamayacağını eog = object()varsaymak yerine kullanın float('-inf').
bfontaine

@bfontaine İyi fikir
smac89

1

Benim durumumda, öğeleri birleştiren bir işleve geçmeden önce bir dizi jeneratör doldurulup doldurulmadığını bilmem gerekiyordu, yani zip(...). Çözüm, kabul edilen cevaptan benzer, ancak yeterince farklıdır:

Tanım:

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

Kullanımı:

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"

Benim özel sorun yinelenebilir boş veya tam olarak aynı sayıda giriş özelliği vardır.


1

Ben sadece bu çözümü boş yinelemelerde çalışmak olarak buldum.

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)

Veya bunun için istisna kullanmak istemiyorsanız,

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)

In işaretli çözümü sizin gibi boş jeneratörler için kullanmak mümkün değildir

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

İşte bir şey verilip verilmediğini kontrol ederken bir yineleyici döndürmeye devam etmek için kullandığım basit bir yaklaşım sadece döngü çalışırsa kontrol edin:

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

0

İşte jeneratörü saran basit bir dekoratör, bu yüzden boşsa None döndürür. Kodunuzun, jeneratörün döngüden önce bir şey üretip üretmeyeceğini bilmesi gerekiyorsa bu yararlı olabilir .

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

Kullanımı:

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

Bunun yararlı olduğu bir örnek kodlamadadır - örn. Jinja2

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

Bu, jeneratör fonksiyonunu iki kez çağırır, bu nedenle jeneratörün başlangıç ​​maliyetine iki kez neden olur. Örneğin, jeneratör işlevi bir veritabanı sorgusu ise bu önemli olabilir.
Ian Goldby

0

islice kullanarak, boş olup olmadığını keşfetmek için sadece ilk yinelemeyi kontrol etmeniz gerekir.

itertools ithalat islice

def isempty (yinelenebilir):
    dönüş listesi (islice (yinelenebilir, 1)) == []


Maalesef, bu tüketici bir okuma ... StopIteration ile denemek / yakalamak zorunda
Quin

0

Herhangi bir () kullanmaya ne dersiniz? Jeneratörlerle kullanıyorum ve iyi çalışıyor. İşte bu konuda biraz açıklayan adam var


2
Her şey için "any ()" kullanamayız. Sadece birden fazla veri çerçevesi içeren bir jeneratör ile kullanmaya çalıştım. Bu iletiyi aldım "Bir DataFrame gerçek değeri belirsiz." herhangi bir (my_generator_of_df)
probitaille

any(generator)Jeneratörün verilebileceği değerleri üreteceğini bildiğinizde çalışır bool- temel veri türleri (örn. int, string) çalışması. any(generator)jeneratör boş olduğunda veya jeneratör yalnızca yanlış değerlere sahip olduğunda Yanlış olur - örneğin, bir jeneratör 0, '' (boş dize) ve False üretecekse, yine de Yanlış olur. Bu, farkında olduğunuz sürece amaçlanan davranış olabilir veya olmayabilir :)
Daniel

0

Sitoolzde peek fonksiyonunu kullanın .

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

Bu işlev tarafından döndürülen yineleyici, bağımsız değişken olarak iletilen orijinal işleve eşdeğer olacaktır.


-2

Sum fonksiyonunu kullanarak çözdüm. Glob.iglob (jeneratörü döndüren) ile kullandığım bir örnek için aşağıya bakın.

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

* Bu muhtemelen BÜYÜK jeneratörler için işe yaramaz, ancak daha küçük listeler için iyi performans göstermelidir

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.