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:

python
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:

text
Rp120,000
Rp95,000

Mari kita bedah alurnya tahap demi tahap:

  1. baris_bersih(transaksi): tahap pertama menghapus spasi di ujung tiap baris (strip()) dan membuang baris kosong — baris " " lenyap di sini.
  2. hanya_produk("kopi", ...): tahap kedua memecah baris dengan split(",") — teknik dari Chapter split() dan join() — lalu meneruskan hanya nilai transaksi produk kopi.
  3. ke_rupiah(...): tahap ketiga mengubah string angka menjadi format rupiah.
  4. pipeline = ke_rupiah(hanya_produk(...)): perhatikan bahwa tidak ada satu nilai pun yang dihitung pada baris ini. Ketiga generator hanya berdiri berjejer menunggu ditarik.
  5. for tampilan in pipeline: loop-lah yang menarik gilirannya: ke_rupiah meminta dari hanya_produk, yang meminta dari baris_bersih, yang membaca satu baris dari transaksi. 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:

python
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:

text
list dua tahap : 5853552 byte
generator       : 400 byte
jumlah versi list    : 333333666666
jumlah versi pipeline: 333333666666

Angka 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):

python
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:

text
kuadrat pertama di atas 100: 121
nilai berikutnya masih ada : 144
dari aliran kosong         : tidak ada nilai

Mari kita bedah caranya:

  1. itertools.count(1): menghasilkan 1, 2, 3, ... tanpa akhir — tidak ada list yang membludak karena tidak ada nilai yang disimpan.
  2. next(q for q in kuadrat if q > 100): generator expression menarik 1, 4, 9, ... satu per satu dan membuang yang tidak lolos; begitu 121 lolos, next() berhenti di sana. Inilah short-circuit: sisa aliran tak terbatas tidak pernah disentuh.
  3. next(kuadrat): generator kuadrat masih hidup di posisi setelah 121, sehingga 144 tersedia. Berhenti lebih awal tidak merusak sumbernya.
  4. next(kosong, "tidak ada nilai"): argument kedua next() adalah nilai default ketika aliran ternyata kosong — tanpa default, next() melempar StopIteration.

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.