Beberapa program Python lambat bukan karena logikanya salah, melainkan karena menghitung ulang hal yang sama berkali-kali. Saya pernah menurunkan waktu sebuah fungsi rekursif dari puluhan detik menjadi hitungan milidetik hanya dengan satu baris dekorator. Trik itu datang dari functools, modul bawaan Python yang jarang dibahas padahal isinya sangat berguna sehari-hari. Lima alat di dalamnya yang paling sering saya pakai: lru_cache, partial, wraps, reduce, dan cached_property. Kita bongkar satu per satu, lengkap dengan kode yang bisa langsung kamu jalankan.
lru_cache: Simpan Hasil, Jangan Hitung Ulang
Kasus paling jelas adalah fungsi rekursif seperti Fibonacci. Tanpa caching, fib(32) memanggil dirinya sendiri jutaan kali karena nilai yang sama dihitung berulang. Tambahkan satu baris @lru_cache dan setiap hasil langsung disimpan, jadi setiap n cukup dihitung sekali. Bandingkan sendiri:
import time
from functools import lru_cache
def fib_naive(n: int) -> int:
if n < 2:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
@lru_cache(maxsize=None)
def fib_cached(n: int) -> int:
if n < 2:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
start = time.perf_counter()
fib_naive(32)
print(f"naive : {time.perf_counter() - start:.3f} detik")
start = time.perf_counter()
fib_cached(200)
print(f"cached: {time.perf_counter() - start:.3f} detik")
print(fib_cached.cache_info())
maxsize menentukan berapa banyak hasil yang disimpan; None berarti simpan semuanya. Pada fungsi dengan argumen yang nyaris tak terbatas, angka seperti 128 atau 256 biasanya cukup untuk menahan pertumbuhan memori. Kamu bisa memeriksa seberapa efektif cache bekerja lewat cache_info(), atau mengosongkannya dengan cache_clear().
Gunakan lru_cache hanya untuk fungsi murni — fungsi yang untuk argumen yang sama selalu mengembalikan hasil yang sama, tanpa efek samping. Jangan memasang cache pada fungsi yang membaca file atau database yang bisa berubah: kamu akan mendapat data basi tanpa sadar.
partial: Bekukan Sebagian Argumen
partial membekukan sebagian argumen sebuah fungsi dan mengembalikan fungsi baru yang lebih spesifik. Ini berguna saat kamu memanggil fungsi yang sama berulang kali dengan sebagian besar parameter yang identik, misalnya klien HTTP internal.
from functools import partial
def kirim_request(url: str, method: str = "GET", timeout: int = 5, headers: dict | None = None):
print(f"{method} {url} (timeout={timeout}s, headers={headers})")
get_json = partial(kirim_request, method="GET", timeout=10)
post_json = partial(kirim_request, method="POST", timeout=30)
get_json("https://api.example.com/users")
post_json("https://api.example.com/users", headers={"X-Token": "abc123"})
# GET https://api.example.com/users (timeout=10s, headers=None)
# POST https://api.example.com/users (timeout=30s, headers={'X-Token': 'abc123'})
Alih-alih menulis ulang method dan timeout di setiap panggilan, kamu membuat dua varian siap pakai. Argumen yang tersisa tetap bisa diisi belakangan, seperti headers pada post_json. Pola yang sama juga rapi dipakai bersama map atau callback yang mengharuskan tanda tangan fungsi tertentu.
wraps: Jaga Metadata Fungsi di Dalam Decorator
Kalau kamu pernah menulis decorator sendiri, kamu pasti menyadari satu masalah: decorator menimpa nama dan docstring fungsi aslinya. wraps menyelesaikannya dengan satu baris. Saya sudah membahas cara membuat decorator dari nol di Python Decorators: Panduan Lengkap — bagian ini melengkapinya.
from functools import wraps
def tanpa_wraps(func):
def wrapper(*args, **kwargs):
print(f"memanggil {func.__name__}")
return func(*args, **kwargs)
return wrapper
def dengan_wraps(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"memanggil {func.__name__}")
return func(*args, **kwargs)
return wrapper
@tanpa_wraps
def a():
"""Fungsi A."""
return "ok"
@dengan_wraps
def b():
"""Fungsi B."""
return "ok"
print(a.__name__, a.__doc__) # wrapper None
print(b.__name__, b.__doc__) # b Fungsi B.
Tanpa @wraps, a.__name__ berubah jadi wrapper dan a.__doc__ menjadi None. Kalau kamu mengandalkan generator dokumentasi seperti Sphinx, atau FastAPI yang membaca docstring untuk menampilkan deskripsi endpoint, metadata yang hilang ini membuat API tampil kosong. Satu baris @wraps(func) mengembalikan semuanya.
reduce: Lipat Iterable Menjadi Satu Nilai
reduce melipat sebuah iterable menjadi satu nilai dengan menerapkan fungsi dua argumen secara berulang. Banyak yang menganggapnya kuno, tapi untuk agregasi non-standar ia jauh lebih ekspresif daripada loop manual dengan variabel akumulator.
from functools import reduce
angka = [1, 2, 3, 4, 5]
total = reduce(lambda acc, n: acc + n, angka)
print(total) # 15
# reduce bukan cuma untuk penjumlahan
kata = ["halo", "dunia", "python"]
gabung = reduce(lambda acc, w: f"{acc} {w}", kata)
print(gabung) # halo dunia python
# dengan nilai awal
dari_seratus = reduce(lambda acc, n: acc + n, angka, 100)
print(dari_seratus) # 115
Untuk penjumlahan, sum(angka) tetap pilihan yang lebih jelas dan cepat. Tapi begitu operasinya tidak standar — menggabungkan kata, mencari nilai ekstrem dengan aturan sendiri, atau membangun struktur data bertahap — reduce menyelamatkanmu dari boilerplate for.
cached_property dan Sisanya
cached_property mirip @property, tapi hasilnya disimpan setelah perhitungan pertama. Cocok untuk atribut turunan yang mahal dihitung dan jarang berubah, seperti agregasi data di dalam sebuah objek.
from functools import cached_property
class Laporan:
def __init__(self, data: list[float]):
self.data = data
@cached_property
def rata_rata(self) -> float:
print("menghitung rata-rata...")
return sum(self.data) / len(self.data)
laporan = Laporan([10, 20, 30, 40])
print(laporan.rata_rata) # menghitung rata-rata... 25.0
print(laporan.rata_rata) # 25.0 (tanpa print, hasil di-cache)
Perbedaan utama dengan lru_cache: hasilnya menempel pada instance, bukan pada fungsi. Setiap objek Laporan punya cache sendiri, dan nilai bisa dihapus dengan del laporan.rata_rata jika perlu dihitung ulang. Masih ada total_ordering, yang otomatis melengkapi operator perbandingan saat kamu hanya mendefinisikan __eq__ dan satu operator lain — topik magic method ini saya bahas lebih dalam di Belajar OOP di Python. Ada juga singledispatch, yang memungkinkan overloading fungsi berdasarkan tipe argumen pertamanya tanpa menulis deretan if isinstance.
functools tidak mengubah cara kamu menulis Python secara drastis, tapi ia menghilangkan boilerplate dan mempercepat fungsi yang mahal dengan biaya nyaris nol. Kalau kamu penasaran seberapa besar pengaruh caching pada fungsi-fungsi di proyekmu sendiri, ukur dulu dengan profiler — panduannya sudah saya tulis di Mengoptimalkan Performa Python: Profiling dengan cProfile. Mulailah dari satu alat hari ini: pasang @lru_cache pada satu fungsi murni yang lambat, lalu baca angka cache_info()-nya.