Kembali atau hasilkan dari fungsi yang memanggil generator?

30

Saya punya generator generatordan juga metode kenyamanan untuk itu - generate_all.

def generator(some_list):
  for i in some_list:
    yield do_something(i)

def generate_all():
  some_list = get_the_list()
  return generator(some_list) # <-- Is this supposed to be return or yield?

Haruskah generate_all returnatau yield? Saya ingin para pengguna kedua metode untuk menggunakannya sama, yaitu

for x in generate_all()

harus sama dengan

some_list = get_the_list()
for x in generate(some_list)
hyankov
sumber
2
Ada alasan untuk menggunakan keduanya. Untuk contoh ini, return lebih efisien
Fisikawan Gila
1
Ini mengingatkan saya pada pertanyaan serupa yang pernah saya ajukan: "hasil dari iterable" vs "return iter (iterable)" . Meskipun tidak secara khusus tentang generator, pada dasarnya sama dengan generator dan iterator sangat mirip dengan python. Juga strategi membandingkan bytecode seperti yang diusulkan oleh jawabannya mungkin berguna di sini.
PeterE

Jawaban:

12

Generator malas mengevaluasi sehingga returnatau yieldakan berperilaku berbeda ketika Anda men-debug kode Anda atau jika ada pengecualian.

Dengan returnpengecualian yang terjadi di Anda generatortidak akan tahu apa-apa generate_all, itu karena ketika generatorbenar-benar dieksekusi Anda telah meninggalkan generate_allfungsi. Dengan yielddi sana akan ada generate_alldi traceback.

def generator(some_list):
    for i in some_list:
        raise Exception('exception happened :-)')
        yield i

def generate_all():
    some_list = [1,2,3]
    return generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-3-b19085eab3e1> in <module>
      8     return generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-3-b19085eab3e1> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

Dan jika menggunakan yield from:

def generate_all():
    some_list = [1,2,3]
    yield from generator(some_list)

for item in generate_all():
    ...
Exception                                 Traceback (most recent call last)
<ipython-input-4-be322887df35> in <module>
      8     yield from generator(some_list)
      9 
---> 10 for item in generate_all():
     11     ...

<ipython-input-4-be322887df35> in generate_all()
      6 def generate_all():
      7     some_list = [1,2,3]
----> 8     yield from generator(some_list)
      9 
     10 for item in generate_all():

<ipython-input-4-be322887df35> in generator(some_list)
      1 def generator(some_list):
      2     for i in some_list:
----> 3         raise Exception('exception happened :-)')
      4         yield i
      5 

Exception: exception happened :-)

Namun ini datang pada biaya kinerja. Lapisan generator tambahan memang memiliki beberapa overhead. Jadi returnumumnya akan sedikit lebih cepat daripada yield from ...(atau for item in ...: yield item). Dalam kebanyakan kasus ini tidak akan terlalu menjadi masalah, karena apa pun yang Anda lakukan di generator biasanya mendominasi run-time sehingga lapisan tambahan tidak akan terlihat.

Namun yieldmemiliki beberapa keuntungan tambahan: Anda tidak terbatas pada satu iterable saja, Anda juga dapat dengan mudah menghasilkan item tambahan:

def generator(some_list):
    for i in some_list:
        yield i

def generate_all():
    some_list = [1,2,3]
    yield 'start'
    yield from generator(some_list)
    yield 'end'

for item in generate_all():
    print(item)
start
1
2
3
end

Dalam kasus Anda, operasinya cukup sederhana dan saya tidak tahu apakah perlu membuat beberapa fungsi untuk ini, orang dapat dengan mudah menggunakan built-in mapatau ekspresi generator sebagai gantinya:

map(do_something, get_the_list())          # map
(do_something(i) for i in get_the_list())  # generator expression

Keduanya harus identik (kecuali untuk beberapa perbedaan ketika pengecualian terjadi) untuk digunakan. Dan jika mereka membutuhkan nama yang lebih deskriptif, maka Anda masih bisa membungkusnya dalam satu fungsi.

Ada beberapa pembantu yang membungkus operasi yang sangat umum pada iterables bawaan dan yang lebih lanjut dapat ditemukan dalam itertoolsmodul bawaan. Dalam kasus sederhana seperti itu saya hanya akan menggunakan ini dan hanya untuk kasus non-sepele tulis generator Anda sendiri.

Tapi saya menganggap kode Anda yang sebenarnya lebih rumit sehingga mungkin tidak berlaku tetapi saya pikir itu tidak akan menjadi jawaban yang lengkap tanpa menyebutkan alternatif.

MSeifert
sumber
17

Anda mungkin mencari Delegasi Generator (PEP380)

Untuk iterator sederhana, yield from iterablepada dasarnya hanyalah bentuk singkat darifor item in iterable: yield item

def generator(iterable):
  for i in iterable:
    yield do_something(i)

def generate_all():
  yield from generator(get_the_list())

Ini cukup ringkas dan juga memiliki sejumlah keunggulan lain, seperti dapat rantai rantai yang sewenang-wenang / berbeda!

ti7
sumber
Oh maksudmu penamaan itu list? Ini contoh yang buruk, bukan kode asli yang ditempelkan dalam pertanyaan, saya mungkin harus mengeditnya.
hyankov
Ya - jangan takut, saya cukup bersalah atas kode contoh yang bahkan tidak akan berjalan pada awalnya bertanya ..
ti7
2
Yang pertama juga bisa menjadi satu-liner :). yield from map(do_something, iterable)atau bahkanyield from (do_something(x) for x in iterable)
Fisikawan Gila
2
"Ini contoh kode sepenuhnya!"
ti7
3
Anda hanya perlu pendelegasian jika Anda sendiri, melakukan sesuatu selain mengembalikan generator baru. Jika Anda baru saja mengembalikan generator baru, tidak perlu ada delegasi. Jadi yield fromtidak ada gunanya kecuali pembungkus Anda melakukan sesuatu yang lain generator-y.
ShadowRanger
14

return generator(list)lakukan apa yang kamu inginkan. Tapi perhatikan itu

yield from generator(list)

akan setara, tetapi dengan kesempatan untuk menghasilkan lebih banyak nilai setelah generatorhabis. Sebagai contoh:

def generator_all_and_then_some():
    list = get_the_list()
    yield from generator(list)
    yield "one last thing"
chepner
sumber
5
Saya percaya ada perbedaan kecil antara yield fromdan returnketika konsumen generator throwspengecualian di dalamnya - dan dengan operasi lain yang dipengaruhi oleh jejak tumpukan.
WorldSEnder
9

Dua pernyataan berikut akan tampak setara secara fungsional dalam kasus khusus ini:

return generator(list)

dan

yield from generator(list)

Nantinya kira-kira sama dengan

for i in generator(list):
    yield i

The returnpernyataan mengembalikan generator yang Anda cari. Sebuah yield fromatau yieldpernyataan ternyata seluruh fungsi Anda menjadi sesuatu yang kembali generator, yang melewati salah satu yang Anda cari.

Dari sudut pandang pengguna, tidak ada perbedaan. Namun secara internal, returnini bisa dibilang lebih efisien karena tidak membungkus generator(list)generator pass-thru yang berlebihan. Jika Anda berencana untuk melakukan setiap proses pada unsur-unsur dari generator dibungkus, menggunakan beberapa bentuk yieldtentu saja.

Fisikawan Gila
sumber
4

Anda akan returnmelakukannya.

yielding * akan menyebabkan generate_all()mengevaluasi ke generator itu sendiri, dan memanggil nextgenerator luar itu akan mengembalikan generator batin yang dikembalikan oleh fungsi pertama, yang bukan yang Anda inginkan.

* Tidak termasuk yield from

Carcigenicate
sumber