Generator di Python: yield, iterator, serta list dan dict comprehension
Generator di Python adalah objek yang menghasilkan nilai satu per satu saat diminta, tanpa menyimpan seluruh urutan di memori; dibuat dengan fungsi berkata kunci yield atau ekspresi generator dalam tanda kurung. Generator list, dict, dan set (comprehension) adalah penulisan singkat untuk membangun koleksi jadi dalam satu baris: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}.
Generator di Python adalah objek yang menghasilkan nilai satu per satu, hanya ketika diminta, dan tidak menyimpan seluruh urutan di memori. Ia dibuat dengan dua cara: sebagai fungsi dengan kata kunci yield, atau sebagai ekspresi generator dalam tanda kurung. Berdekatan dengannya ada list, dict, dan set comprehension — penulisan singkat untuk membangun koleksi jadi dalam satu baris. Berikut dibahas cara kerja python yield, perbedaan generator dengan iterator, dan cara menulis comprehension yang tetap mudah dibaca.
Iterator: fondasi perulangan for
Untuk memahami generator, kita perlu lebih dulu memahami iterator. Iterable adalah sesuatu yang bisa ditelusuri dengan perulangan: list, string, dict, berkas. Iterator adalah objek yang mengingat posisinya saat ini dan, setiap kali dipanggil next(), memberikan elemen berikutnya; jika elemen sudah habis, ia melempar StopIteration.
nums = [10, 20, 30]
it = iter(nums) # ambil iterator dari list
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
next(it) # StopIteration
Perulangan for persis melakukan ini: memanggil iter(), lalu next() sampai StopIteration. Iterator sendiri bisa ditulis sebagai kelas dengan metode __iter__ dan __next__:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3))) # [3, 2, 1]
Cara ini berfungsi, tetapi banyak kode berulang. Generator melakukan hal yang sama dalam tiga baris.
Apa itu generator di Python dan cara kerja yield
Fungsi generator adalah fungsi biasa yang di dalam tubuhnya memakai yield. Memanggilnya tidak menjalankan kode, melainkan mengembalikan objek generator. Setiap next() menjalankan tubuh fungsi sampai yield berikutnya, mengembalikan nilai, lalu membekukan fungsi beserta variabel lokalnya. next() berikutnya melanjutkan tepat dari titik itu.
def countdown(start):
while start > 0:
yield start
start -= 1
gen = countdown(3)
print(gen) # <generator object countdown at 0x...>
print(next(gen)) # 3
print(list(gen)) # [2, 1] — dilanjutkan dari titik berhenti
Ketika fungsi mencapai akhirnya (atau return), generator otomatis melempar StopIteration, dan perulangan for berakhir dengan rapi. Di sinilah perbedaan utama yield dan return: return mengakhiri fungsi dan mengembalikan satu nilai, sedangkan yield menjeda fungsi dan bisa menghasilkan nilai berapa pun banyaknya.
Generator dan iterator: perbedaannya
Setiap generator adalah iterator: ia punya __iter__ dan __next__. Tetapi tidak setiap iterator adalah generator. Perbedaannya ada pada cara pembuatan dan kemampuannya.
| Iterator (kelas) | Generator | |
|---|---|---|
| Cara membuat | kelas dengan __iter__ dan __next__ | fungsi dengan yield atau (x for x in ...) |
| Keadaan | diatur manual di atribut | disimpan otomatis di variabel lokal |
| StopIteration | harus Anda lemparkan sendiri | muncul sendiri saat fungsi selesai |
| Tambahan | sesuai yang Anda tulis | send(), throw(), close() |
Keduanya punya satu sifat penting yang sama: hanya bisa ditelusuri sekali. Perulangan for kedua pada generator yang sudah habis tidak menghasilkan apa-apa — buatlah generator baru.
Generator tak terbatas dan membaca berkas besar
Generator tidak harus berakhir. Urutan tak terbatas aman selama Anda hanya mengambil sejumlah elemen tertentu dari situ:
from itertools import islice
def ids():
n = 1
while True:
yield f"ID-{n:04d}"
n += 1
print(list(islice(ids(), 3))) # ['ID-0001', 'ID-0002', 'ID-0003']
Skenario yang paling praktis adalah memproses berkas besar baris demi baris. Generator bisa disambung menjadi pipa, sehingga pada setiap saat hanya satu baris yang ada di memori, bukan seluruh berkas:
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.rstrip("\n")
def only_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
for line in only_errors(read_lines("app.log")):
print(line)
yield from dan send
yield from meneruskan semua nilai dari iterable lain, sehingga perulangan bersarang tidak perlu ditulis:
def flatten(groups):
for g in groups:
yield from g
print(list(flatten([[1, 2], [3], [4, 5]]))) # [1, 2, 3, 4, 5]
Metode send() mengirim nilai ke dalam generator: nilai itu menjadi hasil dari ekspresi yield. Sebelum send pertama, generator harus dijalankan dulu dengan next(). Contohnya, penghitung rata-rata yang berjalan:
def averager():
total, count = 0, 0
avg = None
while True:
value = yield avg
total += value
count += 1
avg = total / count
a = averager()
next(a) # sampai yield pertama
print(a.send(10)) # 10.0
print(a.send(20)) # 15.0
Dalam kode aplikasi, send jarang dipakai, tetapi sering ditanyakan saat wawancara kerja.
range: apakah itu generator?
Bukan. range adalah urutan malas: ia juga tidak menyimpan semua angka, tetapi mendukung indeks, len(), pengecekan in, dan bisa ditelusuri berkali-kali. Generator tidak bisa melakukan semua itu.
r = range(5)
print(r[2], len(r), 3 in r) # 2 5 True
print(list(r), list(r)) # keduanya [0, 1, 2, 3, 4]
List comprehension: generator list
List comprehension ditulis [ekspresi for x in koleksi]. Ia membuat list baru dan tidak mengubah yang asli. Bacanya dari kiri ke kanan: pertama «apa yang dimasukkan», lalu «dari mana diambil».
amounts = [100, 250, 400]
# perulangan biasa
with_markup = []
for a in amounts:
with_markup.append(a * 1.2)
# sama dengan comprehension
with_markup = [a * 1.2 for a in amounts]
print(with_markup) # [120.0, 300.0, 480.0]
Meskipun disebut «generator list», hasilnya adalah list biasa yang sudah jadi, bukan generator: semua elemen dihitung sekaligus.
List comprehension dengan if dan if else
Di sinilah sering terjadi kebingungan. Kondisi filter ditaruh di akhir dan membuang elemen. Kondisi pemilihan (operator ternary) ditaruh di awal dan mengubah setiap elemen menjadi salah satu dari dua nilai, tanpa membuang apa pun.
amounts = [100, 250, 400]
big = [a for a in amounts if a > 200] # [250, 400]
labels = ["banyak" if a > 200 else "sedikit" for a in amounts]
# ['sedikit', 'banyak', 'banyak']
# filter dan pemilihan sekaligus
odd_marks = ["genap" if n % 2 == 0 else "ganjil" for n in range(6) if n > 2]
# ['ganjil', 'genap', 'ganjil']
Penulisan [a for a in amounts if a > 200 else 0] adalah kesalahan sintaks: else hanya ada pada kondisi di awal.
List bersarang dan list dua dimensi
Dua for ditulis dalam urutan yang sama dengan perulangan bersarang, dari atas ke bawah. Ini membantu agar urutan tidak tertukar saat menggabungkan list dari list:
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# setara dengan:
# for g in groups:
# for x in g:
# flat.append(x)
print(flat) # [1, 2, 3, 4, 5]
List dua dimensi (matriks) dibuat dengan comprehension di dalam comprehension. Kesalahan klasiknya adalah perkalian list: baris-barisnya menjadi objek yang sama.
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]] — kedua baris ikut berubah
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
# transposisi
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)]) # [[1, 4], [2, 5], [3, 6]]
Generator dict dan set
Pola yang sama berlaku untuk koleksi lain. Kurung kurawal dengan titik dua membangun dict (dict comprehension), tanpa titik dua membangun set (set comprehension).
spent = [("makan", 500), ("taksi", 300), ("makan", 200)]
categories = {cat for cat, _ in spent}
print(categories) # {'makan', 'taksi'} — urutan set tidak dijamin
last = {cat: amount for cat, amount in spent}
print(last) # {'makan': 200, 'taksi': 300}
squares = {n: n ** 2 for n in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
Perhatikan last: jika kunci berulang, nilai terakhir yang menang, jadi jumlah 500 hilang. Jika ingin menjumlahkan, comprehension tidak cocok — tulis perulangan atau gunakan collections.Counter / defaultdict.
Bagaimana dengan generator tuple?
Di Python tidak ada «tuple comprehension» tersendiri. Tanda kurung menghasilkan ekspresi generator, dan tuple dibuat dengan memberikannya ke tuple():
t = tuple(n * 2 for n in range(3)) # (0, 2, 4)
Ekspresi generator: menghasilkan, bukan menyimpan
Jika kurung siku diganti kurung biasa, hasilnya adalah ekspresi generator. Ia tidak menghitung apa pun di muka dan menghasilkan nilai satu per satu. Untuk satu juta elemen, perbedaan memorinya sangat besar:
import sys
as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list)) # sekitar 8 MB
print(sys.getsizeof(as_gen)) # beberapa ratus byte
Ekspresi generator paling sering dipakai di dalam fungsi agregasi. Jika generator adalah satu-satunya argumen, tanda kurung tambahan tidak perlu ditulis. any() dan all() juga berhenti pada jawaban pertama dan tidak memeriksa sisanya.
spent = [("makan", 500), ("taksi", 300), ("makan", 200)]
total = sum(a for _, a in spent) # 1000
has_big = any(a > 400 for _, a in spent) # True, setelah 500 tidak diperiksa lagi
biggest = max(spent, key=lambda p: p[1]) # ('makan', 500)
Aturan memilihnya sederhana: jika hasilnya dibutuhkan berkali-kali, perlu indeks atau len(), pakai list. Jika cukup ditelusuri sekali untuk dihitung sesuatu, pakai generator.
Ruang lingkup: variabel tidak bocor
Pertanyaan yang sering membuat bingung bahkan programmer berpengalaman:
x = 10
squares = [x for x in range(3)]
print(x) # 10
Variabel perulangan di dalam comprehension hidup di ruang lingkupnya sendiri dan tidak menimpa variabel luar (ini berlaku di Python 3; di Python 2 perilakunya berbeda). Di perulangan for biasa, setelah selesai x akan bernilai 2. Sejak 3.12 (PEP 709) comprehension disisipkan ke dalam fungsi yang melingkupinya sehingga lebih cepat, tetapi aturan ruang lingkupnya tetap sama.
Pembongkaran * dan ** di comprehension: baru di Python 3.15 (PEP 798)
Tanda bintang untuk pembongkaran sudah lama bisa ditulis di pemanggilan fungsi dan di literal list, tetapi di dalam comprehension itu menghasilkan SyntaxError. Di Python 3.15 larangan ini dicabut: *elemen berarti «masukkan di sini semua elemennya». Ini berfungsi di list, set, dan generator, termasuk yang asinkron.
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups] # [1, 2, 3, 4, 5]
uniq = {*g for g in groups} # set
lazy = (*g for g in groups) # generator malas
Tanda bintang ganda melakukan hal yang sama untuk dict. Ini nyaman untuk pengaturan berlapis «bawaan → berkas → flag»: setiap lapisan berikutnya menimpa kunci lapisan sebelumnya.
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
Dua catatan dari materi kursus. Tanda bintang tidak boleh digabung dengan ekspresi kondisional tanpa tanda kurung: [*w if w else 0 for w in weeks] adalah kesalahan sintaks, kondisinya harus dibungkus kurung. Dan kode seperti ini tidak akan berjalan di 3.14 ke bawah: jika pustaka Anda mendukung versi lama, pakai perulangan for ganda atau itertools.chain.from_iterable.
Generator asinkron
Jika fungsi async def memakai yield, hasilnya adalah generator asinkron. Ia ditelusuri dengan async for, dan di dalamnya bisa menunggu jaringan atau disk dengan await. Ada juga comprehension asinkron.
import asyncio
async def ticks(n):
for i in range(n):
await asyncio.sleep(0.1)
yield i
async def main():
result = [i async for i in ticks(3)]
print(result) # [0, 1, 2]
asyncio.run(main())
Kesalahan umum
- Comprehension bertingkat tiga. Dua
for, duaif, dan ternary dalam satu baris tidak akan terbaca seminggu kemudian. Jika tidak muat dalam satu baris yang jelas, tulis perulangan biasa: PEP 8 mengutamakan keterbacaan di atas keringkasan. - Menelusuri generator dua kali.
sum(gen)kedua akan menghasilkan 0, karena generator sudah habis. - Comprehension demi efek samping.
[print(x) for x in xs]membangun listNoneyang tidak perlu. Untuk tindakan, gunakan perulanganfor. - Mengharapkan list dari ekspresi generator.
(x for x in xs)[0]menghasilkanTypeError: generator tidak punya indeks.
Latihan generator
- Tulis generator
fib()yang menghasilkan bilangan Fibonacci tanpa henti, lalu ambil 10 pertama denganislice. - Dengan satu baris, buat dict «kata → panjang» untuk kata yang lebih dari tiga huruf.
- Hitung jumlah kuadrat bilangan genap sampai satu juta tanpa membuat list.
- Tulis generator
chunks(seq, n)yang menghasilkan potongan list sepanjangnelemen.
Ringkasan
Iterator memberikan elemen satu per satu lewat next(), dan generator adalah cara termudah untuk membuatnya: fungsi dengan python yield atau ekspresi dalam tanda kurung. Generator list, dict, dan set membangun koleksi jadi dalam satu baris; filter if ditaruh di akhir, pemilihan if else di awal. Jika ingin menghemat memori atau memproses aliran data sekali jalan, gunakan generator; jika hasilnya dibutuhkan berulang kali, gunakan list. Comprehension, generator, dan pembongkaran baru dari Python 3.15 dipraktikkan di kursus dengan contoh pencatat pengeluaran.
Pertanyaan umum
Bolehkah memakai return di dalam generator?
Boleh. return tanpa nilai hanya mengakhiri generator, sedangkan return dengan nilai disimpan di atribut value dari exception StopIteration; nilai itu diterima oleh ekspresi yield from.
Bagaimana mengubah generator menjadi list?
Berikan generator ke list(): list(gen). Setelah itu generator sudah habis, dan semua nilainya berada di memori.
Mana yang lebih cepat, list comprehension atau perulangan dengan append?
Biasanya comprehension sedikit lebih cepat, karena tidak memanggil metode append di setiap iterasi. Namun pilihlah berdasarkan keterbacaan; selisihnya jarang menentukan.
Bagaimana mengetahui bahwa sebuah objek adalah generator?
Gunakan inspect.isgenerator(obj) untuk objek, atau inspect.isgeneratorfunction(f) untuk fungsi. Untuk memastikan objek adalah iterator, gunakan isinstance(obj, collections.abc.Iterator).
Apa bedanya ekspresi generator dan list comprehension?
Kurung siku langsung membangun seluruh list di memori, sedangkan kurung biasa membuat generator yang menghitung elemen saat diminta dan hanya bisa ditelusuri sekali.
Topik ini dibahas mendalam dengan praktik di kursus “Python 3.15: от новичка до профи”.
Ke kursus