%%{init: {"flowchart": {"diagramPadding": 40}}}%%
flowchart LR
A["Berkas CSV<br/>300.000 baris"] --> B["Generator<br/>baca_rekaman()"]
B --> C["Hitung jumlah dan<br/>cacah per sektor"]
C --> D["Rata-rata<br/>per sektor"]
Bab 30 — Iterator dan Generator: Membaca Satu per Satu, Tanpa Menumpuk
Seri I4 — Bahasa Python Tingkat Lanjut
Semua skrip bab ini ada di folder kode/bab30/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Seri I1 sampai I3 membuat kode Anda rapi dan hasilnya bisa dipertanggungjawabkan. Di Seri I4, kita memperkuat bahasa Python itu sendiri: fitur-fitur yang membuat kode lebih hemat, lebih ringkas, dan lebih tahan banting. Kita mulai dari yang paling sering dipakai diam-diam: perulangan.
Bayangkan seorang mandor yang mencatat panen. Cara pertama: ia menunggu semua karung selesai diangkut, menumpuknya di gudang, baru mulai mencatat. Cara kedua: ia mencatat satu karung setiap kali ada yang datang, lalu karung itu dilepas. Cara pertama butuh gudang besar. Cara kedua hanya butuh meja kecil. Bab ini tentang cara kedua.

Apa yang dikerjakan for: protokol iterasi
Perulangan for bekerja pada apa pun yang bisa diiterasi (iterable): list, tuple, dict, string, berkas. Di baliknya ada dua langkah: Python meminta iterator (semacam penanda bacaan) lewat iter(), lalu terus meminta isi berikutnya lewat next() sampai muncul sinyal StopIteration.
01_protokol_iterasi.py
# Bab 30 - Apa yang sebenarnya dikerjakan perulangan for
kolam = ["A", "B", "C"]
pengulang = iter(kolam) # minta "penunjuk bacaan" dari list
print(type(pengulang).__name__)
print(next(pengulang))
print(next(pengulang))
print(next(pengulang))
try:
next(pengulang) # sudah habis
except StopIteration:
print("StopIteration: tidak ada isi lagi")
# for melakukan hal yang sama, dan menangkap StopIteration untuk Anda
for nama in kolam:
print("Kolam", nama)list_iterator
A
B
C
StopIteration: tidak ada isi lagi
Kolam A
Kolam B
Kolam C
iter(kolam)memberi objek bertipelist_iterator, penanda bacaan atas list itu.- Tiga panggilan
next()memberiA,B,C. Panggilan keempat melemparStopIteration, artinya “habis”. - Perulangan
formelakukan persis itu, dan menangkapStopIterationuntuk Anda, sehingga Anda tidak pernah melihatnya.
Iterable = benda yang bisa diminta iterator-nya (iter()). Iterator = benda yang bisa diminta isi berikutnya (next()). Iterator sekali jalan: setelah habis, ia tidak mundur.
Iterator buatan sendiri dengan kelas
Agar objek buatan Anda bisa dipakai di for, kelasnya cukup punya dua metode: __iter__ (mengembalikan iterator) dan __next__ (memberi isi berikutnya, atau melempar StopIteration).
02_kelas_iterator.py
# Bab 30 - Membuat iterator sendiri dengan kelas
class HitungMundur:
"""Menghitung mundur hari sampai panen."""
def __init__(self, hari):
self.hari = hari
def __iter__(self):
return self
def __next__(self):
if self.hari <= 0:
raise StopIteration
self.hari -= 1
return self.hari + 1
for sisa in HitungMundur(3):
print("Sisa", sisa, "hari")
hitung = HitungMundur(2)
print(list(hitung))
print(list(hitung)) # sudah habis: kosongSisa 3 hari
Sisa 2 hari
Sisa 1 hari
[2, 1]
[]
Perhatikan bagian akhirnya: list(hitung) pertama menghasilkan [2, 1], tetapi yang kedua []. Iterator-nya sudah habis dibaca. Ini sifat dasar iterator, dan akan muncul lagi sebagai jebakan di bawah.
Generator: cara pendek membuat iterator
Menulis kelas hanya untuk menghitung mundur itu repot. Python punya jalan pintas: fungsi biasa yang memakai yield menjadi fungsi generator. yield berarti “berikan nilai ini, lalu jeda di sini sampai diminta lagi”.
03_generator_yield.py
# Bab 30 - Fungsi generator dengan yield
def hitung_mundur(hari):
print(" (mulai)")
while hari > 0:
yield hari
print(" (lanjut setelah yield)")
hari -= 1
print(" (selesai)")
gen = hitung_mundur(2)
print("Generator dibuat, belum ada yang jalan")
print("nilai pertama:", next(gen))
print("nilai kedua:", next(gen))
try:
next(gen)
except StopIteration:
print("StopIteration")Generator dibuat, belum ada yang jalan
(mulai)
nilai pertama: 2
(lanjut setelah yield)
nilai kedua: 1
(lanjut setelah yield)
(selesai)
StopIteration
Telusuri urutannya, karena di sinilah letak sihirnya:
hitung_mundur(2)belum menjalankan apa-apa. Hanya terbentuk objek generator. Itu sebabnya “(mulai)” belum tercetak saat “Generator dibuat”.next(gen)pertama menjalankan fungsi dari awal sampaiyield 2, lalu berhenti. Tercetak “(mulai)” lalu nilainya, 2.next(gen)kedua melanjutkan tepat setelahyieldtadi: “(lanjut setelah yield)”,hariturun menjadi 1, laluyield 1.next(gen)ketiga melanjutkan,whileselesai, tercetak “(selesai)”, dan fungsi berakhir, sehingga munculStopIteration.
Generator adalah fungsi yang mengingat posisinya. Semua variabel lokalnya tetap hidup di antara dua yield.
Ekspresi generator dan bukti hemat memori
Seperti list comprehension (Bab 9), ada versi generatornya: ganti kurung siku dengan kurung biasa. Hasilnya bukan list, melainkan generator yang menghitung isi saat diminta.
04_ekspresi_generator.py
# Bab 30 - Ekspresi generator dan penghematan memori
import sys
import tracemalloc
kuadrat_list = [n * n for n in range(1_000_000)] # kurung siku: semua dibuat sekarang
kuadrat_gen = (n * n for n in range(1_000_000)) # kurung biasa: dibuat satu per satu
print("jumlah list :", sum(kuadrat_list))
print("jumlah generator:", sum(kuadrat_gen))
del kuadrat_list
# Ukuran objek generator tetap kecil, berapa pun banyaknya isi
kecil = (n for n in range(10))
besar = (n for n in range(10_000_000))
print("ukuran sama untuk 10 dan 10 juta isi:", sys.getsizeof(kecil) == sys.getsizeof(besar))
def puncak_memori(pembuat):
tracemalloc.start()
hasil = sum(pembuat())
_, puncak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return hasil, puncak
_, puncak_list = puncak_memori(lambda: [n for n in range(500_000)])
_, puncak_gen = puncak_memori(lambda: (n for n in range(500_000)))
print("list memakai memori puncak lebih dari 100x generator:", puncak_list > 100 * puncak_gen)jumlah list : 333332833333500000
jumlah generator: 333332833333500000
ukuran sama untuk 10 dan 10 juta isi: True
list memakai memori puncak lebih dari 100x generator: True
- Kedua cara menghasilkan jumlah yang sama,
333332833333500000: hasilnya tidak berubah, hanya cara kerjanya. - Objek generator berukuran sama (
sys.getsizeofsama) baik untuk 10 isi maupun 10 juta isi. Ia tidak menyimpan isinya, hanya “resep” cara membuatnya. tracemalloc(alat bawaan untuk mencatat pemakaian memori) menunjukkan bahwa memori puncak list lebih dari 100 kali memori puncak generator untuk 500.000 angka.
Perhatikan bahwa program ini tidak mencetak angka byte, hanya perbandingan (True). Ukuran persisnya berubah antar versi Python dan antar komputer. Perbandingan “jauh lebih besar” tetap benar di mana pun.
Kalau Anda hanya perlu melewati data sekali (menjumlahkan, mencari yang terbesar, menyaring), jangan buat list dulu. sum(x * x for x in data) lebih hemat dan tidak butuh kurung siku.
Generator lebih jauh: itertools, yield from, dan jebakan
05_generator_lanjut.py
# Bab 30 - itertools, yield from, dan jebakan generator habis
from itertools import islice
def bilangan_tak_hingga():
n = 1
while True: # tidak pernah berhenti, tetapi aman karena malas
yield n
n += 1
print(list(islice(bilangan_tak_hingga(), 5))) # ambil 5 saja
def semua_kolam():
yield from ["A1", "A2"]
yield from ["B1"]
print(list(semua_kolam()))
# Jebakan: generator hanya bisa dibaca SEKALI
panen = (kg for kg in [245.5, 310.0, 198.5])
print("pertama:", sum(panen))
print("kedua :", sum(panen)) # kosong, bukan error[1, 2, 3, 4, 5]
['A1', 'A2', 'B1']
pertama: 754.0
kedua : 0
- Generator
bilangan_tak_hingga()tidak pernah berhenti, tetapi aman: ia hanya bekerja bila diminta.itertools.islice(..., 5)mengambil lima isi pertama lalu berhenti meminta. yield frommeneruskan semua isi dari iterable lain, jadisemua_kolam()menggabungkan dua daftar.- Jebakan:
sum(panen)pertama memberi 754,0, tetapi yang kedua memberi0. Generator hanya bisa dibaca sekali. Tidak ada galat, hanya hasil kosong, jadi bug ini licin. Bila data perlu dibaca dua kali, buat generator baru atau simpan ke list.
Merangkai generator menjadi pipa
Kekuatan terbesar generator: beberapa generator bisa disambung seperti pipa air. Setiap tahap mengerjakan satu tugas kecil, dan data mengalir satu isi per satu isi.
06_pipa_generator.py
# Bab 30 - Merangkai generator menjadi pipa
catatan = ["kolam,A,540.5", "kolam,B,rusak", "kolam,C,498.0", "", "kolam,D,610.0"]
def tanpa_kosong(baris):
for b in baris:
if b.strip():
yield b
def pecah(baris):
for b in baris:
yield b.split(",")
def hanya_angka(rekaman):
for sektor, unit, nilai in rekaman:
try:
yield unit, float(nilai)
except ValueError:
print(" dilewati:", unit, nilai)
pipa = hanya_angka(pecah(tanpa_kosong(catatan)))
print("pipa dibuat, belum ada yang diproses")
for unit, bobot in pipa:
print(unit, bobot)pipa dibuat, belum ada yang diproses
A 540.5
dilewati: B rusak
C 498.0
D 610.0
Saat pipa dibuat, belum ada yang diproses (itu sebabnya “pipa dibuat, belum ada yang diproses” tampil lebih dulu). Pekerjaan baru terjadi di for. Baris kosong disaring oleh tanpa_kosong, baris kolam,B,rusak dilewati oleh hanya_angka (dengan pesan “dilewati”), dan sisanya mengalir sampai ke ujung. Tidak ada list perantara.
Proyek Bersambung I4: membaca berkas data besar
Proyek Seri I4 menguatkan kode lapangan Anda, bab demi bab. Langkah pertama: membaca berkas catatan lapangan yang besar. Skrip di bawah membuat berkas tiruan (angka contoh, fiktif) lima sektor (hutan, kebun, sawah, kolam, kandang) berisi 300.000 baris, besarnya lebih dari 5 MB, lalu membacanya dengan generator.
07_proyek_baca_besar.py
# Bab 30 - Proyek bersambung I4: membaca berkas besar dengan generator
import random
import tracemalloc
from pathlib import Path
folder = Path(__file__).parent / "_tmp"
folder.mkdir(exist_ok=True)
berkas = folder / "lapangan_besar.csv"
# 1) Buat berkas tiruan (angka contoh, fiktif)
rentang = {"hutan": (20, 60), "kebun": (150, 350), "sawah": (3000, 7000),
"kolam": (400, 700), "kandang": (300, 450)}
acak = random.Random(42)
with open(berkas, "w", encoding="utf-8") as f:
f.write("sektor,unit,hari,nilai\n")
for i in range(300_000):
sektor = list(rentang)[i % 5]
bawah, atas = rentang[sektor]
f.write(f"{sektor},{sektor[0].upper()}{i % 7 + 1},{i // 5 + 1},{acak.uniform(bawah, atas):.1f}\n")
print("ukuran berkas lebih dari 5 MB:", berkas.stat().st_size > 5_000_000)
# 2) Generator membaca baris demi baris
def baca_rekaman(jalur):
with open(jalur, encoding="utf-8") as f:
next(f) # lewati judul kolom
for baris in f:
sektor, unit, hari, nilai = baris.strip().split(",")
yield sektor, unit, int(hari), float(nilai)
def rata_rata_per_sektor(jalur):
jumlah, cacah = {}, {}
for sektor, _, _, nilai in baca_rekaman(jalur):
jumlah[sektor] = jumlah.get(sektor, 0) + nilai
cacah[sektor] = cacah.get(sektor, 0) + 1
return {s: jumlah[s] / cacah[s] for s in sorted(jumlah)}, sum(cacah.values())
tracemalloc.start()
rata, banyak = rata_rata_per_sektor(berkas)
_, puncak_gen = tracemalloc.get_traced_memory()
tracemalloc.stop()
print("rekaman dibaca:", banyak)
for sektor, nilai in rata.items():
print(f"{sektor:8} rata-rata {nilai:8.1f}")
# 3) Pembanding: baca semua ke list dulu
tracemalloc.start()
semua = list(baca_rekaman(berkas))
_, puncak_list = tracemalloc.get_traced_memory()
tracemalloc.stop()
print("jumlah rekaman di list:", len(semua))
print("cara list memakai memori puncak lebih dari 20x generator:", puncak_list > 20 * puncak_gen)ukuran berkas lebih dari 5 MB: True
rekaman dibaca: 300000
hutan rata-rata 40.0
kandang rata-rata 375.2
kebun rata-rata 250.1
kolam rata-rata 550.2
sawah rata-rata 4996.2
jumlah rekaman di list: 300000
cara list memakai memori puncak lebih dari 20x generator: True
Telusuri hasilnya:
- Berkas dibuat dengan benih acak tetap (
random.Random(42)), sehingga angkanya sama setiap dijalankan. Berkasnya ditaruh di_tmp/, bukan di folder skrip. - Tiap sektor mendapat 60.000 baris (300.000 dibagi 5). Rata-rata per sektor mengikuti rentang yang kita tetapkan: sawah sekitar 5.000, kolam sekitar 550, dan seterusnya.
- Generator
baca_rekamanmemakaiwith open(...)(Bab 13): berkas ditutup otomatis. Perulanganfor baris in fsendiri sudah membaca berkas baris demi baris, tanpa memuat semuanya. - Pembanding:
list(baca_rekaman(berkas))menyimpan semua 300.000 rekaman, dan memori puncaknya lebih dari 20 kali lipat. Karena hanya perbandingan yang dicetak, keluarannya stabil.
sys.getsizeof hanya mengukur objek itu sendiri, bukan isi yang dirujuknya. Untuk list, ukurannya ikut bertambah karena list memang menyimpan penunjuk ke setiap isi. Untuk generator, ukurannya tetap karena ia hanya menyimpan keadaan terakhirnya (posisi di fungsi dan variabel lokal). Karena itu bab ini memakai tracemalloc untuk membandingkan pemakaian memori sungguhan, dan hanya mencetak perbandingannya.
Latihan
- Tulis fungsi generator
genap_sampai(n)yang memberikan bilangan genap dari 2 sampain. Cetaklist(genap_sampai(10)). - Ubah ekspresi generator pada
04_ekspresi_generator.pysupaya menjumlahkan hanya kuadrat bilangan ganjil. Apakah jumlahnya berubah? Mengapa? - Tambahkan satu tahap pada pipa di
06_pipa_generator.py: generatordi_atas(rekaman, batas)yang hanya meloloskan bobot di atas 500. Sambungkan di ujung pipa. - Tantangan: ubah
07_proyek_baca_besar.pysupaya juga menghitung nilai terbesar tiap sektor dalam sekali baca, tanpa menyimpan seluruh rekaman.
Ringkasan Bab
formemakai protokol iterasi:iter()untuk meminta iterator,next()untuk isi berikutnya,StopIterationbila habis.- Generator dibuat dengan
yield; fungsinya berjeda dan melanjutkan dari posisi terakhir. - Ekspresi generator
(x for x in ...)menghitung saat diminta, ukurannya tetap kecil, dan menghemat memori. - Generator hanya bisa dibaca sekali; hasil kosong pada pembacaan kedua adalah jebakan klasik.
- Beberapa generator bisa dirangkai menjadi pipa tanpa list perantara.
- Proyek I4: berkas besar dibaca dengan generator, 300.000 rekaman dirangkum tanpa menumpuk di memori.
Selanjutnya, di Bab 31: decorator dan closure, cara “membungkus” fungsi dengan tambahan (log, coba-ulang) tanpa mengubah isinya.