Panduan Lengkap Iterator Pattern di Python
Pendahuluan
Pernahkah kamu bertanya-tanya bagaimana Python bisa membuat perulangan for bekerja secara konsisten — baik untuk list, string, file, maupun objek buatan sendiri? Jawabannya ada pada Iterator Pattern: sebuah pola desain yang memisahkan logika traversal dari struktur data itu sendiri.
Berbeda dengan pola behavioral lain seperti Strategy atau Observer yang berfokus pada komunikasi antar objek, Iterator Pattern berfokus pada cara mengakses elemen satu per satu tanpa perlu mengetahui detail internal dari koleksi tersebut. Di Python, pola ini bukan sekadar konsep abstrak — ia tertanam langsung ke dalam bahasa melalui protokol iterator bawaan.
Bayangkan kamu sedang membangun platform e-commerce seperti Tokopedia. Kamu perlu memproses jutaan transaksi harian dari database secara bertahap tanpa memuat semuanya ke memori sekaligus. Di sinilah Iterator Pattern menjadi solusi yang elegan dan efisien. Untuk memahami fondasi dari pola-pola seperti ini, kamu bisa mulai dari artikel Mengenal Design Patterns: Fondasi Arsitektur Plugin yang Scalable.
Memahami Protokol Iterator di Python: __iter__ dan __next__
Python mengimplementasikan Iterator Pattern melalui dua metode ajaib (dunder methods):
__iter__()— dipanggil saat objek dimasukkan ke dalam perulangan; harus mengembalikan objek iterator__next__()— dipanggil setiap iterasi untuk mengambil elemen berikutnya; harus memunculkanStopIterationsaat elemen habis
Objek yang mengimplementasikan kedua metode ini disebut iterator. Objek yang hanya mengimplementasikan __iter__() dan mengembalikan iterator terpisah disebut iterable.
class HitungMundur:
"""Iterable yang menghitung mundur dari n ke 1."""
def __init__(self, mulai: int):
self.mulai = mulai
def __iter__(self):
# Mengembalikan objek iterator terpisah
return HitungMundurIterator(self.mulai)
class HitungMundurIterator:
"""Iterator yang menyimpan state hitungan mundur."""
def __init__(self, angka: int):
self.angka = angka
def __iter__(self):
return self # Iterator mengembalikan dirinya sendiri
def __next__(self):
if self.angka <= 0:
raise StopIteration
nilai = self.angka
self.angka -= 1
return nilai
# Penggunaan
hitung = HitungMundur(5)
for angka in hitung:
print(angka)
# Output:
# 5
# 4
# 3
# 2
# 1
# Bisa diiterasi ulang karena HitungMundur adalah iterable (bukan iterator)
print(list(hitung))
# Output: [5, 4, 3, 2, 1]
Poin penting: Pisahkan iterable dari iterator. Iterable bisa diiterasi berkali-kali; iterator hanya bisa dipakai sekali karena menyimpan state posisi saat ini.
Konsep pemisahan ini mirip dengan bagaimana struktur data seperti Linked List memisahkan node dari logika traversalnya — struktur menyimpan data, sedangkan pointer menyimpan posisi saat ini.
Cara Pythonic Membuat Iterator dengan Generator dan yield
Menulis kelas iterator secara manual sering kali verbose. Python menyediakan cara yang jauh lebih ringkas: generator function menggunakan kata kunci yield.
def hitung_mundur(mulai: int):
"""Generator yang menghitung mundur dari mulai ke 1."""
while mulai > 0:
yield mulai
mulai -= 1
# Penggunaan persis seperti iterator biasa
for angka in hitung_mundur(5):
print(angka)
# Output:
# 5
# 4
# 3
# 2
# 1
# Generator object juga mendukung next()
gen = hitung_mundur(3)
print(next(gen)) # Output: 3
print(next(gen)) # Output: 2
print(next(gen)) # Output: 1
# next(gen) → StopIteration
Ketika Python menemukan yield, eksekusi fungsi dijeda dan nilai dikembalikan. Saat next() dipanggil berikutnya, eksekusi dilanjutkan dari titik yang sama. Ini adalah keunggulan generator: lazy evaluation — nilai baru dihitung hanya saat dibutuhkan.
Generator Expression
Untuk kasus sederhana, gunakan generator expression (mirip list comprehension tapi lebih hemat memori):
import sys
angka = range(1_000_000)
# List comprehension — membuat seluruh list di memori
list_biasa = [x * x for x in angka]
print(f"Ukuran list: {sys.getsizeof(list_biasa):,} bytes")
# Output: Ukuran list: 8,697,456 bytes
# Generator expression — hanya menyimpan state
gen_expr = (x * x for x in angka)
print(f"Ukuran generator: {sys.getsizeof(gen_expr):,} bytes")
# Output: Ukuran generator: 208 bytes
Perbedaan memori ini sangat signifikan saat bekerja dengan data besar.
Iterator Tingkat Lanjut dengan Modul itertools
Modul itertools dari standard library Python menyediakan iterator yang sudah dioptimasi di level C — lebih cepat dan hemat memori dibanding implementasi Python murni.
import itertools
# chain — menggabungkan beberapa iterable tanpa membuat list baru
kategori_a = ["python", "javascript"]
kategori_b = ["go", "rust"]
kategori_c = ["java", "kotlin"]
semua_bahasa = itertools.chain(kategori_a, kategori_b, kategori_c)
print(list(semua_bahasa))
# Output: ['python', 'javascript', 'go', 'rust', 'java', 'kotlin']
# islice — mengambil sebagian dari iterator (seperti slicing tapi lazy)
def bilangan_prima():
"""Generator bilangan prima tanpa batas."""
yield 2
kandidat = itertools.count(3, 2) # 3, 5, 7, 9, ...
primes = []
for n in kandidat:
if all(n % p != 0 for p in primes if p * p <= n):
primes.append(n)
yield n
# Ambil 10 bilangan prima pertama tanpa menghitung semuanya
sepuluh_prima = list(itertools.islice(bilangan_prima(), 10))
print(sepuluh_prima)
# Output: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
# groupby — mengelompokkan elemen berurutan berdasarkan kunci
transaksi = [
{"tanggal": "2026-04-01", "jumlah": 150_000},
{"tanggal": "2026-04-01", "jumlah": 75_000},
{"tanggal": "2026-04-02", "jumlah": 200_000},
{"tanggal": "2026-04-03", "jumlah": 50_000},
]
for tanggal, grup in itertools.groupby(transaksi, key=lambda x: x["tanggal"]):
total = sum(t["jumlah"] for t in grup)
print(f"{tanggal}: Rp {total:,}")
# Output:
# 2026-04-01: Rp 225,000
# 2026-04-02: Rp 200,000
# 2026-04-03: Rp 50,000
Beberapa fungsi itertools lain yang sering dipakai:
| Fungsi | Kegunaan |
|---|---|
itertools.count(n) | Counter tak terbatas mulai dari n |
itertools.cycle(iterable) | Mengulang iterable tanpa henti |
itertools.takewhile(pred, it) | Ambil elemen selama kondisi terpenuhi |
itertools.dropwhile(pred, it) | Lewati elemen selama kondisi terpenuhi |
itertools.zip_longest(*its) | Zip dengan padding jika panjang berbeda |
itertools.batched(it, n) | Bagi iterable ke dalam batch (Python 3.12+) |
Contoh Kasus Nyata: Memproses File Besar secara Efisien
Misalkan kamu membangun sistem analitik log seperti yang dibutuhkan platform ride-hailing sekelas Gojek. File log bisa mencapai gigabytes — tidak mungkin dimuat ke RAM sekaligus.
import itertools
import random
from pathlib import Path
from typing import Generator
def buat_log_dummy(path: str, jumlah_baris: int = 10_000) -> None:
"""Membuat file log dummy untuk pengujian."""
levels = ["INFO", "WARNING", "ERROR", "DEBUG"]
services = ["payment-service", "auth-service", "order-service", "notif-service"]
with open(path, "w") as f:
for i in range(jumlah_baris):
level = random.choice(levels)
service = random.choice(services)
f.write(
f"2026-04-10 10:{i % 60:02d}:00 | {level} | {service} | Pesan log ke-{i}\n"
)
def baca_log_chunk(
path: str | Path,
encoding: str = "utf-8"
) -> Generator[str, None, None]:
"""
Generator yang membaca file besar baris per baris.
Memori yang digunakan konstan, tidak bergantung ukuran file.
"""
with open(path, encoding=encoding) as f:
for baris in f:
yield baris.rstrip("\n")
def filter_error(baris_gen: Generator) -> Generator[str, None, None]:
"""Hanya loloskan baris yang mengandung kata 'ERROR'."""
return (baris for baris in baris_gen if "ERROR" in baris)
def parse_log(baris_gen: Generator) -> Generator[dict, None, None]:
"""Ubah baris teks menjadi dict terstruktur."""
for baris in baris_gen:
bagian = baris.split(" | ", maxsplit=3)
if len(bagian) == 4:
yield {
"timestamp": bagian[0],
"level": bagian[1],
"service": bagian[2],
"pesan": bagian[3],
}
def proses_log_pipeline(path: str) -> None:
"""
Pipeline pemrosesan log menggunakan generator chaining.
Setiap tahap hanya memproses satu baris dalam satu waktu.
"""
# Buat pipeline — belum ada yang dieksekusi di sini
baris_gen = baca_log_chunk(path)
error_gen = filter_error(baris_gen)
parsed_gen = parse_log(error_gen)
# Ambil 100 error pertama dalam batch
batch = list(itertools.islice(parsed_gen, 100))
# Hitung error per service
error_per_service: dict[str, int] = {}
for log in batch:
service = log["service"]
error_per_service[service] = error_per_service.get(service, 0) + 1
# Tampilkan hasil
print("=== Ringkasan Error (100 pertama) ===")
for service, count in sorted(
error_per_service.items(), key=lambda x: x[1], reverse=True
):
print(f" {service}: {count} error")
# Jalankan simulasi
log_path = "/tmp/aplikasi.log"
buat_log_dummy(log_path, jumlah_baris=50_000)
proses_log_pipeline(log_path)
# Contoh output (angka bervariasi karena random):
# === Ringkasan Error (100 pertama) ===
# payment-service: 28 error
# order-service: 26 error
# auth-service: 24 error
# notif-service: 22 error
Teknik generator chaining di atas memastikan setiap baris diproses satu per satu melalui seluruh pipeline — dari pembacaan, filter, hingga parsing — tanpa pernah memuat lebih dari beberapa baris ke memori secara bersamaan.
Pertanyaan yang Sering Diajukan
Apa perbedaan antara iterable dan iterator di Python?
Iterable adalah objek yang bisa diiterasi (misalnya list, tuple, string) — ia mengimplementasikan __iter__() yang mengembalikan iterator baru setiap kali dipanggil. Iterator adalah objek yang menyimpan state posisi saat ini dan mengimplementasikan __next__(). Iterator juga iterable (ia mengembalikan dirinya sendiri dari __iter__()), tetapi iterable tidak selalu iterator. Akibatnya, iterable bisa diulang berkali-kali, sedangkan iterator hanya sekali.
Bagaimana cara kerja yield di dalam generator function?
Saat Python menemukan yield, eksekusi fungsi dijeda dan nilai di-yield dikembalikan ke pemanggil. Frame fungsi (termasuk semua variabel lokal) disimpan di memori. Ketika next() dipanggil berikutnya, eksekusi dilanjutkan persis dari baris setelah yield terakhir. Ini berbeda dari return yang mengakhiri fungsi sepenuhnya.
Mengapa generator lebih hemat memori dibanding list?
List menyimpan semua elemennya di memori sekaligus. Generator hanya menghitung dan menyimpan satu nilai pada satu waktu — nilai berikutnya baru dihitung saat diminta. Untuk dataset dengan jutaan elemen, perbedaan ini bisa berarti selisih megabytes hingga gigabytes penggunaan RAM.
Kapan sebaiknya menulis kelas iterator manual vs menggunakan generator?
Gunakan generator function untuk kasus sederhana — kodenya jauh lebih ringkas dan mudah dibaca. Gunakan kelas iterator manual saat kamu butuh: (1) multiple state yang kompleks, (2) metode tambahan selain __next__, (3) kemampuan reset/restart iterator, atau (4) integrasi dengan protokol lain seperti __len__ atau __getitem__.
Apa kegunaan itertools dibanding membuat loop sendiri?
Fungsi-fungsi itertools diimplementasikan di level C sehingga lebih cepat dari loop Python biasa. Selain itu, mereka dirancang untuk lazy evaluation sehingga hemat memori, dan kodenya lebih deklaratif sehingga mudah dipahami. Untuk operasi chaining, filtering, atau grouping pada data besar, itertools hampir selalu lebih baik dari implementasi manual.
Kesimpulan
Iterator Pattern di Python bukan sekadar pola desain — ia adalah fondasi dari cara kerja bahasa itu sendiri. Dengan memahami protokol __iter__ dan __next__, kamu bisa membuat objek custom yang bekerja mulus dengan semua fitur Python seperti for, list(), sum(), dan sebagainya.
Generator dengan yield memberikan cara yang lebih ringkas dan idiomatis, sementara modul itertools menyediakan senjata canggih yang sudah dioptimasi untuk kebutuhan tingkat lanjut. Gabungkan ketiganya — custom iterator, generator, dan itertools — dan kamu bisa membangun pipeline pemrosesan data yang efisien bahkan untuk dataset berukuran gigabytes.
Konsep lazy evaluation yang dipelajari di sini juga sangat relevan saat kamu menjelajahi dunia Implementasi Queue: Mengelola Antrian Secara Efisien, di mana efisiensi memori dan urutan pemrosesan sama pentingnya.
Selamat belajar dan terus berlatih! Iterator Pattern adalah salah satu pola yang paling sering dipakai tanpa disadari — begitu kamu paham cara kerjanya, kode Python kamu akan naik level secara signifikan. Eksplorasi artikel-artikel lainnya di KamusNgoding untuk terus memperdalam pemahamanmu.