1. Pengenalan Generators
Generator adalah fungsi khusus di Python yang bisa menghasilkan rangkaian nilai secara bertahap, alih-alih menghitung dan menyimpan semua nilai sekaligus di memori. Generator menggunakan keyword yield untuk mengembalikan nilai satu per satu sambil mempertahankan state fungsi.
Konsep generator didasarkan pada lazy evaluation — nilai hanya dihitung ketika dibutuhkan. Ini membuat generator sangat efisien untuk bekerja dengan data berukuran besar atau bahkan data tak terbatas (infinite sequences).
Generator vs List
| Aspek | List | Generator |
|---|---|---|
| Memori | Semua elemen di RAM | Hanya elemen saat ini |
| Evaluasi | Eager (langsung hitung) | Lazy (hitung saat dibutuhkan) |
| Akses | Random access [index] | Sequential only |
| Ulang | Bisa diulang berkali-kali | Sekali pakai habis |
| len() | ✅ Bisa | ❌ Tidak bisa |
| Kegunaan | Data kecil-sedang | Data besar/infinite |
┌───────────────────────────────────────────────────────┐ │ LIST vs GENERATOR │ │ │ │ LIST (Eager): │ │ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ │ │ 1 │ 4 │ 9 │16 │25 │36 │49 │64 │81 │100│ │ │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │ │ Semua 10 elemen langsung ada di memori │ │ │ │ GENERATOR (Lazy): │ │ ┌───┐ │ │ │ 1 │ ──► next() ──► ┌───┐ │ │ └───┘ │ 4 │ ──► next() ──► ┌───┐ │ │ └───┘ │ 9 │ ──► │ │ └───┘ │ │ Elemen dihasilkan satu per satu saat diminta │ └───────────────────────────────────────────────────────┘
2. Keyword Yield
Keyword yield adalah jantung dari generator. Ketika Python menemukan yield dalam sebuah fungsi, fungsi tersebut secara otomatis menjadi generator function. yield mengembalikan nilai dan mengekskusi fungsi, tetapi mempertahankan state lokalnya sehingga bisa dilanjutkan dari tempat terakhir.
# Generator function pertama
def hitung_mundur(n):
print("Mulai hitung mundur!")
while n > 0:
yield n # Keluarkan nilai n, pause eksekusi
n -= 1
print("Selesai!")
# Membuat generator object
gen = hitung_mundur(5)
print(type(gen)) # <class 'generator'>
# Mengambil nilai satu per satu dengan next()
print(next(gen)) # Mulai hitung mundur! → 5
print(next(gen)) # 4
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1
# print(next(gen)) # Selesai! → StopIteration exception
# Menggunakan for loop (otomatis menangani StopIteration)
print("\n--- Ulang dengan for loop ---")
for angka in hitung_mundur(3):
print(angka)
# Mulai hitung mundur!
# 3
# 2
# 1
# Selesai!
Yield vs Return
# RETURN: mengembalikan nilai dan mengakhiri fungsi
def fungsi_return():
yield "ini bukan return" # ← fungsi ini generator!
return "ini return" # ← StopIteration dengan value
# YIELD: mengembalikan nilai DAN mempertahankan state
def fungsi_yield():
print("Langkah 1")
yield "nilai pertama"
print("Langkah 2") # Lanjut dari sini saat next() dipanggil lagi
yield "nilai kedua"
print("Langkah 3")
yield "nilai ketiga"
gen = fungsi_yield()
print(next(gen)) # Langkah 1 → "nilai pertama"
print(next(gen)) # Langkah 2 → "nilai kedua"
print(next(gen)) # Langkah 3 → "nilai ketiga"
# yield bisa mengembalikan beberapa nilai
def pembagi(angka, pembagi_list):
for p in pembagi_list:
if p != 0:
yield angka / p, p # Yield tuple
for hasil, pembagi in pembagi(100, [2, 4, 5, 0, 10]):
print(f"100 / {pembagi} = {hasil}")
# 100 / 2 = 50.0
# 100 / 4 = 25.0
# 100 / 5 = 20.0
# 100 / 10 = 10.0
Perbedaan kunci: return mengakhiri fungsi dan mengembalikan satu nilai. yield mengekskusi fungsi sementara dan mengembalikan satu nilai — fungsi bisa dilanjutkan lagi dari tempat yield terakhir saat next() dipanggil.
3. Generator Functions
Generator untuk Menghasilkan Bilangan
# Generator Fibonacci
def fibonacci():
a, b = 0, 1
while True: # Infinite generator!
yield a
a, b = b, a + b
# Ambil 10 bilangan Fibonacci pertama
gen = fibonacci()
for _ in range(10):
print(next(gen), end=" ")
# 0 1 1 2 3 5 8 13 21 34
# Generator bilangan prima
def bilangan_prima():
"""Generator bilangan prima tak terbatas"""
yield 2
kandidat = 3
prima_sebelumnya = [2]
while True:
is_prima = True
for p in prima_sebelumnya:
if p * p > kandidat:
break
if kandidat % p == 0:
is_prima = False
break
if is_prima:
prima_sebelumnya.append(kandidat)
yield kandidat
kandidat += 2 # Hanya cek ganjil
# Ambil 15 bilangan prima pertama
prima_gen = bilangan_prima()
prima_list = [next(prima_gen) for _ in range(15)]
print(prima_list)
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
# Generator untuk membaca file baris per baris
def baca_file_besar(filepath):
"""Membaca file besar tanpa memuat semua ke memori"""
try:
with open(filepath, 'r') as f:
for baris in f:
yield baris.strip()
except FileNotFoundError:
print(f"File {filepath} tidak ditemukan")
# Generator range custom
def range_custom(start, stop=None, step=1):
"""Replikasi range() menggunakan generator"""
if stop is None:
start, stop = 0, start
current = start
while (step > 0 and current < stop) or (step < 0 and current > stop):
yield current
current += step
for x in range_custom(0, 10, 2):
print(x, end=" ")
# 0 2 4 6 8
Generator yang Bisa Dihentikan
# Generator dengan batas
def ambil_n(iterable, n):
"""Ambil n elemen pertama dari iterable"""
for i, item in enumerate(iterable):
if i >= n:
return # Menghentikan generator
yield item
# Menggunakan dengan infinite generator
gen = fibonacci()
sepuluh_pertama = list(ambil_n(gen, 10))
print(sepuluh_pertama)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Generator dengan kondisi berhenti
def ambil_selama(iterable, kondisi):
"""Ambil elemen selama kondisi terpenuhi"""
for item in iterable:
if not kondisi(item):
return
yield item
# Ambil Fibonacci yang kurang dari 100
kecil = list(ambil_selama(fibonacci(), lambda x: x < 100))
print(kecil)
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
# Generator dengan enumerate
def enumerate_gen(iterable, start=0):
"""Generator version of enumerate"""
n = start
for item in iterable:
yield n, item
n += 1
for idx, val in enumerate_gen(["a", "b", "c"]):
print(f"{idx}: {val}")
# 0: a
# 1: b
# 2: c
4. Generator Expressions
Generator expression (generator comprehension) adalah cara singkat untuk membuat generator menggunakan sintaks yang mirip dengan list comprehension, tetapi dengan tanda kurung ().
# Generator expression vs List comprehension
list_comp = [x ** 2 for x in range(10)] # List
gen_exp = (x ** 2 for x in range(10)) # Generator
print(type(list_comp)) # <class 'list'>
print(type(gen_exp)) # <class 'generator'>
# Generator expression langsung digunakan dengan fungsi yang butuh iterable
total = sum(x ** 2 for x in range(100))
print(total) # 328350
terbesar = max(x ** 2 for x in range(-10, 11))
print(terbesar) # 100
# any() dan all() dengan generator
angka = [2, 4, 6, 8, 10]
print(all(x % 2 == 0 for x in angka)) # True
angka = [1, 2, 3, 4, 5]
print(any(x > 3 for x in angka)) # True
# String join dengan generator
kata = ["Python", "Generator", "Sangat", "Efisien"]
kalimat = " ".join(k.lower() for k in kata)
print(kalimat) # python generator sangat efisien
# Filtering dengan generator expression
data = range(1, 101)
genap_gen = (x for x in data if x % 2 == 0)
print(list(genap_gen)[:10]) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# Generator expression dalam fungsi
def statistik(data):
"""Hitung statistik dari generator"""
data_list = list(data) # Konsumsi generator sekali
n = len(data_list)
total = sum(data_list)
return {
"total": total,
"rata_rata": total / n,
"min": min(data_list),
"max": max(data_list)
}
# Note: generator habis setelah dikonsumsi sekali!
gen = (x ** 2 for x in range(1, 11))
print(statistik(gen))
# {'total': 385, 'rata_rata': 38.5, 'min': 1, 'max': 100}
5. Memory Efficiency
Keunggulan utama generator adalah penghematan memori. Alih-alih menyimpan seluruh rangkaian data dalam memori, generator hanya menyimpan satu elemen pada satu waktu.
import sys
# === Perbandingan memori ===
n = 1_000_000
# List: menyimpan 1 juta elemen di RAM
list_data = [x ** 2 for x in range(n)]
print(f"List: {sys.getsizeof(list_data):>12,} bytes") # ~8,448,728 bytes (~8 MB)
# Generator: hanya menyimpan state
gen_data = (x ** 2 for x in range(n))
print(f"Generator:{sys.getsizeof(gen_data):>12,} bytes") # ~200 bytes
# Generator function
def gen_func():
for x in range(n):
yield x ** 2
gen_func_data = gen_func()
print(f"GenFunc: {sys.getsizeof(gen_func_data):>12,} bytes") # ~112 bytes
print(f"\nList menggunakan {sys.getsizeof(list_data) // sys.getsizeof(gen_data)}x lebih banyak memori!")
# === Demonstrasi dengan data besar ===
import os
import psutil
def get_memory_mb():
process = psutil.Process(os.getpid())
return process.memory_info().rss / 1024 / 1024
# List approach
print(f"Sebelum: {get_memory_mb():.1f} MB")
big_list = list(range(10_000_000))
print(f"Setelah list: {get_memory_mb():.1f} MB")
del big_list
# Generator approach
big_gen = range(10_000_000) # range adalah lazy sequence
print(f"Setelah range: {get_memory_mb():.1f} MB")
# ✅ GUNAKAN GENERATOR KETIKA:
# 1. Data terlalu besar untuk muat di memori
def baca_csv_generator(filepath):
import csv
with open(filepath, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
yield row # Satu baris pada satu waktu
# 2. Data adalah infinite sequence
def penghitung_waktu():
"""Stopwatch generator"""
import time
mulai = time.time()
while True:
yield time.time() - mulai
# 3. Pipeline processing (chaining)
def proses_pipeline(data):
"""Pipeline: filter → transform → aggregate"""
# Step 1: Filter (generator)
terfilter = (x for x in data if x > 0)
# Step 2: Transform (generator)
tertransformasi = (x ** 2 for x in terfilter)
# Step 3: Aggregate (consume)
return sum(tertransformasi)
data = [-3, 1, -4, 1, 5, -9, 2, 6, -5, 3]
print(proses_pipeline(data)) # 1 + 1 + 25 + 4 + 36 + 9 = 76
# ❌ JANGAN PAKAI GENERATOR KETIKA:
# 1. Butuh akses random (indexing)
# 2. Butuh iterasi berulang kali
# 3. Butuh len()
# 4. Data kecil dan perlu di-manipulate sebagai collection
6. Lazy Evaluation
Lazy evaluation berarti nilai hanya dihitung ketika benar-benar dibutuhkan. Ini berbeda dari eager evaluation di mana semua nilai dihitung sebelumnya.
# Lazy evaluation dengan generator
def operasi_berat(x):
"""Simulasi operasi yang memakan waktu"""
import time
time.sleep(0.1) # Simulasi delay
return x ** 2
# EAGER: Semua dihitung sebelum digunakan
# %timeit list_berat = [operasi_berat(x) for x in range(10)]
# Akan butuh ~1 detik karena semua 10 elemen langsung dihitung
# LAZY: Hanya dihitung saat diminta
gen_berat = (operasi_berat(x) for x in range(10))
# Tidak ada perhitungan yang terjadi di sini!
print("Generator dibuat, belum ada perhitungan")
# Perhitungan terjadi di sini (satu per satu):
print(next(gen_berat)) # 0 — baru hitung elemen pertama
print(next(gen_berat)) # 1 — baru hitung elemen kedua
# Sisanya belum dihitung sampai diminta!
# Chain generators — lazy pipeline
def angka_gen():
for i in range(100):
yield i
def filter_genap(source):
for x in source:
if x % 2 == 0:
yield x
def kali_dua(source):
for x in source:
yield x * 2
# Pipeline — semua lazy, belum ada perhitungan
pipeline = kali_dua(filter_genap(angka_gen()))
# Perhitungan terjadi di sini, satu per satu
for i, val in enumerate(pipeline):
if i >= 5:
break
print(val, end=" ")
# 0 4 8 12 16
7. Send, Throw & Close
Generator mendukung komunikasi dua arah melalui method send(), throw(), dan close().
# === Generator dengan send() ===
def accumulator():
"""Generator yang menerima nilai melalui send()"""
total = 0
while True:
nilai = yield total # yield total DAN terima nilai baru
if nilai is None:
break
total += nilai
gen = accumulator()
next(gen) # Inisialisasi: jalankan sampai yield pertama
print(gen.send(10)) # 10 (kirim 10, terima total)
print(gen.send(20)) # 30 (kirim 20, terima total)
print(gen.send(5)) # 35 (kirim 5, terima total)
# === Generator sebagai coroutine sederhana ===
def logger():
"""Generator yang menerima pesan log"""
log_list = []
while True:
pesan = yield
if pesan is None:
return log_list
log_list.append(pesan)
log_gen = logger()
next(log_gen) # Inisialisasi
log_gen.send("User login: Budi")
log_gen.send("Page visited: /dashboard")
log_gen.send("Action: click button")
try:
log_gen.send(None)
except StopIteration as e:
logs = e.value
print(logs)
# ['User login: Budi', 'Page visited: /dashboard', 'Action: click button']
# === throw() — mengirim exception ke generator ===
def pembagi():
while True:
try:
nilai = yield
hasil = 100 / nilai
print(f"100 / {nilai} = {hasil}")
except ZeroDivisionError:
print("⚠️ Tidak bisa bagi nol!")
except GeneratorExit:
print("👋 Generator ditutup!")
return
gen = pembagi()
next(gen)
gen.send(5) # 100 / 5 = 20.0
gen.send(0) # ⚠️ Tidak bisa bagi nol!
gen.throw(ValueError, "Error custom") # Akan raise ValueError di yield
# close() — menghentikan generator
gen = pembagi()
next(gen)
gen.send(10) # 100 / 10 = 10.0
gen.close() # 👋 Generator ditutup!
8. Yield From
yield from memungkinkan generator untuk mendelegasikan produksi nilai ke sub-generator atau iterable lain. Ini menyederhanakan nested generator patterns.
# === yield from dasar ===
def gen_a():
yield 1
yield 2
yield 3
def gen_b():
yield 10
yield 20
yield 30
# Tanpa yield from — verbose
def gabungan_lama():
for x in gen_a():
yield x
for x in gen_b():
yield x
# Dengan yield from — singkat!
def gabungan():
yield from gen_a()
yield from gen_b()
print(list(gabungan())) # [1, 2, 3, 10, 20, 30]
# === Flatten nested dengan yield from ===
def flatten(nested_list):
"""Flatten nested list menggunakan yield from"""
for item in nested_list:
if isinstance(item, list):
yield from flatten(item) # Rekursif!
else:
yield item
data = [1, [2, 3], [4, [5, 6]], [7, 8, [9, 10]]]
print(list(flatten(data)))
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# === yield from dengan return value ===
def sub_generator():
yield 1
yield 2
return "selesai" # Return value bisa ditangkap!
def delegator():
hasil = yield from sub_generator()
print(f"Sub-generator mengembalikan: {hasil}")
gen = delegator()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration:
pass
# Sub-generator mengembalikan: selesai
# === Pipeline dengan yield from ===
def baca_data():
for i in range(10):
yield i
def filter_data(source):
for x in source:
if x % 2 == 0:
yield x
def transform_data(source):
for x in source:
yield x ** 2
def pipeline():
yield from transform_data(filter_data(baca_data()))
print(list(pipeline()))
# [0, 4, 16, 36, 64]
9. Modul Itertools
Modul itertools menyediakan koleksi tools yang sangat powerful untuk bekerja dengan iterator dan generator. Ini adalah salah satu modul standar Python yang paling berguna.
import itertools
# === count() — bilangan tak terbatas ===
counter = itertools.count(start=10, step=3)
print(next(counter)) # 10
print(next(counter)) # 13
print(next(counter)) # 16
# Gunakan islice untuk membatasi
sepuluh_pertama = list(itertools.islice(counter, 10))
# === cycle() — iterasi berulang tak terbatas ===
colors = itertools.cycle(["merah", "hijau", "biru"])
for _ in range(7):
print(next(colors), end=" ")
# merah hijau biru merah hijau biru merah
# === repeat() — mengulang nilai ===
threes = itertools.repeat(3, times=5)
print(list(threes)) # [3, 3, 3, 3, 3]
# === chain() — menggabungkan beberapa iterable ===
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
gabungan = list(itertools.chain(a, b, c))
print(gabungan) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# === islice() — slicing untuk generator ===
infinite = itertools.count()
ambil_5 = list(itertools.islice(infinite, 5))
print(ambil_5) # [0, 1, 2, 3, 4]
ambil_range = list(itertools.islice(itertools.count(), 5, 15, 2))
print(ambil_range) # [5, 7, 9, 11, 13]
import itertools
# === permutations() — semua permutasi ===
items = ['A', 'B', 'C']
perms = list(itertools.permutations(items))
print(perms)
# [('A','B','C'), ('A','C','B'), ('B','A','C'),
# ('B','C','A'), ('C','A','B'), ('C','B','A')]
# Permutasi dengan panjang tertentu
perms_2 = list(itertools.permutations(items, 2))
print(perms_2)
# [('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')]
# === combinations() — semua kombinasi (tanpa urutan) ===
nums = [1, 2, 3, 4]
combs = list(itertools.combinations(nums, 2))
print(combs)
# [(1,2), (1,3), (1,4), (2,3), (2,4), (3,4)]
# === combinations_with_replacement() ===
combs_r = list(itertools.combinations_with_replacement([1, 2, 3], 2))
print(combs_r)
# [(1,1), (1,2), (1,3), (2,2), (2,3), (3,3)]
# === product() — Cartesian product ===
warna = ["merah", "biru"]
ukuran = ["S", "M", "L"]
produk = list(itertools.product(warna, ukuran))
print(produk)
# [('merah','S'), ('merah','M'), ('merah','L'),
# ('biru','S'), ('biru','M'), ('biru','L')]
# Product dengan repeat
kartu = list(itertools.product(range(1, 4), repeat=2))
print(kartu)
# [(1,1), (1,2), (1,3), (2,1), (2,2), (2,3), (3,1), (3,2), (3,3)]
import itertools
# === groupby() — mengelompokkan data ===
data = [
{"kota": "Jakarta", "nama": "Budi"},
{"kota": "Jakarta", "nama": "Ani"},
{"kota": "Bandung", "nama": "Dimas"},
{"kota": "Bandung", "nama": "Sari"},
{"kota": "Surabaya", "nama": "Rini"},
]
# Data HARUS diurutkan dulu untuk groupby!
data.sort(key=lambda x: x["kota"])
for kota, grup in itertools.groupby(data, key=lambda x: x["kota"]):
nama_list = [item["nama"] for item in grup]
print(f"{kota}: {nama_list}")
# Bandung: ['Dimas', 'Sari']
# Jakarta: ['Budi', 'Ani']
# Surabaya: ['Rini']
# === accumulate() — kumulatif ===
angka = [1, 2, 3, 4, 5]
kumulatif = list(itertools.accumulate(angka))
print(kumulatif) # [1, 3, 6, 10, 15]
# Dengan operator kustom
import operator
produk_kumulatif = list(itertools.accumulate(angka, operator.mul))
print(produk_kumulatif) # [1, 2, 6, 24, 120]
# accumulate untuk running max
data = [3, 1, 4, 1, 5, 9, 2, 6]
running_max = list(itertools.accumulate(data, max))
print(running_max) # [3, 3, 4, 4, 5, 9, 9, 9]
# === takewhile & dropwhile ===
angka = [1, 3, 5, 2, 4, 6, 7, 9]
ambil = list(itertools.takewhile(lambda x: x < 5, angka))
print(ambil) # [1, 3]
buang = list(itertools.dropwhile(lambda x: x < 5, angka))
print(buang) # [5, 2, 4, 6, 7, 9]
# === compress() — filter berdasarkan selector ===
data = ['A', 'B', 'C', 'D', 'E']
selector = [1, 0, 1, 0, 1]
terpilih = list(itertools.compress(data, selector))
print(terpilih) # ['A', 'C', 'E']
# === starmap() ===
pairs = [(2, 3), (4, 5), (6, 7)]
hasil = list(itertools.starmap(pow, pairs))
print(hasil) # [8, 1024, 279936]
10. Studi Kasus Praktis
Log File Processor
import itertools
from datetime import datetime
# Simulasi log entries
def generate_logs():
"""Generator yang menghasilkan log entries"""
logs = [
{"level": "INFO", "msg": "User login", "time": "2026-01-01 10:00"},
{"level": "ERROR", "msg": "DB connection failed", "time": "2026-01-01 10:01"},
{"level": "INFO", "msg": "Page loaded", "time": "2026-01-01 10:02"},
{"level": "WARNING", "msg": "Slow query", "time": "2026-01-01 10:03"},
{"level": "ERROR", "msg": "Timeout", "time": "2026-01-01 10:04"},
{"level": "INFO", "msg": "User logout", "time": "2026-01-01 10:05"},
{"level": "ERROR", "msg": "404 Not Found", "time": "2026-01-01 10:06"},
]
for log in logs:
yield log
# Pipeline: filter → transform → aggregate
def filter_errors(logs):
for log in logs:
if log["level"] == "ERROR":
yield log
def extract_messages(logs):
for log in logs:
yield log["msg"]
# Pipeline chaining
all_logs = generate_logs()
errors = filter_errors(all_logs)
messages = extract_messages(errors)
print("Error messages:")
for msg in messages:
print(f" ❌ {msg}")
# Error messages:
# ❌ DB connection failed
# ❌ Timeout
# ❌ 404 Not Found
Window Sliding Generator
import itertools
def sliding_window(iterable, n):
"""Generator untuk sliding window"""
it = iter(iterable)
window = list(itertools.islice(it, n))
if len(window) == n:
yield tuple(window)
for item in it:
window = window[1:] + [item]
yield tuple(window)
# Moving average
def moving_average(data, window_size):
"""Hitung moving average"""
for window in sliding_window(data, window_size):
yield sum(window) / len(window)
data = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
ma_3 = list(moving_average(data, 3))
print(f"Data: {data}")
print(f"MA-3: {[f'{x:.1f}' for x in ma_3]}")
# MA-3: ['20.0', '30.0', '40.0', '50.0', '60.0', '70.0', '80.0', '90.0']
# Chunk generator — bagi data menjadi batch
def chunk(iterable, size):
"""Bagi iterable menjadi batch berukuran tetap"""
it = iter(iterable)
while True:
batch = list(itertools.islice(it, size))
if not batch:
break
yield batch
data = range(1, 13)
for batch in chunk(data, 4):
print(batch)
# [1, 2, 3, 4]
# [5, 6, 7, 8]
# [9, 10, 11, 12]
11. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Generators: