Bagaimana saya tahu jika generator kosong dari awal?


146

Apakah ada cara sederhana untuk menguji jika generator memiliki item, seperti peek, hasNext, isEmpty, sesuatu sepanjang garis?


Koreksi saya jika saya salah, tetapi jika Anda bisa membuat solusi yang benar-benar generik untuk generator apa pun , itu akan sama dengan menetapkan breakpoint pada pernyataan hasil dan memiliki kemampuan untuk "mundur". Apakah itu berarti mengkloning frame stack pada hasil dan mengembalikannya pada StopIteration?

Yah, kurasa kembalikan mereka StopIteration atau tidak, tapi setidaknya StopIteration akan memberitahumu itu kosong. Ya aku perlu tidur ...

4
Kurasa aku tahu mengapa dia menginginkan ini. Jika Anda melakukan pengembangan web dengan templat, dan meneruskan nilai pengembalian ke templat seperti Cheetah atau apalah, daftar kosong []itu mudah digunakan Falsey sehingga Anda dapat melakukan centang pada isinya dan melakukan perilaku khusus untuk sesuatu atau tidak sama sekali. Generator benar bahkan jika mereka tidak menghasilkan elemen.
jpsimons

Inilah kasus penggunaan saya ... Saya menggunakan glob.iglob("filepattern")pola wildcard yang disediakan pengguna, dan saya ingin memperingatkan pengguna jika polanya tidak cocok dengan file apa pun. Tentu saya bisa mengatasi ini dengan berbagai cara, tetapi berguna untuk dapat dengan bersih menguji apakah iteratornya kosong atau tidak.
LarsH

Dapat menggunakan solusi ini: stackoverflow.com/a/11467686/463758
balki

Jawaban:


53

Jawaban sederhana untuk pertanyaan Anda: tidak, tidak ada cara sederhana. Ada banyak pekerjaan di sekitar.

Seharusnya tidak ada cara yang sederhana, karena apa itu generator: cara untuk menghasilkan urutan nilai tanpa memegang urutan dalam memori . Jadi tidak ada traversal mundur.

Anda bisa menulis fungsi has_next atau bahkan mungkin menamparnya ke generator sebagai metode dengan dekorator mewah jika Anda mau.


2
cukup adil, itu masuk akal. saya tahu tidak ada cara untuk menemukan panjang generator, tetapi saya pikir saya mungkin telah melewatkan cara untuk menemukan apakah pada awalnya akan menghasilkan apa pun.
Dan

1
Oh, dan untuk referensi, saya mencoba menerapkan saran "penghias mewah" saya sendiri. KERAS. Rupanya copy.deepcopy tidak berfungsi pada generator.
David Berger

47
Saya tidak yakin saya bisa setuju dengan "seharusnya tidak ada cara sederhana". Ada banyak abstraksi dalam ilmu komputer yang dirancang untuk menampilkan urutan nilai tanpa memegang urutan dalam memori, tetapi itu memungkinkan pemrogram untuk bertanya apakah ada nilai lain tanpa menghapusnya dari "antrian" jika ada. Ada yang namanya mengintip ke depan tanpa memerlukan "traversal mundur". Itu tidak berarti bahwa desain iterator harus menyediakan fitur seperti itu, tetapi itu pasti berguna. Mungkin Anda keberatan atas dasar bahwa nilai pertama mungkin berubah setelah mengintip?
LarsH

9
Saya keberatan dengan alasan bahwa implementasi yang khas bahkan tidak menghitung nilai sampai dibutuhkan. Seseorang dapat memaksa antarmuka untuk melakukan ini, tetapi itu mungkin kurang optimal untuk implementasi yang ringan.
David Berger

6
@ S.Lott Anda tidak perlu membuat seluruh urutan untuk mengetahui apakah urutannya kosong atau tidak. Nilai penyimpanan satu elemen sudah cukup - lihat jawaban saya.
Mark Ransom

98

Saran:

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

Pemakaian:

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
Saya tidak mengerti maksud mengembalikan elemen pertama dua kali return first, itertools.chain([first], rest).
njzk2

6
@ njzk2 Saya akan pergi untuk operasi "mengintip" (maka nama fungsi). wiki "mengintip adalah operasi yang mengembalikan nilai bagian atas koleksi tanpa menghapus nilai dari data"
John Fouhy

Ini tidak akan berfungsi jika generator dirancang untuk menghasilkan None. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul

4
@ Paul Perhatikan nilai pengembalian dengan cermat. Jika generator dilakukan - yaitu, tidak kembali None, tetapi menaikkan StopIteration- hasil dari fungsinya None. Kalau tidak, itu adalah tuple, yang bukan None.
Dana Gugatan Monica

Ini banyak membantu saya dengan proyek saya saat ini. Saya menemukan contoh serupa dalam kode untuk modul perpustakaan standar 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
rekan

29

Cara sederhana adalah dengan menggunakan parameter opsional untuk next () yang digunakan jika generator habis (atau kosong). Sebagai contoh:

iterable = some_generator()

_exhausted = object()

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

Sunting: Memperbaiki masalah yang ditunjukkan dalam komentar mehtunguh.


1
Tidak. Ini tidak benar untuk generator mana pun di mana nilai pertama yang dihasilkan tidak benar.
mehtunguh

7
Gunakan object()bukannya classuntuk menjadikannya salah satu baris pendek: _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa

13

next(generator, None) is not None

Atau ganti Nonetetapi nilai apa pun yang Anda tahu itu tidak ada di generator Anda.

Sunting : Ya, ini akan melewati 1 item dalam generator. Namun, sering kali saya memeriksa apakah generator kosong hanya untuk keperluan validasi, maka jangan benar-benar menggunakannya. Atau kalau tidak, saya melakukan sesuatu seperti:

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

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

Artinya, ini berfungsi jika generator Anda berasal dari suatu fungsi , seperti pada generator().


4
Kenapa ini bukan jawaban terbaik? Jika generator kembali None?
Sait

8
Mungkin karena ini memaksa Anda untuk benar-benar mengkonsumsi generator daripada hanya menguji apakah itu kosong.
bfontaine

3
Ini buruk karena saat Anda memanggil berikutnya (generator, Tidak ada) Anda akan melewati 1 item jika tersedia
Nathan Do

Benar, Anda akan kehilangan elemen pertama gen Anda dan juga Anda akan mengkonsumsi gen Anda, bukan menguji apakah itu kosong.
AJ

12

Pendekatan terbaik, IMHO, akan menghindari tes khusus. Sering kali, penggunaan generator adalah tes:

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)

Jika itu tidak cukup baik, Anda masih dapat melakukan tes eksplisit. Pada titik ini, thingakan berisi nilai terakhir yang dihasilkan. Jika tidak ada yang dihasilkan, itu tidak akan ditentukan - kecuali Anda sudah mendefinisikan variabel. Anda bisa memeriksa nilainya thing, tapi itu agak tidak bisa diandalkan. Alih-alih, cukup atur bendera di dalam blok dan periksa setelahnya:

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

3
Solusi ini akan mencoba untuk mengkonsumsi seluruh generator sehingga tidak dapat digunakan untuk generator yang tak terbatas.
Viktor Stískala

@ ViktorStískala: Saya tidak mengerti maksud Anda. Akan bodoh untuk menguji apakah generator yang tak terbatas menghasilkan hasil apa pun.
vezult

Saya ingin menunjukkan bahwa solusi Anda dapat berisi break di for loop, karena Anda tidak memproses hasil lainnya dan tidak berguna bagi mereka untuk dihasilkan. range(10000000)adalah generator yang terbatas (Python 3), tetapi Anda tidak perlu memeriksa semua item untuk mengetahui apakah itu menghasilkan sesuatu.
Viktor Stískala

1
@ ViktorStískala: Dipahami. Namun, maksud saya adalah ini: Secara umum, Anda sebenarnya ingin beroperasi pada output generator. Dalam contoh saya, jika tidak ada yang dihasilkan, Anda sekarang tahu itu. Kalau tidak, Anda beroperasi pada output yang dihasilkan sebagaimana dimaksud - "Penggunaan generator adalah tes". Tidak perlu untuk tes khusus, atau mengkonsumsi keluaran generator secara sia-sia. Saya telah mengedit jawaban saya untuk memperjelas ini.
vezult

8

Saya benci untuk menawarkan solusi kedua, terutama yang saya tidak akan menggunakan sendiri, tetapi, jika Anda benar - benar harus melakukan ini dan tidak mengkonsumsi generator, seperti dalam jawaban lain:

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)

Sekarang saya benar-benar tidak menyukai solusi ini, karena saya percaya ini bukan bagaimana generator harus digunakan.


4

Saya menyadari bahwa pos ini sudah berusia 5 tahun pada saat ini, tetapi saya menemukannya saat mencari cara idiomatis untuk melakukan ini, dan tidak melihat solusi saya diposting. Jadi untuk anak cucu:

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)

Tentu saja, karena saya yakin banyak komentator akan menunjukkan, ini adalah hacky dan hanya berfungsi sama sekali dalam situasi terbatas tertentu (misalnya generator bebas efek samping, misalnya). YMMV.


1
Ini hanya akan memanggil gengenerator satu kali untuk setiap item, sehingga efek sampingnya tidak terlalu buruk. Tapi itu akan menyimpan salinan dari segala sesuatu yang telah ditarik dari generator melalui b, tetapi tidak melalui a, sehingga implikasi memori mirip dengan hanya menjalankan list(gen)dan memeriksa itu.
Matthias Fripp

Ini memiliki dua masalah. 1. Itertool ini mungkin memerlukan penyimpanan tambahan yang signifikan (tergantung pada berapa banyak data sementara yang perlu disimpan). Secara umum, jika satu iterator menggunakan sebagian besar atau semua data sebelum iterator lain dimulai, lebih cepat menggunakan list () daripada tee (). 2. tee iterators bukan threadsafe. RuntimeError dapat dimunculkan saat menggunakan secara bersamaan iterator yang dikembalikan oleh panggilan tee () yang sama, bahkan jika iterable aslinya adalah threadsafe.
AJ

3

Maaf atas pendekatan yang jelas, tetapi cara terbaik adalah melakukan:

for item in my_generator:
     print item

Sekarang Anda telah mendeteksi bahwa generator kosong saat Anda menggunakannya. Tentu saja, item tidak akan pernah ditampilkan jika generator kosong.

Ini mungkin tidak cocok dengan kode Anda, tetapi ini adalah ungkapan untuk generator: iterasi, jadi mungkin Anda mungkin sedikit mengubah pendekatan Anda, atau tidak menggunakan generator sama sekali.


Atau ... penanya dapat memberikan beberapa petunjuk mengapa seseorang mencoba mendeteksi generator kosong?
S.Lott

maksud Anda "tidak ada yang ditampilkan karena generator kosong"?
SilentGhost

S.Lott. Saya setuju. Saya tidak mengerti mengapa. Tapi saya pikir bahkan jika ada alasan, masalahnya mungkin lebih baik berbalik menggunakan setiap item sebagai gantinya.
Ali Afshar

1
Ini tidak memberi tahu program jika generator kosong.
Ethan Furman

3

Yang perlu Anda lakukan untuk melihat apakah generator kosong adalah mencoba untuk mendapatkan hasil selanjutnya. Tentu saja jika Anda tidak siap untuk menggunakan hasil itu maka Anda harus menyimpannya untuk mengembalikannya lagi nanti.

Berikut kelas pembungkus yang dapat ditambahkan ke iterator yang ada untuk menambahkan __nonzero__tes, sehingga Anda dapat melihat apakah generator kosong dengan sederhana if. Mungkin juga bisa diubah menjadi dekorator.

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)

Begini cara Anda menggunakannya:

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

Perhatikan bahwa Anda dapat memeriksa kekosongan setiap saat, tidak hanya pada awal iterasi.


Ini menuju ke arah yang benar. Ini harus dimodifikasi untuk memungkinkan mengintip ke depan sejauh yang Anda inginkan, menyimpan hasil sebanyak yang diperlukan. Idealnya itu akan memungkinkan untuk mendorong barang sewenang-wenang ke kepala sungai. Pushable-iterator adalah abstraksi yang sangat berguna yang sering saya gunakan.
sfkleach

@ sfkleach Saya tidak melihat perlunya menyulitkan ini untuk mengintip ke depan, ini cukup berguna dan menjawab pertanyaan. Meskipun ini adalah pertanyaan lama, namun masih sering muncul, jadi jika Anda ingin meninggalkan jawaban Anda sendiri, seseorang mungkin menganggapnya berguna.
Mark Ransom

Mark benar bahwa solusinya menjawab pertanyaan, yang merupakan poin kuncinya. Aku seharusnya mengatakannya dengan lebih baik. Apa yang saya maksud adalah bahwa pushable-iterators dengan pushback tanpa batas adalah ungkapan yang saya temukan sangat berguna & implementasinya bisa dibilang lebih sederhana. Seperti yang disarankan saya akan memposting kode varian.
sfkleach

2

Diminta oleh Mark Ransom, inilah kelas yang bisa Anda gunakan untuk membungkus iterator apa pun sehingga Anda bisa mengintip ke depan, mendorong nilai kembali ke aliran dan memeriksa kosong. Ini adalah ide sederhana dengan implementasi sederhana yang saya temukan sangat berguna di masa lalu.

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

Hanya jatuh di utas ini dan menyadari bahwa jawaban yang sangat sederhana dan mudah dibaca hilang:

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

Jika kita tidak seharusnya mengkonsumsi item apa pun maka kita perlu menyuntikkan item pertama ke generator:

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

Contoh:

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

Pada akhir generator StopIterationdinaikkan, karena dalam kasus Anda akhir tercapai segera, pengecualian dinaikkan. Tetapi biasanya Anda tidak harus memeriksa keberadaan nilai berikutnya.

hal lain yang dapat Anda lakukan adalah:

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

2
Yang mana sebenarnya menghabiskan seluruh generator. Sayangnya, tidak jelas dari pertanyaan apakah ini perilaku yang diinginkan atau tidak diinginkan.
S.Lott

sebagai cara lain untuk "menyentuh" ​​generator, saya kira.
SilentGhost

Saya menyadari ini sudah tua, tetapi menggunakan 'list ()' tidak bisa menjadi cara terbaik, jika daftar yang dihasilkan tidak kosong tetapi pada kenyataannya besar maka ini sia-sia sia-sia
Chris_Rands

1

Jika Anda perlu tahu sebelum menggunakan generator, maka tidak, tidak ada cara sederhana. Jika Anda bisa menunggu sampai setelah menggunakan generator, ada cara sederhana:

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

Cukup bungkus generator dengan itertools.chain , letakkan sesuatu yang akan mewakili akhir iterable sebagai iterable kedua, kemudian cukup periksa untuk itu.

Ex:

import itertools

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

Sekarang yang tersisa adalah untuk memeriksa nilai yang kita tambahkan ke akhir iterable, ketika Anda membacanya maka itu akan menandakan akhir

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

Gunakan eog = object()alih-alih dengan asumsi bahwa float('-inf')tidak akan pernah terjadi di iterable.
bfontaine

@bfontaine Ide bagus
smac89

1

Dalam kasus saya, saya perlu tahu apakah sejumlah generator sudah diisi sebelum saya meneruskannya ke fungsi, yang menggabungkan item, yaitu zip(...),. Solusinya mirip, tetapi cukup berbeda, dari jawaban yang diterima:

Definisi:

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

Pemakaian:

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"

Masalah khusus saya memiliki properti bahwa iterables kosong atau memiliki jumlah entri yang persis sama.


1

Saya hanya menemukan solusi ini berfungsi untuk iterasi kosong juga.

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)

Atau jika Anda tidak ingin menggunakan pengecualian untuk ini coba gunakan

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)

Dalam solusi yang ditandai Anda tidak dapat menggunakannya untuk generator kosong seperti

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

Berikut ini adalah pendekatan sederhana saya yang saya gunakan untuk terus mengembalikan iterator sambil memeriksa apakah ada sesuatu yang dihasilkan. Saya hanya memeriksa apakah loop berjalan:

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

0

Berikut adalah dekorator sederhana yang membungkus generator, sehingga tidak menghasilkan apa-apa jika kosong. Ini bisa berguna jika kode Anda perlu tahu apakah generator akan menghasilkan sesuatu sebelum mengulanginya.

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

Pemakaian:

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

Salah satu contoh di mana ini berguna adalah dalam templating code - ie jinja2

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

Ini memanggil fungsi generator dua kali, sehingga akan dikenakan biaya start-up generator dua kali. Itu bisa sangat besar jika, misalnya, fungsi generator adalah permintaan basis data.
Ian Goldby

0

menggunakan islice Anda hanya perlu memeriksa hingga iterasi pertama untuk mengetahui apakah isinya kosong.

dari itertools import islice

def isempty (iterable):
    daftar kembali (islice (iterable, 1)) == []


Maaf, ini adalah bacaan konsumtif ... Harus mencoba / menangkap dengan StopIteration
Quin

0

Bagaimana dengan menggunakan ()? Saya menggunakannya dengan generator dan berfungsi dengan baik. Di sini ada seorang pria yang menjelaskan sedikit tentang ini


2
Kita tidak bisa menggunakan "any ()" untuk generator semuanya. Hanya mencoba menggunakannya dengan generator yang berisi banyak dataframe. Saya mendapat pesan ini "Nilai kebenaran dari DataFrame adalah ambigu." pada setiap (my_generator_of_df)
probitaille

any(generator)berfungsi saat Anda tahu generator akan menghasilkan nilai yang dapat digunakan untuk bool- tipe data dasar (mis. int, string) berfungsi. any(generator)akan menjadi False ketika generator kosong, atau ketika generator hanya memiliki nilai false - misalnya, jika generator akan menghasilkan 0, '' (string kosong), dan False, maka itu akan tetap salah. Ini mungkin atau mungkin bukan perilaku yang diinginkan, asalkan Anda menyadarinya :)
Daniel

0

Gunakan fungsi mengintip di 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 yang dikembalikan oleh fungsi ini akan sama dengan yang asli yang diteruskan sebagai argumen.


-2

Saya menyelesaikannya dengan menggunakan fungsi penjumlahan. Lihat di bawah untuk contoh yang saya gunakan dengan glob.iglob (yang mengembalikan generator).

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

* Ini mungkin tidak akan berfungsi untuk generator BESAR tetapi harus berkinerja baik untuk daftar yang lebih kecil

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.