Fungsi Generator Kosong Python

99

Dalam python, seseorang dapat dengan mudah mendefinisikan fungsi iterator, dengan meletakkan kata kunci hasil di badan fungsi, seperti:

def gen():
    for i in range(100):
        yield i

Bagaimana saya bisa mendefinisikan fungsi generator yang tidak menghasilkan nilai (menghasilkan 0 nilai), kode berikut tidak berfungsi, karena python tidak dapat mengetahui bahwa itu seharusnya generator dan bukan fungsi normal:

def empty():
    pass

Saya bisa melakukan sesuatu seperti

def empty():
    if False:
        yield None

Tapi itu akan sangat buruk. Apakah ada cara yang bagus untuk mewujudkan fungsi iterator yang kosong?

Konstantin Weitz
sumber

Jawaban:

133

Anda dapat menggunakan returnsekali di generator; itu menghentikan iterasi tanpa menghasilkan apa pun, dan dengan demikian memberikan alternatif eksplisit untuk membiarkan fungsi kehabisan ruang lingkup. Jadi gunakan yielduntuk mengubah fungsi menjadi generator, tetapi awali dengan returnmenghentikan generator sebelum menghasilkan apa pun.

>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

Saya tidak yakin itu jauh lebih baik dari yang Anda miliki - itu hanya menggantikan ifpernyataan tidak ada operasi dengan pernyataan tidak ada operasi yield. Tapi itu lebih idiomatis. Perhatikan bahwa hanya menggunakan yieldtidak berhasil.

>>> def f():
...     yield
... 
>>> list(f())
[None]

Mengapa tidak digunakan saja iter(())?

Pertanyaan ini menanyakan secara khusus tentang fungsi generator yang kosong . Untuk alasan itu, saya menganggapnya sebagai pertanyaan tentang konsistensi internal sintaksis Python, daripada pertanyaan tentang cara terbaik untuk membuat iterator kosong secara umum.

Jika pertanyaan sebenarnya tentang cara terbaik untuk membuat iterator kosong, maka Anda mungkin setuju dengan Zectbumo tentang penggunaan iter(()). Namun, penting untuk diperhatikaniter(()) tidak mengembalikan fungsi! Ini secara langsung mengembalikan iterable kosong. Misalkan Anda bekerja dengan API yang mengharapkan callable yang mengembalikan iterable. Anda harus melakukan sesuatu seperti ini:

def empty():
    return iter(())

( Penghargaan harus diberikan kepada Unutbu karena memberikan versi pertama yang benar dari jawaban ini.)

Sekarang, Anda mungkin menemukan penjelasan di atas lebih jelas, tetapi saya bisa membayangkan situasi di mana itu menjadi kurang jelas. Pertimbangkan contoh daftar panjang definisi fungsi generator (dibuat-buat) ini:

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

Di akhir daftar panjang itu, saya lebih suka melihat sesuatu yang mengandung a yield, seperti ini:

def empty():
    return
    yield

atau, dengan Python 3.3 dan yang lebih baru (seperti yang disarankan oleh DSM ), ini:

def empty():
    yield from ()

Kehadiran yieldkata kunci membuatnya jelas sekilas bahwa ini hanyalah fungsi generator, persis seperti yang lainnya. Perlu waktu lebih lama untuk melihat bahwa iter(())versi tersebut melakukan hal yang sama.

Ini perbedaan yang halus, tapi sejujurnya menurut saya yieldfungsi berbasis lebih mudah dibaca dan dipelihara.

pengirim
sumber
Ini memang lebih baik daripada if False: yieldtetapi masih agak membingungkan bagi orang-orang yang tidak mengetahui pola ini
Konstantin Weitz
1
Ew, sesuatu setelah kembali? Saya mengharapkan sesuatu seperti itertools.empty().
Grault
1
@Jesdisciple, yah, returnberarti sesuatu yang berbeda di dalam generator. Ini lebih seperti break.
pengirim
Saya suka solusi ini karena (relatif) ringkas, dan tidak melakukan pekerjaan tambahan seperti membandingkan False.
Pi Marillion
Jawaban Unutbu bukanlah "fungsi generator yang benar" seperti yang Anda sebutkan karena mengembalikan sebuah iterator.
Zectbumo
71
iter(())

Anda tidak membutuhkan generator. Ayo teman-teman!

Zectbumo
sumber
3
Saya sangat menyukai jawaban ini yang terbaik. Cepat, mudah untuk menulis, cepat dalam eksekusi, dan lebih menarik bagi saya daripada iter([])fakta sederhana yang ()konstan sementara []dapat membuat contoh objek daftar baru dalam memori setiap kali dipanggil.
Mumbleskates
2
Bekerja kembali melalui utas ini, saya merasa terdorong untuk menunjukkan bahwa jika Anda menginginkan pengganti drop-in yang sebenarnya untuk fungsi generator, Anda harus menulis sesuatu seperti empty = lambda: iter(())atau def empty(): return iter(()).
pengirim
Jika Anda harus memiliki generator maka Anda sebaiknya menggunakan (_ for _ in ()) seperti yang disarankan orang lain
Zectbumo
1
@ Zectbumo, itu masih bukan fungsi generator . Itu hanya generator. Fungsi generator mengembalikan generator baru setiap kali dipanggil.
senderle
1
Ini mengembalikan a, tuple_iteratorbukan generator. Jika Anda memiliki kasus di mana generator Anda tidak perlu mengembalikan apa pun, jangan gunakan jawaban ini.
Boris
53

Python 3.3 (karena saya sedang yield frombersemangat, dan karena @senderle mencuri pikiran pertama saya):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

Tetapi saya harus mengakui, saya kesulitan menemukan kasus penggunaan untuk ini yang mana iter([])atau (x)range(0)tidak akan bekerja sama baiknya.

DSM
sumber
Saya sangat suka sintaks ini. hasil dari luar biasa!
Konstantin Weitz
2
Saya pikir ini jauh lebih mudah dibaca oleh seorang pemula daripada return; yieldatau if False: yield None.
abarnert
1
Ini adalah solusi paling elegan
Maksym Ganenko
"Tapi harus saya akui, saya kesulitan memikirkan kasus penggunaan untuk ini yang mana iter([])atau (x)range(0)tidak akan bekerja sama baiknya." -> Tidak yakin apa (x)range(0)itu, tetapi kasus penggunaan bisa menjadi metode yang dimaksudkan untuk diganti dengan generator yang lengkap di beberapa kelas yang diturunkan. Untuk tujuan konsistensi, Anda bahkan ingin yang basis, dari mana orang lain mewarisi, untuk mengembalikan generator seperti yang menimpanya.
Vedran Šego
19

Pilihan lainnya adalah:

(_ for _ in ())
Ben Reynwar
sumber
Tidak seperti opsi lain, Pycharm menganggap ini konsisten dengan petunjuk tipe standar yang digunakan untuk generator, sepertiGenerator[str, Any, None]
Michał Jabłoński
3

Apakah itu fungsi generator? Jika tidak, bagaimana dengan

def f():
    return iter(())
unutbu
sumber
2

Cara "standar" untuk membuat iterator kosong tampak seperti iter ([]). Saya menyarankan untuk membuat [] argumen default ke iter (); ini ditolak dengan argumen yang bagus, lihat http://bugs.python.org/issue25215 - Jurjen

Jurjen Bos
sumber
2

Seperti yang dikatakan @senderle, gunakan ini:

def empty():
    return
    yield

Saya menulis jawaban ini sebagian besar untuk membagikan pembenaran lain untuk itu.

Salah satu alasan untuk memilih solusi ini di atas yang lain adalah karena solusi ini optimal sejauh menyangkut penafsir.

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

Seperti yang bisa kita lihat, the empty_returnmemiliki bytecode yang sama persis dengan fungsi kosong biasa; sisanya melakukan sejumlah operasi lain yang tidak mengubah perilakunya. Satu-satunya perbedaan antara empty_returndan noopadalah bahwa yang pertama memiliki set flag generator:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

Tentu saja, kekuatan argumen ini sangat bergantung pada implementasi khusus Python yang digunakan; juru bahasa alternatif yang cukup cerdas mungkin memperhatikan bahwa operasi lain tidak berguna dan mengoptimalkannya. Namun, meskipun ada pengoptimalan seperti itu, pengoptimalan tersebut memerlukan waktu untuk melaksanakannya dan untuk menjaga agar asumsi pengoptimalan tidak rusak, seperti iterpengenal pada cakupan global yang dipantulkan ke sesuatu yang lain (meskipun kemungkinan besar akan menunjukkan bug jika itu sebenarnya terjadi). Dalam hal empty_returntidak ada yang bisa dioptimalkan, jadi CPython yang relatif naif tidak akan membuang waktu untuk operasi palsu.

pengguna3840170
sumber
0
generator = (item for item in [])

sumber
0

Saya ingin memberikan contoh berbasis kelas karena kami belum memiliki saran apa pun. Ini adalah iterator yang dapat dipanggil yang tidak menghasilkan item. Saya yakin ini adalah cara langsung dan deskriptif untuk menyelesaikan masalah.

class EmptyGenerator:
    def __iter__(self):
        return self
    def __next__(self):
        raise StopIteration

>>> list(EmptyGenerator())
[]
Zectbumo
sumber
Bisakah Anda menambahkan penjelasan mengapa / bagaimana ini bekerja untuk menyelesaikan masalah OP?
SherylHohman
Harap jangan hanya memposting kode sebagai jawaban, tetapi juga berikan penjelasan tentang fungsi kode Anda dan bagaimana kode tersebut menyelesaikan masalah pertanyaan. Jawaban dengan penjelasan biasanya lebih bermanfaat dan berkualitas lebih baik, dan lebih cenderung menarik suara positif.
SherylHohman