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 #

Modul itertools menyediakan tiga kategori utama fungsi iterator yang dirancang khusus untuk memproses aliran data (data streams) secara efisien dan hemat memori. Klasifikasi ketiga jenis iterator tersebut dapat dilihat pada diagram berikut:

flowchart TD
    Itertools["Kategori Iterator pada itertools"] --> Inf["Infinite Iterators (Iterasi Tanpa Batas)"]
    Itertools --> Term["Terminating Iterators (Iterasi dengan Batas)"]
    Itertools --> Comb["Combinatoric Iterators (Kombinatorik)"]
    
    Inf --> Inf1["count()"]
    Inf --> Inf2["cycle()"]
    Inf --> Inf3["repeat()"]
    
    Term --> Term1["chain()"]
    Term --> Term2["islice()"]
    Term --> Term3["groupby()"]
    Term --> TermOthers["Lainnya (accumulate, compress, dll.)"]
    
    Comb --> Comb1["product()"]
    Comb --> Comb2["permutations()"]
    Comb --> Comb3["combinations()"]

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 #

  • chain untuk menggabungkan beberapa iterable tanpa menyalin ke memori; chain.from_iterable untuk meng-flatten list of lists.
  • islice untuk mengambil sebagian elemen dari generator atau iterable besar tanpa memuat semua data.
  • groupby untuk mengelompokkan elemen — wajib sort terlebih dahulu berdasarkan kunci yang sama, atau hasilnya tidak sesuai harapan.
  • product untuk Cartesian product (pengganti nested for loop); combinations untuk kombinasi tanpa urutan; permutations untuk kombinasi dengan urutan.
  • partial untuk membuat fungsi baru dengan beberapa argumen sudah dikunci — menghindari repetisi argumen yang sama.
  • lru_cache / cache untuk memoization otomatis — cocok untuk fungsi murni yang sering dipanggil dengan argumen yang sama; argumen harus hashable.
  • reduce untuk akumulasi nilai yang tidak punya built-in — hindari untuk operasi yang sudah ada sum(), max(), min().
  • @wraps wajib digunakan dalam setiap dekorator agar metadata fungsi asli tidak hilang.

← Sebelumnya: Random   Berikutnya: Typing & Dataclasses →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact