Memahami Generator dan Iterator di Python: Lazy Evaluation untuk Pemrosesan Data Stream yang Efisien
Membedakan Iterable, Iterator, dan Protocol Iterasi di Python
Sebelum membahas generator, kita perlu memahami dua konsep dasar yang sering tertukar: iterable dan iterator. Iterable adalah objek yang bisa diiterasi, seperti list, tuple, dict, dan str. Objek ini menyimpan seluruh datanya di memori dan bisa diakses berulang kali. Iterator adalah objek yang melacak posisi iterasi dan menghasilkan nilai satu per satu sesuai permintaan.
Protocol iterasi di Python dibangun di atas dua fungsi bawaan. Fungsi iter() mengambil sebuah iterable dan mengembalikan iterator-nya. Fungsi next() mengambil iterator dan mengembalikan nilai berikutnya. Ketika iterator sudah tidak memiliki nilai tersisa, pemanggilan next() akan melempar StopIteration. Perilaku inilah yang dimanfaatkan for loop secara internal: loop memanggil iter() sekali, lalu memanggil next() berulang kali sampai StopIteration muncul.
Perbedaan kunci antara keduanya terletak pada cara data disimpan. Iterable menyimpan seluruh data di memori sejak awal. Iterator menghasilkan nilai secara bertahap dan hanya menyimpan state posisi saat ini. Konsekuensinya, sebuah iterable bisa diiterasi berkali-kali, sedangkan iterator hanya bisa diiterasi sekali. Setelah iterator habis, kita perlu memanggil iter() lagi untuk mendapatkan iterator baru dari iterable yang sama.
Pemahaman tentang protocol ini penting karena hampir semua fitur iterasi di Python, termasuk for loop, list comprehension, dan fungsi seperti map() atau filter(), bekerja di atas mekanisme yang sama. Mari kita lihat protocol iterasi secara manual.
angka = [10, 20, 30]
iterator = iter(angka)
for _ in range(4):
try:
print(next(iterator))
except StopIteration:
print("StopIteration: iterator sudah habis")Output:
10
20
30
StopIteration: iterator sudah habisPada contoh ini, iter(angka) mengubah list menjadi iterator. Empat kali pemanggilan next() menghasilkan tiga nilai pertama, lalu melempar StopIteration pada pemanggilan keempat. Blok try dan except menangkap exception tersebut sehingga program tidak berhenti. Inilah mekanisme yang sama dengan yang dijalankan for loop di balik layar.
Perhatikan bahwa kita tidak pernah menyentuh list angka secara langsung setelah iter() dipanggil. Seluruh proses iterasi berjalan melalui iterator. Pola ini menjadi fondasi dari konsep yang akan kita bahas selanjutnya: bagaimana Python menghasilkan nilai secara bertahap tanpa harus menyimpan semuanya di memori.
Generator Function dengan yield sebagai Inti Lazy Evaluation
Generator function adalah fungsi yang menggunakan yield alih-alih return. Perbedaan perilakunya fundamental. return menghentikan fungsi sepenuhnya dan mengembalikan satu nilai. yield menangguhkan eksekusi, menyimpan seluruh state fungsi, dan mengembalikan satu nilai ke pemanggil. Ketika next() dipanggil lagi, eksekusi dilanjutkan tepat dari titik yield terakhir.

Gambar: Alur kontrol coroutine — eksekusi ditangguhkan di titik `yield` dan dilanjutkan dari titik yang sama saat `next()` dipanggil — Sumber: Wikimedia Commons
Workflow eksekusi inilah yang membuat generator hemat memori. Nilai diproduksi on-demand, bukan disimpan sekaligus. Untuk data berukuran besar, perbedaannya sangat terlihat. Mari kita bandingkan generator dengan list yang dibangun penuh.
import sys
def deret_besar(n):
for i in range(n):
yield i
gen = deret_besar(1_000_000)
list_penuh = list(range(1_000_000))
print(f"Ukuran generator: {sys.getsizeof(gen)} bytes")
print(f"Ukuran list: {sys.getsizeof(list_penuh)} bytes")Output:
Ukuran generator: 104 bytes
Ukuran list: 8000056 bytesGenerator deret_besar menghasilkan satu juta nilai, tetapi ukurannya tetap 104 bytes. List yang menyimpan nilai yang sama membutuhkan sekitar 8 MB. Perbedaan ini terjadi karena generator tidak pernah menyimpan nilai yang sudah dihasilkan. Setiap nilai diproduksi, dikonsumsi, lalu dibuang. Untuk aplikasi yang memproses data stream, pendekatan ini memungkinkan kita menangani data yang jauh lebih besar dari kapasitas memori.
State yang disimpan generator mencakup nilai variabel lokal, posisi eksekusi, dan kondisi loop. Semua informasi ini dipertahankan di antara pemanggilan next(). Kemampuan ini membuka pola yang tidak mungkin dilakukan dengan list biasa, seperti menghasilkan deret tak hingga atau membaca file yang terus bertambah. Generator function juga bisa menerima parameter, sehingga satu fungsi bisa menghasilkan pola data yang berbeda-beda tergantung argumen yang diberikan.
Generator Expression dan Perbandingan Memori dengan List Comprehension
Generator expression adalah sintaks ringkas untuk membuat generator, mirip dengan list comprehension tetapi menggunakan kurung biasa (...) alih-alih kurung siku [...]. Perbedaan evaluasinya signifikan. List comprehension membangun seluruh list di memori sebelum hasilnya bisa digunakan. Generator expression menghasilkan nilai secara lazy, satu per satu, saat dikonsumsi.

Gambar: Evaluasi lazy bertahap — nilai dihitung satu per satu saat dibutuhkan, dari thunk yang belum dievaluasi hingga bentuk normal — Sumber: Wikimedia Commons
Generator expression bisa langsung dikonsumsi oleh fungsi bawaan seperti sum(), max(), atau any(). Fungsi-fungsi ini menerima iterable dan memprosesnya tanpa perlu list lengkap. Perbedaan memori menjadi signifikan pada dataset besar, log file, atau hasil query yang panjang. Mari kita bandingkan keduanya.
import sys
n = 1_000_000
list_kuadrat = [x * x for x in range(n)]
gen_kuadrat = (x * x for x in range(n))
print(f"List comprehension: {sys.getsizeof(list_kuadrat)} bytes")
print(f"Generator expression: {sys.getsizeof(gen_kuadrat)} bytes")
print(f"Total list: {sum(list_kuadrat)}")
print(f"Total generator: {sum(gen_kuadrat)}")Output:
List comprehension: 8448728 bytes
Generator expression: 104 bytes
Total list: 333332833333500000
Total generator: 333332833333500000Kedua pendekatan menghasilkan total yang sama, tetapi footprint memori berbeda drastis. List comprehension menyimpan satu juta angka kuadrat sekaligus. Generator expression hanya menyimpan state iterasi. Ada batasan yang perlu diingat: generator expression tidak bisa di-index dan hanya bisa diiterasi sekali. Setelah habis dikonsumsi, kita perlu membuat generator baru untuk mengulanginya.
Dalam praktik, perbedaan ini paling terasa ketika data datang dari sumber eksternal. Log file yang berisi jutaan baris, hasil query database yang panjang, atau respons API yang di-paginate semuanya bisa diproses dengan generator expression tanpa khawatir memori penuh. Sebaliknya, untuk data kecil yang akan diakses berulang kali, list comprehension tetap pilihan yang wajar karena overhead generator justru tidak memberikan keuntungan.
Menyusun Pipeline Pemrosesan Data Stream dengan Generator Chaining
Generator chaining adalah pola menghubungkan beberapa generator sehingga output satu menjadi input berikutnya. Setiap tahap memproses satu elemen pada satu waktu tanpa materialisasi data di tengah. Pola ini sangat cocok untuk data stream: file besar, API pagination, atau real-time feed.

Gambar: Alur data melalui tahap-tahap pipeline — analog dengan generator chaining di mana output satu tahap menjadi input tahap berikutnya — Sumber: Wikimedia Commons
Mari kita bangun pipeline tiga tahap untuk memproses file log. Tahap pertama membaca baris file satu per satu. Tahap kedua memfilter baris yang mengandung ERROR. Tahap ketiga mengekstrak jam dari timestamp dan mengagregasi jumlah error per jam.
from collections import Counter
def baca_baris(path):
with open(path) as f:
for baris in f:
yield baris.rstrip()
def filter_error(baris):
return "ERROR" in baris
def ekstrak_jam(baris):
return baris.split()[1][:2]
def hitung_error_per_jam(path):
baris = baca_baris(path)
baris_error = (b for b in baris if filter_error(b))
jam = (ekstrak_jam(b) for b in baris_error)
return Counter(jam)
print(hitung_error_per_jam("app.log"))Output:
Counter({'14': 31, '16': 21, '15': 16})Fungsi baca_baris adalah generator function yang membaca file baris demi baris. Dua generator expression berikutnya memfilter dan mengekstrak data tanpa membangun list perantara. Counter mengagregasi hasil akhir. Seluruh pipeline memproses file sebesar apa pun dengan memori konstan, karena hanya satu baris yang berada di memori pada satu waktu.
Pola komposisi ini bisa diperluas tanpa batas. Kita bisa menambahkan tahap normalisasi data, deduplikasi, atau transformasi lain di tengah pipeline tanpa mengubah struktur keseluruhan. Setiap tahap tetap berupa generator yang berdiri sendiri, sehingga mudah diuji dan digunakan kembali. Inilah kekuatan utama generator chaining: kompleksitas pemrosesan bertambah, tetapi footprint memori tidak ikut bertambah.
Panduan Memilih Generator vs List dan Best Practices
Kapan kita memilih generator dan kapan memilih list? Aturan keputusannya sederhana. Gunakan generator untuk data besar atau one-pass processing. Gunakan list untuk akses acak berulang dan data kecil yang sering diakses.
Beberapa best practice perlu diperhatikan. Konsumsi generator sekali saja, jangan menyimpan hasilnya tanpa alasan. Jika generator expression terlalu panjang sehingga sulit dibaca, pecah menjadi generator function bernama. Kombinasi dengan itertools memperkaya pipeline lazy: islice untuk memotong stream, chain untuk menggabungkan beberapa iterable, dan groupby untuk mengelompokkan data berurutan.
Sebagai contoh, itertools.islice memungkinkan kita mengambil seribu baris pertama dari file log tanpa membaca sisanya. itertools.chain menggabungkan beberapa file log menjadi satu stream yang seragam. Alat-alat ini melengkapi generator sehingga pipeline lazy bisa menangani skenario yang lebih kompleks dengan kode yang tetap ringkas.
Lazy evaluation memberikan dampak terbesar ketika data tidak muat di memori, ketika setiap elemen hanya diproses sekali, dan ketika kita ingin memulai pemrosesan sebelum seluruh data tersedia. Memahami kapan menerapkan konsep ini akan membuat kode Python kita lebih efisien dan scalable.
Ingin menguasai Python secara mendalam? Kunjungi kursus dan bootcamp di Rumah Coding untuk belajar langsung dari praktisi berpengalaman, termasuk struktur data, optimasi performa, dan pola pemrograman Python yang siap digunakan di industri.