Ketika kamu menulis perulangan for di Python, sebenarnya ada mekanisme canggih yang bekerja di balik layar: protokol iterator. Memahami konsep iterable, iterator, dan generator bukan sekadar teori — ini kunci untuk menulis kode yang hemat memori dan mampu menangani data dalam jumlah besar. Di tutorial ini, kita akan membahas semuanya dari nol, lengkap dengan contoh kode yang bisa langsung kamu jalankan.

Apa Itu Iterable dan Iterator?

Secara sederhana, iterable adalah objek apa pun yang bisa di-loop (diiterasi) menggunakan for, seperti list, tuple, string, dict, dan set. Di balik layar, Python memanggil fungsi bawaan iter() untuk mengubah iterable menjadi iterator, lalu memanggil next() berulang kali sampai muncul pengecualian StopIteration. Mari kita lihat prosesnya secara manual:

# Sebuah list adalah iterable, bukan iterator
angka = [10, 20, 30]

# iter() mengubah iterable menjadi iterator
it = iter(angka)

# next() mengambil elemen satu per satu
print(next(it))  # 10
print(next(it))  # 20
print(next(it))  # 30

# Elemen habis -> StopIteration
try:
    print(next(it))
except StopIteration:
    print("Iterator sudah habis!")

Perhatikan bahwa iterator bersifat stateful: ia "mengingat" posisi terakhirnya. Karena itu, kamu bisa memanggil iter() pada iterable berkali-kali dan selalu mendapat iterator baru, tetapi sebuah iterator hanya bisa dilalui satu kali.

Membuat Iterator Kustom dengan __iter__ dan __next__

Kamu bisa membuat iterator sendiri dengan mengimplementasikan dua metode dunder: __iter__() (mengembalikan objek iterator, biasanya self) dan __next__() (mengembalikan elemen berikutnya atau melempar StopIteration). Berikut contoh kelas Countdown yang menghitung mundur:

python
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        nilai = self.current
        self.current -= 1
        return nilai

for angka in Countdown(3):
    print(angka)
# Output:
# 3
# 2
# 1
# 0

Kenapa harus ada StopIteration? Karena itulah sinyal bagi perulangan for untuk berhenti. Tanpa pengecualian ini, perulangan tidak akan pernah tahu kapan data habis.

Mengenal Generator: Cara Paling Mudah Membuat Iterator

Menulis kelas iterator seperti di atas cukup panjang. Untungnya, Python menyediakan cara yang jauh lebih ringkas: generator. Generator adalah fungsi yang menggunakan kata kunci yield sebagai pengganti return. Setiap kali kamu memanggil fungsi generator, ia tidak langsung dieksekusi, melainkan mengembalikan objek generator (yang merupakan iterator).

python
def countdown(start):
    while start >= 0:
        yield start
        start -= 1

# Fungsi generator mengembalikan objek generator
gen = countdown(3)
print(gen)  # <generator object countdown at 0x...>

for angka in gen:
    print(angka)
# Output:
# 3
# 2
# 1
# 0

Keajaiban yield adalah ia menjeda fungsi dan menyimpan seluruh state lokalnya (nilai variabel, posisi eksekusi). Ketika next() dipanggil lagi, eksekusi dilanjutkan tepat setelah titik yield terakhir. Konsep inilah yang disebut lazy evaluation — nilai baru dihitung saat dibutuhkan.

Generator Expression: List Comprehension yang Malas

Sama seperti list comprehension, kamu bisa membuat generator secara ekspresif menggunakan generator expression. Bedanya hanya pada penggunaan tanda kurung () alih-alih kurung siku []. Hasilnya adalah objek generator yang malas, bukan list yang langsung dibuat.

python
import sys

# List comprehension: langsung menghitung semua nilai
kuadrat_list = [x ** 2 for x in range(1_000_000)]

# Generator expression: nilai dihitung satu per satu
kuadrat_gen = (x ** 2 for x in range(1_000_000))

print(sys.getsizeof(kuadrat_list))  # jutaan byte
print(sys.getsizeof(kuadrat_gen))   # hanya ~ 200 byte!

Tips: Gunakan generator expression ketika kamu hanya perlu mengiterasi hasil sekali dan tidak membutuhkan akses acak (indexing) atau pemakaian berulang. Untuk data besar, selisih memorinya bisa sangat signifikan.

Kapan Generator Sangat Berguna?

Kasus penggunaan generator paling nyata adalah saat memproses data yang tidak muat di memori, misalnya file berukuran besar atau stream data. Alih-alih membaca seluruh file ke list, kamu bisa membacanya baris per baris dengan generator. Berikut contoh menghitung jumlah baris yang mengandung ERROR tanpa memuat semuanya sekaligus:

python
def baca_baris(nama_file):
    with open(nama_file, "r", encoding="utf-8") as f:
        for baris in f:
            yield baris.strip()

total = 0
for baris in baca_baris("data_besar.log"):
    if "ERROR" in baris:
        total += 1

print(f"Ada {total} baris yang mengandung ERROR")

yield from: Delegasi ke Sub-Generator

Ketika satu generator perlu mendelegasikan sebagian kerjanya ke generator lain, gunakan yield from. Ini lebih ringkas daripada menulis loop bersarang dan otomatis meneruskan next(), send(), serta pengecualian ke sub-generator.

python
def gen_angka():
    yield 1
    yield 2

def gen_huruf():
    yield "a"
    yield "b"

def gen_gabungan():
    yield from gen_angka()
    yield from gen_huruf()
    yield "selesai"

print(list(gen_gabungan()))
# Output: [1, 2, 'a', 'b', 'selesai']

Generator sebagai Coroutine: send(), throw(), dan close()

Selain menghasilkan nilai, generator bisa menerima nilai melalui metode send(). Fitur ini mengubah generator menjadi coroutine sederhana yang bisa berkomunikasi dua arah dengan pemanggilnya. Nilai yang dikirim akan menjadi hasil dari ekspresi yield.

python
def akumulator():
    total = 0
    while True:
        nilai = yield total       # kirim total, terima nilai
        if nilai is None:
            break
        total += nilai

acc = akumulator()
next(acc)                        # majukan ke yield pertama

print(acc.send(10))              # total = 10
print(acc.send(5))               # total = 15
print(acc.send(20))              # total = 35
acc.close()                      # tutup generator dengan bersih

Metode throw() dipakai untuk menyuntikkan pengecualian ke dalam generator, sedangkan close() memicu GeneratorExit agar generator bisa membersihkan resource (misalnya menutup koneksi database). Meski coroutine murni kini lebih sering digantikan oleh asyncio, memahami mekanisme ini membantumu membaca kode library yang lebih tua.

Async Generator: Iterator untuk Kode Asynchronous

Jika kamu sudah bekerja dengan asyncio (kita bahas lengkap di artikel Menguasai Async/Await di Python), kamu bisa menggabungkan kekuatan generator dengan asynchronous programming lewat async generator menggunakan async def dan yield:

python
import asyncio

async def ambil_data():
    for i in range(3):
        await asyncio.sleep(1)   # simulasi I/O lambat
        yield f"data-{i}"

async def main():
    async for item in ambil_data():
        print(item)

asyncio.run(main())

Best Practice dan Kesalahan Umum

Berikut beberapa hal penting yang sering membuat pemula terjebak saat memakai generator:

Generator hanya bisa diiterasi satu kali. Setelah habis, memanggil list(gen) lagi akan menghasilkan list kosong. Jika kamu perlu mengulang hasilnya, ubah menjadi list terlebih dahulu atau buat ulang generatornya.

Beberapa poin lain yang perlu diingat: pertama, kode di dalam generator tidak dieksekusi saat fungsi dipanggil, melainkan saat next() pertama dijalankan. Kedua, karena generator itu malas, kesalahan (exception) baru muncul saat iterasi, bukan saat definisi. Ketiga, jangan gunakan generator untuk data kecil yang perlu diakses berulang secara acak — list biasa justru lebih tepat.

Kesimpulan

Iterator dan generator adalah fondasi yang membuat Python terasa begitu ekspresif. Dengan yield, kamu bisa menulis kode yang hemat memori, lebih bersih, dan siap menangani data besar. Konsep ini juga menjadi pintu masuk menuju topik lanjutan seperti decorator (yang sering dipakai bersama generator) dan pemrograman asynchronous. Jika kamu baru mulai belajar Python, pastikan dulu fondasinya kuat lewat Cara Install Python dan Menulis Program Pertamamu. Selamat bereksperimen!