Berkas log berukuran gigabyte tidak bisa dimuat utuh ke memori, padahal pola pengolahannya sederhana: ambil baris, buang yang kosong, saring yang relevan, ubah formatnya, lalu tampilkan. Solusinya adalah pipeline generator — setiap tahap adalah generator yang menarik satu nilai dari tahap sebelumnya, mengolahnya, dan menyerahkannya ke tahap berikutnya. Tidak ada tahap yang menyimpan seluruh data; evaluasi hanya terjadi saat nilai benar-benar dibutuhkan.
Pipeline Generator Berlapis #
Kita simulasikan data mentah berupa daftar baris transaksi — lengkap dengan baris kosong yang harus dibuang:
transaksi = [
"kopi,120000",
" ",
"teh,80000",
"kopi,95000",
"roti,45000",
]
def baris_bersih(sumber):
for baris in sumber:
baris = baris.strip()
if baris:
yield baris
def hanya_produk(nama, sumber):
for baris in sumber:
produk, nilai = baris.split(",")
if produk == nama:
yield nilai
def ke_rupiah(sumber):
for nilai in sumber:
yield f"Rp{int(nilai):,}"
pipeline = ke_rupiah(hanya_produk("kopi", baris_bersih(transaksi)))
for tampilan in pipeline:
print(tampilan)Output yang Dihasilkan:
Rp120,000
Rp95,000Mari kita bedah alurnya tahap demi tahap:
baris_bersih(transaksi): tahap pertama menghapus spasi di ujung tiap baris (strip()) dan membuang baris kosong — baris" "lenyap di sini.hanya_produk("kopi", ...): tahap kedua memecah baris dengansplit(",")— teknik dari Chapter split() dan join() — lalu meneruskan hanya nilai transaksi produkkopi.ke_rupiah(...): tahap ketiga mengubah string angka menjadi format rupiah.pipeline = ke_rupiah(hanya_produk(...)): perhatikan bahwa tidak ada satu nilai pun yang dihitung pada baris ini. Ketiga generator hanya berdiri berjejer menunggu ditarik.for tampilan in pipeline: loop-lah yang menarik gilirannya:ke_rupiahmeminta darihanya_produk, yang meminta daribaris_bersih, yang membaca satu baris daritransaksi. Satu nilai melintasi seluruh pipeline sebelum nilai berikutnya mulai ditarik.
Memori: List di Setiap Tahap versus Pipeline #
Kita ukur konsekuensi desain kedua pendekatan pada satu juta angka, disaring lalu dikalikan dua:
import sys
sumber = range(1_000_000)
tahap_saring = [n for n in sumber if n % 3 == 0]
tahap_kali = [n * 2 for n in tahap_saring]
print("list dua tahap :", sys.getsizeof(tahap_saring) + sys.getsizeof(tahap_kali), "byte")
gen_saring = (n for n in sumber if n % 3 == 0)
gen_kali = (n * 2 for n in gen_saring)
print("generator :", sys.getsizeof(gen_saring) + sys.getsizeof(gen_kali), "byte")
print("jumlah versi list :", sum(tahap_kali))
print("jumlah versi pipeline:", sum(gen_kali))Output yang Dihasilkan:
list dua tahap : 5853552 byte
generator : 400 byte
jumlah versi list : 333333666666
jumlah versi pipeline: 333333666666Angka 5853552 byte pada keluaran di atas adalah biaya dua wadah list — versi list membangun tahap saring utuh, lalu membaca seluruhnya lagi untuk membangun tahap kedua. Dua baris 333333666666 membuktikan hasil akhirnya identik; generator hanya mengganti cara mengalirkan nilai, bukan hasilnya. Ketika setiap tahap cukup satu kali jalan, pipeline adalah pilihan default yang hemat.
Short-Circuit pada Aliran Tak Terbatas #
Karena nilai dihitung hanya saat ditarik, pipeline bisa bekerja di atas sumber yang tidak pernah berakhir. Module itertools (kita bahas tuntas pada Chapter itertools) menyediakan generator tak terbatas seperti itertools.count(1):
import itertools
kuadrat = (n * n for n in itertools.count(1))
print("kuadrat pertama di atas 100:", next(q for q in kuadrat if q > 100))
print("nilai berikutnya masih ada :", next(kuadrat))
kosong = (n for n in [])
print("dari aliran kosong :", next(kosong, "tidak ada nilai"))Output yang Dihasilkan:
kuadrat pertama di atas 100: 121
nilai berikutnya masih ada : 144
dari aliran kosong : tidak ada nilaiMari kita bedah caranya:
itertools.count(1): menghasilkan1, 2, 3, ...tanpa akhir — tidak ada list yang membludak karena tidak ada nilai yang disimpan.next(q for q in kuadrat if q > 100): generator expression menarik1, 4, 9, ...satu per satu dan membuang yang tidak lolos; begitu121lolos,next()berhenti di sana. Inilah short-circuit: sisa aliran tak terbatas tidak pernah disentuh.next(kuadrat): generatorkuadratmasih hidup di posisi setelah121, sehingga144tersedia. Berhenti lebih awal tidak merusak sumbernya.next(kosong, "tidak ada nilai"): argument keduanext()adalah nilai default ketika aliran ternyata kosong — tanpa default,next()melemparStopIteration.
Dengan ini rangkaian Decorators dan Generators tuntas: decorator membungkus perilaku, generator mengalirkan nilai secara hemat, dan pipeline menyusunnya menjadi pengolahan data yang efisien dan siap dipakai pada aliran berukuran berapa pun.