Kata "hasil" memiliki dua arti: untuk menghasilkan sesuatu (misalnya, untuk menghasilkan jagung), dan untuk menghentikan membiarkan orang / hal lain terus berlanjut (misalnya, mobil yang menghasilkan untuk pejalan kaki). Kedua definisi tersebut berlaku untuk yieldkata kunci Python ; apa yang membuat fungsi generator istimewa adalah bahwa tidak seperti dalam fungsi biasa, nilai-nilai dapat "dikembalikan" ke pemanggil sementara hanya menjeda, tidak mengakhiri, fungsi generator.
Paling mudah membayangkan generator sebagai salah satu ujung pipa dua arah dengan ujung "kiri" dan ujung "kanan"; pipa ini adalah medium di mana nilai dikirim antara generator itu sendiri dan tubuh fungsi generator. Setiap ujung pipa memiliki dua operasi push:, yang mengirimkan nilai dan blok sampai ujung pipa lainnya menarik nilai, dan tidak mengembalikan apa pun; danpull, yang memblokir sampai ujung pipa mendorong nilai, dan mengembalikan nilai yang didorong. Saat runtime, eksekusi memantul bolak-balik antara konteks di kedua sisi pipa - setiap sisi berjalan hingga mengirim nilai ke sisi lain, di mana titik itu berhenti, membiarkan sisi lain berjalan, dan menunggu nilai dalam kembali, di mana sisi lain berhenti dan dilanjutkan. Dengan kata lain, setiap ujung pipa beroperasi dari saat menerima nilai hingga saat mengirimkan nilai.
Pipa fungsional simetris, tetapi - dengan konvensi saya mendefinisikan dalam jawaban ini - ujung kiri hanya tersedia di dalam tubuh fungsi generator dan dapat diakses melalui yieldkata kunci, sedangkan ujung kanan adalah generator dan dapat diakses melalui sendfungsi generator . Sebagai antarmuka tunggal ke masing-masing ujung pipa, yielddan sendmelakukan tugas ganda: mereka masing-masing mendorong dan menarik nilai ke / dari ujung pipa, yieldmendorong ke kanan dan menarik ke kiri sementara sendmelakukan sebaliknya. Tugas ganda ini adalah inti dari kebingungan seputar semantik pernyataan seperti x = yield y. Memecah yield- mecah sendmenjadi dua langkah push / pull yang eksplisit akan membuat semantiknya lebih jelas:
- Misalkan
ggenerator. g.sendmendorong nilai ke kiri melalui ujung kanan pipa.
- Eksekusi dalam konteks
gjeda, memungkinkan tubuh fungsi generator dijalankan.
- Nilai yang didorong oleh
g.sendditarik ke kiri oleh yielddan diterima di ujung kiri pipa. In x = yield y, xditugaskan ke nilai yang ditarik.
- Eksekusi berlanjut di dalam tubuh fungsi generator hingga baris berikutnya yang berisi
yieldtercapai.
yieldmendorong nilai ke kanan melalui ujung kiri pipa, kembali ke g.send. Di x = yield y, ydidorong ke kanan melalui pipa.
- Eksekusi di dalam tubuh fungsi generator berhenti, yang memungkinkan lingkup luar untuk melanjutkan di mana ia tinggalkan.
g.send resume dan menarik nilai dan mengembalikannya kepada pengguna.
- Kapan
g.sendselanjutnya dipanggil, kembali ke Langkah 1.
Meskipun bersifat siklus, prosedur ini memang memiliki permulaan: kapan g.send(None)- yang merupakan next(g)kependekan dari - pertama kali disebut (adalah ilegal untuk melewatkan sesuatu selain dari panggilan Nonepertama send). Dan itu mungkin memiliki akhir: ketika tidak ada lagi yieldpernyataan yang harus dicapai dalam tubuh fungsi generator.
Apakah Anda melihat apa yang membuat yieldpernyataan (atau lebih tepatnya, generator) begitu istimewa? Berbeda dengan returnkata kunci yang sangat sedikit , yieldmampu memberikan nilai kepada peneleponnya dan menerima nilai dari peneleponnya semuanya tanpa menghentikan fungsi yang dihadapinya! (Tentu saja, jika Anda ingin menghentikan suatu fungsi - atau generator - itu berguna untuk memiliki returnkata kunci juga.) Ketika sebuah yieldpernyataan ditemui, fungsi generator hanya berhenti, dan kemudian mengambil kembali tepat di tempat itu meninggalkan off saat dikirim nilai lain. Dan sendhanya antarmuka untuk berkomunikasi dengan bagian dalam fungsi generator dari luar.
Jika kita benar-benar ingin memecah analogi push / pull / pipe ini sejauh yang kita bisa, kita berakhir dengan pseudocode berikut yang benar-benar mendorong kita pulang, selain dari langkah 1-5, yielddan sendmerupakan dua sisi dari pipa koin yang sama :
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
Transformasi kuncinya adalah bahwa kami telah membagi x = yield ydan value1 = g.send(value2)masing - masing menjadi dua pernyataan: left_end.push(y)dan x = left_end.pull(); dan value1 = right_end.pull()dan right_end.push(value2). Ada dua kasus khusus dari yieldkata kunci: x = yielddan yield y. Ini adalah gula sintaktis, masing-masing, untuk x = yield Nonedan _ = yield y # discarding value.
Untuk detail spesifik mengenai urutan tepat di mana nilai dikirim melalui pipa, lihat di bawah.
Berikut ini adalah model konkret yang agak panjang di atas. Pertama, harus dicatat bahwa untuk generator apa pun g, next(g)persis sama dengan g.send(None). Dengan pemikiran ini, kita hanya dapat fokus pada cara sendkerjanya dan hanya berbicara tentang memajukan generator send.
Misalkan kita punya
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
Sekarang, definisi fkasar kira - kira untuk fungsi biasa (non-generator) berikut:
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
Berikut ini telah terjadi dalam transformasi ini f:
- Kami telah memindahkan implementasi ke fungsi bersarang.
- Kami telah membuat pipa dua arah yang
left_endakan diakses oleh fungsi bersarang dan yang right_endakan dikembalikan dan diakses oleh lingkup luar - right_endadalah apa yang kita kenal sebagai objek generator.
- Dalam fungsi bersarang, hal pertama yang kita lakukan adalah cek yang
left_end.pull()adalah None, mengkonsumsi nilai mendorong dalam proses.
- Dalam fungsi bersarang, pernyataan
x = yield ytelah diganti oleh dua baris: left_end.push(y)dan x = left_end.pull().
- Kami telah mendefinisikan
sendfungsi untuk right_end, yang merupakan lawan dari dua baris yang kami ganti dengan x = yield ypernyataan pada langkah sebelumnya.
Dalam dunia fantasi ini di mana fungsi dapat berlanjut setelah kembali, gditugaskan right_enddan kemudian impl()dipanggil. Jadi dalam contoh kita di atas, jika kita mengikuti eksekusi baris demi baris, apa yang akan terjadi kira-kira sebagai berikut:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
Ini memetakan tepat ke pseudocode 16 langkah di atas.
Ada beberapa detail lainnya, seperti bagaimana kesalahan disebarkan dan apa yang terjadi ketika Anda mencapai ujung generator (pipa ditutup), tetapi ini harus memperjelas bagaimana aliran kontrol dasar bekerja saat senddigunakan.
Dengan menggunakan aturan penentuan yang sama ini, mari kita lihat dua kasus khusus:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
Sebagian besar mereka desugar dengan cara yang sama seperti f, satu-satunya perbedaan adalah bagaimana yieldpernyataan diubah:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
Dalam yang pertama, nilai yang diteruskan ke f1didorong (dihasilkan) pada awalnya, dan kemudian semua nilai yang ditarik (dikirim) didorong (dihasilkan) segera kembali. Dalam yang kedua, xtidak memiliki nilai (belum) ketika pertama kali datang push, jadi UnboundLocalErrordinaikkan.