unisbadri.com » Python Java Golang Typescript Kotlin Ruby Rust Dart PHP

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 #

  • 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