Program sering menghitung hal yang sama berkali-kali: function yang memvalidasi konfigurasi sama, query database yang identik, atau rekursi yang memanggil subsoal yang sama. Caching adalah strategi menyimpan hasil perhitungan agar pemanggilan berikutnya cukup membaca hasil lama. Python menyediakan perangkatnya di module functools — tetapi caching juga punya harga: memori, data basi, dan jebakan argument yang mutable. Pada Chapter ini kita pelajari kapan caching menyelamatkan performa dan kapan justru melukai.
functools.lru_cache: Memoization Siap Pakai #
Rekursi Fibonacci tanpa cache adalah contoh pemborosan paling terkenal — fib(30) menghitung fib(2) ratusan ribu kali. Decorator @functools.lru_cache menyimpan hasil setiap pemanggilan berdasarkan argument-nya:
import functools
import time
def fib_tanpa_cache(n):
if n < 2:
return n
return fib_tanpa_cache(n - 1) + fib_tanpa_cache(n - 2)
@functools.lru_cache(maxsize=None)
def fib_dengan_cache(n):
if n < 2:
return n
return fib_dengan_cache(n - 1) + fib_dengan_cache(n - 2)
mulai = time.perf_counter()
hasil_1 = fib_tanpa_cache(30)
waktu_1 = time.perf_counter() - mulai
mulai = time.perf_counter()
hasil_2 = fib_dengan_cache(30)
waktu_2 = time.perf_counter() - mulai
print(f"tanpa cache : fib(30) = {hasil_1} dalam {waktu_1:.4f} detik")
print(f"dengan cache: fib(30) = {hasil_2} dalam {waktu_2:.8f} detik")
print(f"cache_info : {fib_dengan_cache.cache_info()}")Output yang Dihasilkan (waktu bergantung pada mesin):
tanpa cache : fib(30) = 832040 dalam 0.1109 detik
dengan cache: fib(30) = 832040 dalam 0.00002307 detik
cache_info : CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)Mari kita telaah apa yang terjadi di balik layar:
- Baris
@functools.lru_cache(maxsize=None): mendekorasifib_dengan_cachesehingga setiap pemanggilan memeriksa dictionary internal berisi argument → hasil sebelum function asli dijalankan. - Baris
misses=31: dari 59 pemanggilan total (rekursifib(30)hanya butuh 31 nilai unik), 31 pemanggilan pertama benar-benar menghitung. - Baris
hits=28: 28 pemanggilan sisanya langsung dijawab dari cache — inilah selisih antara 0,11 detik dan 23 mikrosekon.
Parameter maxsize membatasi jumlah hasil yang disimpan; saat penuh, entri yang paling jarang dipakai dibuang (LRU = least recently used). maxsize=None berarti tanpa batas — untuk fungsi kecil seperti ini aman, untuk data besar berbahaya karena memori terus tumbuh.
Membaca cache_info dan Mereset dengan cache_clear #
Function terdekorasi membawa dua alat diagnosis. cache_info() melaporkan hits, misses, dan ukuran cache; cache_clear() mengosongkan seluruh cache:
import functools
@functools.lru_cache(maxsize=128)
def ambil_harga(nama_barang):
print(f" menghitung harga untuk {nama_barang}...")
return len(nama_barang) * 1000
print(ambil_harga("beras"))
print(ambil_harga("gula"))
print(ambil_harga("beras")) # cache hit
print(ambil_harga.cache_info())
ambil_harga.cache_clear()
print(ambil_harga.cache_info())Output yang Dihasilkan:
menghitung harga untuk beras...
5000
menghitung harga untuk gula...
4000
5000
CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)Pemanggilan ambil_harga("beras") kedua tidak mencetak baris “menghitung” — function aslinya tidak pernah berjalan. Setelah cache_clear(), seluruh pencacah kembali nol. Angka hits yang selalu rendah adalah sinyal cache tidak efektif dan hanya menambah overhead.
Sejak Python 3.9 tersedia juga functools.cache — versi sederhana lru_cache(maxsize=None) tanpa fitur LRU, cocok untuk memoization murni yang ukurannya pasti terkendali.
Kapan Caching Justru Menyakiti #
import functools
@functools.lru_cache(maxsize=128)
def total_pesanan(nama_barang, jumlah):
return jumlah * 1000
try:
total_pesanan(["beras", "gula"], 2)
except TypeError as galat:
print("TypeError:", galat)Output yang Dihasilkan:
TypeError: unhashable type: 'list'Solusinya sederhana: ubah list menjadi tuple sebelum masuk ke function ter-cache, atau terima elemen individual sebagai argument terpisah.
Jebakan kedua lebih halus: caching bekerja pada argument dan hasil, bukan pada state luar. Function yang membaca file, isi database, atau waktu sekarang akan mengembalikan data basi setelah dunia berubah — cache bisa membuat program salah, bukan sekadar boros. Terkait erat, ingat kembali anti-pola parameter mutable dari Chapter Default Parameters:
def catat_pesanan(nama_barang, keranjang=[]):
# ANTI-POLA: parameter mutable dipakai sebagai penampung status
keranjang.append(nama_barang)
return keranjang
print(catat_pesanan("beras"))
print(catat_pesanan("gula"))
print(catat_pesanan("teh"))Output yang Dihasilkan:
['beras']
['beras', 'gula']
['beras', 'gula', 'teh']List default yang sama dipakai bersama semua pemanggilan — state “menempel” persis seperti cache yang tidak pernah dibersihkan. Bayangkan benda seperti ini disimpan berbulan-bulan di server: memori tumbuh tanpa henti dan isinya semakin tidak relevan.
Memoization Manual dengan Dictionary #
Sebelum decorator ada, memoization ditulis tangan dengan dictionary — dan pola ini tetap berguna saat kita butuh kendali penuh, misalnya memilih sendiri kunci penyimpanannya:
def fib_memo(n, memo=None):
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n < 2:
return n
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
print("fib(10) :", fib_memo(10))
print("fib(60) :", fib_memo(60))
print("fib(100):", fib_memo(100))Output yang Dihasilkan:
fib(10) : 55
fib(60) : 1548008755920
fib(100): 354224848179261915075Perhatikan pola memo=None lalu if memo is None: memo = {} — membuat dictionary baru per pemanggilan teratas, menghindari jebakan parameter mutable di atas, sekaligus membuat dictionary bisa diteruskan turun di antara panggilan rekursif.
Masalah Invalidasi dan Cache Terdistribusi #
Dunia nyata menambahkan satu masalah tersulit: kapan cache harus ditinggalkan? File konfigurasi bisa berubah, harga barang bisa naik, cache di memori bisa hilang saat server restart. Prinsip yang sehat: beri cache masa kedaluwarsa (lru_cache punya cache_clear() untuk itu, dipanggil berkala atau saat data sumber berubah), dan simpan versi/timestamp data sumber sebagai bagian dari logika pembanding.
Konsep ini sama dengan HTTP caching yang kita jumpai sehari-hari di browser — header seperti Cache-Control dan ETag adalah cara server memberi tahu klien berapa lama boleh menyimpan dan bagaimana memvalidasi ulang. Ketika satu mesin tidak cukup, cache pindah ke memori terdistribusi seperti Redis yang dishare antar banyak process; Python cukup berbicara dengannya lewat client seperti redis-py. Skalanya berbeda, tetapi pertanyaannya tetap sama: apa kuncinya, berapa lama sah, dan siapa yang membatalkannya.