Sering kali yang membuat program berhenti bukan kekurangan kecepatan, melainkan kehabisan memori — loading seluruh file log ke RAM sekaligus, atau membangun list jutaan elemen hanya untuk menjumlahkannya. Generator yang sudah kita kenal dari Chapter yield dan Generator Functions dan Lazy Evaluation menawarkan jalan keluar: menghasilkan satu item pada satu waktu sehingga memori yang dipakai tidak lagi sebanding dengan ukuran data. Pada Chapter ini kita ukur sendiri penghematannya dan melihat pola pemakaian yang tepat.
List vs Generator pada Range Besar #
Mari kita buktikan dengan sys.getsizeof(). List menyimpan setiap elemen sekaligus; generator hanya menyimpan kondisi perulangannya:
import sys
daftar = list(range(1_000_000))
generator = (i for i in range(1_000_000))
print("getsizeof(list(range(1_000_000))) :", f"{sys.getsizeof(daftar):,} byte")
print("getsizeof((i for i in range(...))) :", f"{sys.getsizeof(generator):,} byte")
print("elemen pertama generator :", next(generator))Output yang Dihasilkan:
getsizeof(list(range(1_000_000))) : 8,000,056 byte
getsizeof((i for i in range(...))) : 192 byte
elemen pertama generator : 0Selisihnya lebih dari 40.000x: 8 MB untuk array pointer list, versus 192 byte untuk generator. Baris ketiga menegaskan bahwa generator memang benar-benar menghasilkan data — next() hanya mengeluarkan satu elemen, sisanya belum pernah dibuat. Dan angka 8 MB pun masih dangkal: ingat peringatan dari Chapter Memahami Memory Management bahwa getsizeof tidak menghitung object yang dirujuk, dan sejuta object integer di dalam list menambah sekitar 28 MB lagi. Total nyatanya bisa kita lihat dengan tracemalloc, yang mengukur seluruh alokasi selama program berjalan:
import tracemalloc
tracemalloc.start()
total = sum([i * 2 for i in range(1_000_000)])
_, puncak_list = tracemalloc.get_traced_memory()
print(f"list comprehension : total = {total:,}, puncak memori = {puncak_list:,} byte")
tracemalloc.stop()
tracemalloc.start()
total = sum(i * 2 for i in range(1_000_000))
_, puncak_gen = tracemalloc.get_traced_memory()
print(f"generator expression: total = {total:,}, puncak memori = {puncak_gen:,} byte")
tracemalloc.stop()Output yang Dihasilkan:
list comprehension : total = 999,999,000,000, puncak memori = 40,444,616 byte
generator expression: total = 999,999,000,000, puncak memori = 464 byteTotal penjumlahan identik, tetapi puncak memori 40 MB versus 464 byte. Perhatikan bahwa sum() sendiri tidak butuh seluruh data sekaligus — generator memberi angka satu-satu dan sum mengakumulasinya, jadi tidak pernah ada “seluruh data” di memori.
Memproses File Baris demi Baris #
Bentuk paling praktis dari prinsip yang sama adalah membaca file. Pertama kita siapkan file contoh penjualan berisi 200.000 baris:
# Membuat file contoh: penjualan.log (1 baris = 1 transaksi)
with open("penjualan.log", "w", encoding="utf-8") as f:
for i in range(200_000):
f.write(f"TRX-{i:06d};toko-{i % 20:02d};Rp{(i * 1500) % 500_000};lunas\n")
import os
print(f"ukuran file penjualan.log: {os.path.getsize('penjualan.log'):,} byte")Output yang Dihasilkan:
ukuran file penjualan.log: 6,755,200 byteSekarang kita bandingkan dua cara menghitung: f.read() yang memuat seluruh isi ke satu string, versus iterasi file object yang menghasilkan satu baris dalam satu waktu:
import tracemalloc
NAMA_FILE = "penjualan.log"
def hitung_baris_read():
"Membaca seluruh file ke satu string besar."
with open(NAMA_FILE, encoding="utf-8") as f:
isi = f.read()
return isi.count("\n")
def hitung_baris_iterasi():
"Membaca satu baris demi satu baris."
jumlah = 0
with open(NAMA_FILE, encoding="utf-8") as f:
for baris in f:
jumlah += 1
return jumlah
tracemalloc.start()
hasil_1 = hitung_baris_read()
_, puncak_1 = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"read() : {hasil_1:,} baris, puncak memori = {puncak_1:,} byte")
tracemalloc.start()
hasil_2 = hitung_baris_iterasi()
_, puncak_2 = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"iterasi : {hasil_2:,} baris, puncak memori = {puncak_2:,} byte")Output yang Dihasilkan:
read() : 200,000 baris, puncak memori = 13,515,658 byte
iterasi : 200,000 baris, puncak memori = 21,847 byteFile 6,7 MB menjadi string 13,5 MB di memori saat didecode (sekitar dua kali lipat ukuran file), sementara iterasi baris stabil di 21 KB — kurang lebih satu baris buffer dipegang pada satu waktu. Untuk file log server berukuran gigabyte, read() akan langsung membunuh proses; iterasi berjalan dengan memori yang datar-datar saja.
Pipeline Generator: Matematika Memorinya #
Kekuatan generator penuh muncul saat beberapa tahap dirangkai — masing-masing function generator mengambil satu item, mengubahnya, lalu mengoperkannya ke tahap berikutnya:
def baca_baris(nama_file):
"Tahap 1: hasilkan satu baris dalam satu waktu."
with open(nama_file, encoding="utf-8") as f:
for baris in f:
yield baris.rstrip("\n")
def ambil_toko_lunas(baris_generator):
"Tahap 2: ubah tiap baris menjadi pasangan (toko, nilai) untuk transaksi lunas."
for baris in baris_generator:
kode, toko, nilai, status = baris.split(";")
if status == "lunas":
yield toko, int(nilai.removeprefix("Rp"))
def jumlah_per_toko(pasangan_generator):
"Tahap 3: agregasi -- satu-satunya tahap yang menyimpan hasil."
total = {}
for toko, nilai in pasangan_generator:
total[toko] = total.get(toko, 0) + nilai
return total
alur = jumlah_per_toko(ambil_toko_lunas(baca_baris("penjualan.log")))
hasil_tiga = dict(sorted(alur.items())[:3])
print("3 toko pertama :", hasil_tiga)
print("jumlah toko :", len(alur))Output yang Dihasilkan:
3 toko pertama : {'toko-00': 2450000000, 'toko-01': 2465000000, 'toko-02': 2480000000}
jumlah toko : 20Matematika memorinya sederhana: 200.000 baris × 3 tahap list bersarang akan menyimpan jutaan item antara sekaligus. Pada pipeline generator, setiap saat hanya ada satu baris dan satu pasangan di perjalanan — satu-satunya struktur yang tumbuh adalah dictionary agregasi berisi 20 toko, yang memang hasil akhir yang kita inginkan. Memori total pipeline ini mendekati konstan, O(ukuran output), bukan O(ukuran input).
Kapan List Tetap Lebih Baik #
Generator bukan kemenangan mutlak — sifat sekali pakai dan tanpa panjang membuatnya canggung untuk beberapa kebutuhan:
bilangan = (i for i in range(5))
print("konsumsi sekali :", list(bilangan))
print("konsumsi kedua :", list(bilangan)) # generator sudah habis
bilangan_baru = (i for i in range(5))
try:
len(bilangan_baru)
except TypeError as galat:
print("TypeError :", galat)Output yang Dihasilkan:
konsumsi sekali : [0, 1, 2, 3, 4]
konsumsi kedua : []
TypeError : object of type 'generator' has no len()Generator yang sudah dikonsumsi menghasilkan list kosong saat dipakai lagi, dan len() menolak bekerja karena jumlah elemen memang belum diketahui sebelum dihitung. Gunakan list ketika: data perlu diiterasi lebih dari sekali (misalnya diverifikasi lalu diproses), kita butuh len(), slicing, atau indexing acak, atau total ukurannya kecil sehingga kenyamanan list gratis. Untuk semua skenario lain — stream data besar, transformasi berantai, konsumsi satu arah — generator adalah struktur default yang paling hemat.