Generator function mudah dipakai, tetapi bug yang muncul darinya hampir selalu berakar pada kesalahpahaman tentang kapan badan fungsi berjalan dan kapan generator mati. Bab sebelumnya menunjukkan potret singkatnya; chapter ini membedah garis waktunya secara rinci: jeda di setiap yield, kehabisan nilai setelah aliran berakhir, penutupan paksa dengan close(), dan sekilas tentang send() yang bisa menyuntik nilai ke dalam generator.
Garis Waktu Eksekusi yang Malas #
Generator yang baru dibuat berada dalam keadaan tertahan: tidak satu baris pun dari badannya berjalan. Setiap next() membangunkan badan fungsi sampai yield berikutnya. Kita buktikan dengan cetakan yang tersebar di antara yield:
def alur_persiapan():
print("1. menyiapkan bahan")
yield "bahan siap"
print("2. memanaskan oven")
yield "oven panas"
print("3. menuang adonan")
yield "adonan masuk oven"
langkah = alur_persiapan()
print("--- generator baru dibuat, belum ada keluaran ---")
print("next #1:", next(langkah))
print("--- generator tertahan di yield pertama ---")
print("next #2:", next(langkah))
print("next #3:", next(langkah))Output yang Dihasilkan:
--- generator baru dibuat, belum ada keluaran ---
1. menyiapkan bahan
next #1: bahan siap
--- generator tertahan di yield pertama ---
2. memanaskan oven
next #2: oven panas
3. menuang adonan
next #3: adonan masuk ovenUrutan keluaran adalah bukti kemalasan (lazy) yang sebenarnya:
- Setelah
langkah = alur_persiapan(): belum ada satu baris pun dari badan generator yang berjalan, sehingga garis pemisah tercetak lebih dulu. next #1: cetakan"1. menyiapkan bahan"dan penyerahan nilai"bahan siap"terjadi bersamaan dalam satu tarikan.- Antara
next #1dannext #2: generator berhenti persis di barisyieldpertama — dunia luar boleh beraktivitas apa pun, generator tidak bergerak. next #2dannext #3: pola yang sama berulang; cetakan nomor selalu mendahului nilai yang diserahkan.
Generator Habis Sekali Jalan #
Generator yang sudah mencapai akhir alirannya tidak bisa diputar ulang — StopIteration mengunci keadaannya selamanya:
def ambil_tiga():
for huruf in "abc":
yield huruf
gen = ambil_tiga()
print("iterasi pertama:", list(gen))
print("iterasi kedua :", list(gen))Output yang Dihasilkan:
iterasi pertama: ['a', 'b', 'c']
iterasi kedua : []Iterasi kedua tidak melempar galat; interpreter Python hanya menemukan generator yang sudah selesai, sehingga list() menerima nol nilai. Perilaku senada sudah kita jumpai pada Chapter Generator Expressions. Jika data sumber perlu dikonsumsi dua kali, bangun generator baru dengan memanggil ambil_tiga() lagi, atau materialisasikan sekali dengan list().
Menutup Generator dengan close() #
Generator yang tertahan di yield bisa dimatikan lebih awal dengan metode close(). Untuk melihat efeknya, kita tempatkan blok finally di badan generator:
def proses_batch():
try:
yield "batch-1"
yield "batch-2"
yield "batch-3"
finally:
print("pembersihan: koneksi basis data ditutup")
pekerjaan = proses_batch()
print(next(pekerjaan))
pekerjaan.close()
print("generator ditutup")
try:
next(pekerjaan)
except StopIteration:
print("pemanggilan next setelah close: StopIteration")Output yang Dihasilkan:
batch-1
pembersihan: koneksi basis data ditutup
generator ditutup
pemanggilan next setelah close: StopIterationMekanismenya: close() melempar exception GeneratorExit tepat di titik yield tempat generator tertahan. Exception tersebut memicu blok finally sehingga kode pembersihan tetap berjalan, lalu generator berakhir — pemanggilan next() berikutnya hanya menemukan StopIteration.
Bagaimana jika generator ingin mendengar sinyal penutupan itu sendiri? Exception GeneratorExit bisa ditangkap seperti exception biasa:
def berhati_hati():
try:
yield "nilai"
except GeneratorExit:
print("GeneratorExit diterima di dalam generator")
raise
gen = berhati_hati()
print(next(gen))
gen.close()
print("generator sudah ditutup")Output yang Dihasilkan:
nilai
GeneratorExit diterima di dalam generator
generator sudah ditutupBlok except GeneratorExit menerima sinyal, mencetak pesan, lalu melempar ulang dengan raise — melempar ulang adalah praktik yang benar, karena menahan GeneratorExit dengan mencoba yield lagi justru memicu RuntimeError dari interpreter Python.
Menyuntik Nilai dengan send() #
send() adalah kakak dari next(): selain membangunkan generator, send() juga mengirim satu nilai yang diterima sebagai hasil ekspresi yield di dalam badan generator:
def penjumlah():
total = 0
while True:
tambahan = yield total
if tambahan is None:
break
total += tambahan
return total
mesin = penjumlah()
print("nilai awal :", next(mesin))
print("tambah 10 :", mesin.send(10))
print("tambah 5 :", mesin.send(5))
try:
mesin.send(None)
except StopIteration as galat:
print("total akhir:", galat.value)Output yang Dihasilkan:
nilai awal : 0
tambah 10 : 10
tambah 5 : 15
total akhir: 15Ekspresi yield total berperan dua arah: menyerahkan total ke dunia luar, sekaligus menunggu kiriman balik dari send(). Nilai yang dikirim menggantikan hasil yield total di sisi kiri (tambahan), dan return total membuat nilai 15 tertangkap lewat galat.value — mekanisme yang akan kita manfaatkan penuh pada Chapter yield from.
Pada chapter berikutnya kita beranjak ke komposisi: bagaimana satu generator mendelegasikan sebagian alirannya ke generator lain dengan yield from.