Generator dan yield

Fungsi generator dengan yield untuk sekuens lazy dan stream tak terbatas.

Daftar isi

Sebuah fungsi yang memuat pernyataan yield disebut fungsi generator, dan pemanggilannya tidak menjalankan badan fungsi melainkan mengembalikan objek generator yang mengikuti protokol iterasi. Generator memproduksi nilai satu per satu, sesuai permintaan, dan menahan seluruh keadaan lokalnya di antara dua permintaan. Topik ini membahas bentuk dasarnya, mekanisme jeda dan lanjut, pemanfaatannya untuk stream tak terbatas, serta program utuh yang memakai generator sebagai sumber data.

Fungsi generator dasar#

Ciri sebuah fungsi generator adalah yield di dalam badannya, yang mengembalikan satu nilai kepada pemanggil lalu menahan eksekusi di baris itu sampai nilai berikutnya diminta. Fungsi biasa dengan return mengembalikan satu nilai lalu selesai, sedangkan fungsi generator dapat memberikan banyak nilai secara berurutan. Pemanggilannya menghasilkan objek generator, yaitu iterator yang dihitung nilainya saat dibutuhkan.

Peran yield dan return pada fungsi generator berbeda. yield menyerahkan satu nilai, menahan eksekusi, dan siap melanjutkan pada permintaan berikutnya, sedangkan return mengakhiri generator sehingga pemanggilan berikutnya menimbulkan StopIteration. Karena mengikuti protokol iterasi, objek generator dapat langsung dipakai dalam for dan diteruskan ke fungsi konsumsi seperti sum(), list(), atau zip() tanpa penyesuaian apa pun.

python Contoh: fungsi generator bilangan genap
def bilangan_genap(batas):
    n = 0
    while n < batas:
        yield n
        n += 2

hasil = bilangan_genap(8)
print(type(hasil).__name__)
print(list(hasil))
text Keluaran
generator
[0, 2, 4, 6]

Baris hasil = bilangan_genap(8) belum menghitung apa pun, dan buktinya adalah cetakan generator untuk tipe objeknya. Nilai baru terhitung hanya ketika iterasi berjalan, misalnya lewat list() atau for, dan setiap yield menyodorkan satu elemen lalu menunggu. Seluruh nilai dikonsumsi sekaligus pada contoh ini agar mudah diperiksa, tetapi jalannya tetap satu per satu di balik layar.

Keadaan yang tertahan di antara pemanggilan#

Generator menyimpan titik eksekusi beserta seluruh variabel lokalnya setiap kali yield tercapai, dan melanjutkannya persis dari titik itu pada permintaan berikutnya. Perilaku ini terlihat jelas ketika fungsi generator memuat proses yang mencetak sesuatu, karena proses di antara dua yield hanya berjalan pada saat generator diminta melanjutkan.

python Contoh: jeda dan lanjut antara dua yield
def kirim_kuadrat():
    for n in range(1, 4):
        print(f"  proses: menghitung kuadrat {n}")
        yield n * n

g = kirim_kuadrat()
print("dibuat:", type(g).__name__)
print("panggilan 1 ->", next(g))
print("panggilan 2 ->", next(g))
print("panggilan 3 ->", next(g))
text Keluaran
dibuat: generator
  proses: menghitung kuadrat 1
panggilan 1 -> 1
  proses: menghitung kuadrat 2
panggilan 2 -> 4
  proses: menghitung kuadrat 3
panggilan 3 -> 9

Baris pertama menunjukkan bahwa pembuatan objek tidak mengeksekusi badan fungsi, dan setiap pasangan berikutnya menampilkan proses yang berjalan hanya sampai yield tercapai. Ketika badan fungsi selesai, iterator menimbulkan StopIteration sebagaimana aturan protokol iterasi, dan generator dianggap habis.

Keadaan yang tertahan mencakup titik eksekusi, parameter, dan seluruh variabel lokal, sehingga tidak ada proses yang diulang dari awal. Setiap pemanggilan fungsi generator menghasilkan instans yang berdiri sendiri, sehingga dua pemanggilan bilangan_genap(8) menghasilkan dua aliran yang posisi baca dan variabelnya terpisah. Biaya memori per instans juga kecil, karena yang tersimpan hanya keadaannya, dan bukan seluruh nilai yang pernah dikirim.

python Contoh: generator yang habis
def satu_sampai_tiga():
    yield 1
    yield 2
    yield 3

g = satu_sampai_tiga()
print(sum(g))
try:
    next(g)
except StopIteration:
    print("StopIteration: generator habis")
text Keluaran
6
StopIteration: generator habis

Generator untuk stream tak terbatas#

Karena nilai dihitung sesuai permintaan, generator dapat menghasilkan stream tak terbatas, yaitu deretan nilai yang tidak pernah selesai, tanpa menghabiskan memori. Kuncinya adalah perulangan while True yang tidak pernah keluar, dan konsumsinya dibatasi lewat islice dari modul itertools atau pernyataan break. Pola ini cocok untuk nomor urut, pembacaan sensor, dan sumber data yang mengalir terus.

python Contoh: stream tak terbatas yang dibatasi islice
from itertools import islice

def nomor_genap():
    n = 0
    while True:
        yield n
        n += 2

print(list(islice(nomor_genap(), 5)))
text Keluaran
[0, 2, 4, 6, 8]

Perulangan while True di dalam generator tidak menggantung program selama tidak dikonsumsi tanpa batas, karena badan fungsinya hanya berjalan sejauh permintaan yang datang. Fungsi islice di atas meminta lima nilai, sehingga penghitungan berhenti di yield kelima dan tidak berlanjut. Pembatasan lain dilakukan dari sisi konsumen, misalnya break setelah syarat tertentu terpenuhi.

Program utuh: nomor antrean#

Program berikut memakai satu generator antrean untuk dua panggilan layanan yang berbeda. Fungsi layani_loket mengambil sejumlah nomor dari generator yang sama, sehingga nomor terus berlanjut di antara kedua panggilan karena keduanya memakai objek generator yang sama.

python tiket-antrean.py
from itertools import islice


def nomor_antrean():
    nomor = 1
    while True:
        yield nomor
        nomor += 1


def layani_loket(antrean, jumlah):
    for nomor in islice(antrean, jumlah):
        print(f"Loket memanggil nomor {nomor:03d}")


antrean = nomor_antrean()
layani_loket(antrean, 3)
layani_loket(antrean, 2)
text Keluaran
Loket memanggil nomor 001
Loket memanggil nomor 002
Loket memanggil nomor 003
Loket memanggil nomor 004
Loket memanggil nomor 005

Lima nomor keluar dari satu aliran yang berkesinambungan, bukan dari dua antrean yang terpisah. Penentu format :03d mengisi nomor dengan nol di depan hingga tiga digit, sehingga keluarannya tersaji sejajar dalam kolom. Perilaku ini menggarisbawahi bahwa satu objek generator memegang satu posisi baca, sehingga beberapa pemanggil yang mengoper generator yang sama bergantian memajukan posisi tersebut. Apabila setiap pemanggil membutuhkan urutannya sendiri dari awal, buatlah generator baru dengan memanggil nomor_antrean() kembali.

Kesalahan umum#

Kesalahan pertama adalah mencetak objek generator mentah untuk melihat isinya. Cetakan tersebut berupa <generator object ...> dengan alamat memori, sehingga tidak memuat satu nilai pun dan berubah-ubah antar eksekusi. Cara yang benar adalah mengiterasikannya atau mengonversinya menjadi list.

python Contoh: memeriksa isi generator dengan benar
def g_kecil():
    yield 1
    yield 2

g = g_kecil()
print(type(g).__name__)
print(len(list(g)))
text Keluaran
generator
2

Kesalahan kedua adalah memakai ulang generator yang sudah habis dan mengira terjadi kesalahan lain karena hasilnya kosong. sum() pertama di bawah memakan seluruh nilai, sedangkan sum() kedua berjalan atas generator yang sudah habis, sehingga menghasilkan nol.

python Contoh: generator habis dipakai ulang
def dua_angka():
    yield 7
    yield 8

g = dua_angka()
print(sum(g))
print(sum(g))
text Keluaran
15
0

Perbaikan untuk kedua kesalahan itu sama, yaitu membuat generator baru setiap kali data perlu diiterasi dari awal. Apabila datanya memang kecil dan perlu dibaca berkali-kali, simpan hasilnya sebagai list, dan biarkan generator menangani kasus yang mahal atau tak terbatas.

Ringkasan#

  • Fungsi yang memuat yield menghasilkan objek generator, dan badan fungsinya baru berjalan saat iterasi dimulai.
  • Generator menahan titik eksekusi dan variabel lokalnya di antara dua permintaan nilai, lalu berakhir dengan StopIteration.
  • Generator mendukung stream tak terbatas yang dikonsumsi secara terbatas melalui islice atau break.
  • Satu objek generator hanya dapat dipakai sekali, sehingga iterasi baru menuntut generator baru.

Referensi#