Itertools & Functools #
Dua modul ini adalah kunci untuk menulis kode Python yang lebih ekspresif dan efisien. itertools menyediakan building block untuk iterasi — memproses data secara lazy tanpa memuat semuanya ke memori sekaligus. functools menyediakan alat untuk pemrograman fungsional — mengubah, menggabungkan, dan mengoptimasi fungsi. Keduanya sering digunakan bersama, dan memahaminya adalah tanda kode Python yang matang.
Modul itertools
#
chain — Gabungkan Beberapa Iterable
#
chain menggabungkan beberapa iterable seolah-olah menjadi satu, tanpa membuat salinan di memori.
from itertools import chain
# ANTI-PATTERN: gabungkan dengan + (membuat list baru di memori)
hasil = [1, 2, 3] + [4, 5, 6] + [7, 8, 9]
# BENAR: chain tidak membuat salinan, diproses satu per satu
for item in chain([1, 2, 3], [4, 5, 6], [7, 8, 9]):
print(item)
# chain.from_iterable() -- untuk list of lists
data = [[1, 2], [3, 4], [5, 6]]
# ANTI-PATTERN: flatten dengan list comprehension bertingkat
flat = [x for sublist in data for x in sublist]
# BENAR: chain.from_iterable()
flat = list(chain.from_iterable(data))
print(flat) # [1, 2, 3, 4, 5, 6]
# Contoh nyata: gabungkan hasil query dari beberapa tabel
def ambil_semua_produk():
produk_elektronik = ["laptop", "hp", "tablet"]
produk_fashion = ["baju", "celana", "sepatu"]
produk_makanan = ["roti", "susu", "keju"]
return chain(produk_elektronik, produk_fashion, produk_makanan)
for produk in ambil_semua_produk():
print(produk)
islice — Slice Lazy pada Iterable
#
islice mengambil sebagian elemen dari iterable tanpa harus memuat seluruh data terlebih dahulu. Berguna untuk generator atau stream data yang sangat besar.
from itertools import islice
# Ambil N elemen pertama dari generator
def angka_tak_terhingga():
n = 0
while True:
yield n
n += 1
# ANTI-PATTERN: tidak bisa slice generator biasa
# gen = angka_tak_terhingga()
# gen[:10] # TypeError: 'generator' object is not subscriptable
# BENAR: gunakan islice
sepuluh_pertama = list(islice(angka_tak_terhingga(), 10))
print(sepuluh_pertama) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# islice(iterable, start, stop, step)
data = range(100)
print(list(islice(data, 10, 20))) # [10, 11, ..., 19]
print(list(islice(data, 0, 50, 5))) # [0, 5, 10, ..., 45]
# Contoh nyata: baca file CSV baris per baris, skip header, ambil 100 baris
def baca_batch(filepath, skip=1, ambil=100):
with open(filepath) as f:
for baris in islice(f, skip, skip + ambil):
yield baris.strip()
groupby — Kelompokkan Elemen Berurutan
#
groupby mengelompokkan elemen berurutan yang memiliki nilai kunci yang sama. Penting: data harus sudah diurutkan berdasarkan kunci sebelum di-groupby.
from itertools import groupby
data = [
{"nama": "Alice", "dept": "Engineering"},
{"nama": "Bob", "dept": "Engineering"},
{"nama": "Carol", "dept": "Marketing"},
{"nama": "Dave", "dept": "Marketing"},
{"nama": "Eve", "dept": "Engineering"}, # Engineering lagi setelah Marketing
]
# ANTI-PATTERN: groupby tanpa sort dulu
for dept, anggota in groupby(data, key=lambda x: x["dept"]):
print(dept, list(anggota))
# Engineering: Alice, Bob
# Marketing: Carol, Dave
# Engineering: Eve <-- muncul lagi karena tidak diurutkan!
# BENAR: sort dulu berdasarkan kunci yang sama
data_sorted = sorted(data, key=lambda x: x["dept"])
for dept, anggota in groupby(data_sorted, key=lambda x: x["dept"]):
print(dept, [a["nama"] for a in anggota])
# Engineering: ['Alice', 'Bob', 'Eve']
# Marketing: ['Carol', 'Dave']
from itertools import groupby
# Contoh nyata: kelompokkan transaksi per tanggal
transaksi = [
{"tanggal": "2024-01-01", "jumlah": 150000},
{"tanggal": "2024-01-01", "jumlah": 75000},
{"tanggal": "2024-01-02", "jumlah": 200000},
{"tanggal": "2024-01-03", "jumlah": 50000},
{"tanggal": "2024-01-03", "jumlah": 125000},
]
transaksi.sort(key=lambda x: x["tanggal"])
for tanggal, grup in groupby(transaksi, key=lambda x: x["tanggal"]):
total = sum(t["jumlah"] for t in grup)
print(f"{tanggal}: Rp{total:,.0f}")
# 2024-01-01: Rp225,000
# 2024-01-02: Rp200,000
# 2024-01-03: Rp175,000
product, combinations, permutations — Kombinatorik
#
from itertools import product, combinations, permutations
# product() -- Cartesian product (seperti nested for loop)
warna = ["merah", "biru"]
ukuran = ["S", "M", "L"]
for w, u in product(warna, ukuran):
print(f"{w}-{u}", end=" ")
# merah-S merah-M merah-L biru-S biru-M biru-L
# product() dengan repeat -- kombinasi kartu
# semua pasangan dadu (6x6 = 36 kemungkinan)
dadu = list(product(range(1, 7), repeat=2))
print(len(dadu)) # 36
# combinations() -- kombinasi tanpa pengulangan, urutan tidak penting
tim = ["Alice", "Bob", "Carol", "Dave"]
for pasangan in combinations(tim, 2):
print(pasangan)
# ('Alice', 'Bob'), ('Alice', 'Carol'), ('Alice', 'Dave'),
# ('Bob', 'Carol'), ('Bob', 'Dave'), ('Carol', 'Dave')
print(len(list(combinations(tim, 2)))) # 6 = C(4,2)
# permutations() -- seperti kombinasi tapi urutan penting
for urutan in permutations(["A", "B", "C"], 2):
print(urutan)
# ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')
print(len(list(permutations(["A", "B", "C"], 2)))) # 6 = P(3,2)
count, cycle, repeat — Iterator Tak Terhingga
#
from itertools import count, cycle, repeat
# count(start, step) -- hitung dari start, terus menerus
for i, item in zip(count(1), ["a", "b", "c", "d"]):
print(f"{i}. {item}")
# 1. a 2. b 3. c 4. d
# cycle() -- ulang elemen secara siklik
warna_alternating = cycle(["merah", "putih"])
for i, warna in zip(range(6), warna_alternating):
print(f"Baris {i}: {warna}")
# Baris 0: merah Baris 1: putih Baris 2: merah ...
# repeat(object, times) -- ulangi elemen sebanyak n kali
print(list(repeat("x", 5))) # ['x', 'x', 'x', 'x', 'x']
# Berguna dengan map() untuk memberikan argumen tetap
from itertools import starmap
print(list(starmap(pow, [(2, 3), (3, 2), (4, 2)]))) # [8, 9, 16]
takewhile dan dropwhile
#
from itertools import takewhile, dropwhile
data = [1, 3, 5, 2, 8, 4, 7]
# takewhile() -- ambil elemen selama kondisi True, berhenti saat False
print(list(takewhile(lambda x: x < 6, data))) # [1, 3, 5]
# berhenti di 2 karena sebelumnya 5 < 6 True, tapi 2 < 6 juga True...
# -- sebenarnya berhenti saat kondisi PERTAMA KALI False
print(list(takewhile(lambda x: x % 2 != 0, data))) # [1, 3, 5]
# berhenti di 2 (angka genap pertama)
# dropwhile() -- lewati elemen selama kondisi True, ambil sisanya
print(list(dropwhile(lambda x: x < 6, data))) # [2, 8, 4, 7]
# mulai mengambil dari 2 (pertama kali kondisi False: 2 < 6 adalah False?
# -- tidak, 2 < 6 masih True. Tapi urutan: 1<6, 3<6, 5<6, lalu 2 yang ke-4,
# 2 < 6 True, 8 < 6 False -> mulai ambil dari 8)
data2 = [1, 2, 3, 10, 4, 5]
print(list(dropwhile(lambda x: x < 5, data2))) # [10, 4, 5]
zip_longest dan pairwise
#
from itertools import zip_longest, pairwise
# zip_longest() -- zip tapi tidak berhenti di iterable terpendek
nama = ["Alice", "Bob", "Carol"]
skor = [85, 92]
# zip biasa -- berhenti di yang terpendek
print(list(zip(nama, skor))) # [('Alice', 85), ('Bob', 92)]
# zip_longest -- isi dengan fillvalue
print(list(zip_longest(nama, skor, fillvalue=0)))
# [('Alice', 85), ('Bob', 92), ('Carol', 0)]
# pairwise() -- pasangkan elemen berurutan (Python 3.10+)
data = [1, 2, 3, 4, 5]
print(list(pairwise(data))) # [(1,2), (2,3), (3,4), (4,5)]
# Contoh nyata: hitung selisih antar data berurutan
harga = [100, 105, 98, 112, 108]
selisih = [b - a for a, b in pairwise(harga)]
print(selisih) # [5, -7, 14, -4]
Modul functools
#
partial — Fungsi dengan Argumen Terkunci
#
partial membuat fungsi baru dari fungsi yang ada dengan beberapa argumen sudah ditentukan nilainya. Berguna untuk menghindari repetisi argumen yang sama.
from functools import partial
# Fungsi asli
def kirim_email(to: str, subject: str, body: str, from_addr: str = "[email protected]"):
print(f"From: {from_addr} | To: {to} | Subject: {subject}")
print(f"Body: {body}")
# ANTI-PATTERN: ulangi argumen yang sama terus-menerus
kirim_email("[email protected]", "Selamat Datang", "...", from_addr="[email protected]")
kirim_email("[email protected]", "Selamat Datang", "...", from_addr="[email protected]")
# BENAR: buat fungsi baru dengan argumen yang sudah dikunci
kirim_dari_support = partial(kirim_email, from_addr="[email protected]")
kirim_dari_support("[email protected]", "Selamat Datang", "...")
kirim_dari_support("[email protected]", "Verifikasi Email", "...")
# Contoh lain: sort dengan key yang dikonfigurasi
from functools import partial
def ambil_field(obj, field):
return obj[field]
data = [{"nama": "Carol", "usia": 30}, {"nama": "Alice", "usia": 25}, {"nama": "Bob", "usia": 28}]
ambil_nama = partial(ambil_field, field="nama")
ambil_usia = partial(ambil_field, field="usia")
print(sorted(data, key=ambil_nama)) # urut berdasarkan nama
print(sorted(data, key=ambil_usia)) # urut berdasarkan usia
lru_cache — Memoization Otomatis
#
lru_cache (Least Recently Used cache) menyimpan hasil pemanggilan fungsi sehingga pemanggilan dengan argumen yang sama tidak perlu dihitung ulang.
from functools import lru_cache
import time
# Tanpa cache -- sangat lambat untuk n besar
def fibonacci_lambat(n):
if n < 2:
return n
return fibonacci_lambat(n - 1) + fibonacci_lambat(n - 2)
# Dengan cache -- sangat cepat
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # instan
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# cache_clear() -- kosongkan cache
fibonacci.cache_clear()
# @cache (Python 3.9+) -- seperti lru_cache(maxsize=None), tidak ada batas
from functools import cache
@cache
def faktorial(n):
return 1 if n == 0 else n * faktorial(n - 1)
from functools import lru_cache
# Contoh nyata: cache hasil query yang mahal
@lru_cache(maxsize=256)
def ambil_data_kota(kota_id: int) -> dict:
"""Simulasi query database yang lambat."""
time.sleep(0.1) # simulasi latency database
return {"id": kota_id, "nama": f"Kota-{kota_id}", "populasi": kota_id * 10000}
# Pemanggilan pertama: lambat (query database)
data = ambil_data_kota(1) # 0.1 detik
# Pemanggilan kedua dengan argumen sama: instan (dari cache)
data = ambil_data_kota(1) # < 1ms
lru_cache hanya bekerja untuk fungsi dengan argumen yang hashable (immutable). Fungsi yang menerima list, dict, atau objek mutable sebagai argumen tidak bisa di-cache langsung. Konversi ke tuple atau frozenset terlebih dahulu jika perlu.reduce — Akumulasi Nilai
#
reduce mengaplikasikan fungsi dua-argumen secara akumulatif ke elemen iterable dari kiri ke kanan.
from functools import reduce
angka = [1, 2, 3, 4, 5]
# ANTI-PATTERN: gunakan reduce untuk operasi yang sudah ada built-in-nya
total = reduce(lambda a, b: a + b, angka) # gunakan sum() saja!
maksimum = reduce(lambda a, b: a if a > b else b, angka) # gunakan max() saja!
# BENAR: gunakan reduce untuk operasi yang tidak ada built-in-nya
# Contoh: nested dict access
from functools import reduce
config = {
"database": {
"primary": {
"host": "db.example.com",
"port": 5432
}
}
}
def ambil_nested(data: dict, keys: list):
"""Ambil nilai dari nested dict dengan list of keys."""
return reduce(lambda d, k: d[k], keys, data)
print(ambil_nested(config, ["database", "primary", "host"])) # "db.example.com"
print(ambil_nested(config, ["database", "primary", "port"])) # 5432
# Contoh lain: pipeline transformasi
operasi = [
lambda x: x * 2,
lambda x: x + 10,
lambda x: x ** 2,
]
hasil = reduce(lambda val, fn: fn(val), operasi, 5)
# 5 -> *2 -> 10 -> +10 -> 20 -> **2 -> 400
print(hasil) # 400
wraps — Dekorator yang Benar
#
Saat membuat dekorator, gunakan @wraps agar fungsi yang didekorasi mempertahankan metadata aslinya (nama, docstring, signature).
from functools import wraps
import time
# ANTI-PATTERN: dekorator tanpa @wraps
def timer_buruk(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"Waktu: {time.time() - start:.3f}s")
return result
return wrapper
@timer_buruk
def hitung():
"""Fungsi penghitungan."""
return sum(range(1000000))
print(hitung.__name__) # "wrapper" -- nama asli hilang!
print(hitung.__doc__) # None -- docstring hilang!
# BENAR: gunakan @wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"Waktu: {time.time() - start:.3f}s")
return result
return wrapper
@timer
def hitung():
"""Fungsi penghitungan."""
return sum(range(1000000))
print(hitung.__name__) # "hitung" -- nama terjaga
print(hitung.__doc__) # "Fungsi penghitungan." -- docstring terjaga
total_ordering — Lengkapi Operator Perbandingan
#
total_ordering mengisi operator perbandingan yang hilang dari sebuah class. Kamu hanya perlu mendefinisikan __eq__ dan satu dari __lt__, __le__, __gt__, atau __ge__.
from functools import total_ordering
@total_ordering
class Mahasiswa:
def __init__(self, nama: str, ipk: float):
self.nama = nama
self.ipk = ipk
def __eq__(self, other):
return self.ipk == other.ipk
def __lt__(self, other):
return self.ipk < other.ipk
# total_ordering otomatis mengisi: >, >=, <=
mhs1 = Mahasiswa("Alice", 3.75)
mhs2 = Mahasiswa("Bob", 3.50)
print(mhs1 > mhs2) # True
print(mhs1 >= mhs2) # True
print(mhs1 <= mhs2) # False
print(sorted([mhs1, mhs2])) # [Bob(3.5), Alice(3.75)]
Menggabungkan itertools dan functools #
from itertools import groupby, chain
from functools import reduce
# Contoh: analisis penjualan per kategori dari beberapa sumber data
penjualan_jan = [
{"kategori": "Elektronik", "total": 5000000},
{"kategori": "Fashion", "total": 2000000},
{"kategori": "Elektronik", "total": 3500000},
]
penjualan_feb = [
{"kategori": "Fashion", "total": 2500000},
{"kategori": "Elektronik", "total": 4000000},
{"kategori": "Makanan", "total": 1500000},
]
# Gabungkan semua data dengan chain
semua = sorted(
chain(penjualan_jan, penjualan_feb),
key=lambda x: x["kategori"]
)
# Kelompokkan dan jumlahkan per kategori
for kategori, grup in groupby(semua, key=lambda x: x["kategori"]):
total = reduce(lambda acc, x: acc + x["total"], grup, 0)
print(f"{kategori}: Rp{total:,.0f}")
# Elektronik: Rp12,500,000
# Fashion: Rp4,500,000
# Makanan: Rp1,500,000
Ringkasan #
chainuntuk menggabungkan beberapa iterable tanpa menyalin ke memori;chain.from_iterableuntuk meng-flatten list of lists.isliceuntuk mengambil sebagian elemen dari generator atau iterable besar tanpa memuat semua data.groupbyuntuk mengelompokkan elemen — wajib sort terlebih dahulu berdasarkan kunci yang sama, atau hasilnya tidak sesuai harapan.productuntuk Cartesian product (pengganti nested for loop);combinationsuntuk kombinasi tanpa urutan;permutationsuntuk kombinasi dengan urutan.partialuntuk membuat fungsi baru dengan beberapa argumen sudah dikunci — menghindari repetisi argumen yang sama.lru_cache/cacheuntuk memoization otomatis — cocok untuk fungsi murni yang sering dipanggil dengan argumen yang sama; argumen harus hashable.reduceuntuk akumulasi nilai yang tidak punya built-in — hindari untuk operasi yang sudah adasum(),max(),min().@wrapswajib digunakan dalam setiap dekorator agar metadata fungsi asli tidak hilang.