Decorator functools.wraps sudah kita pakai saat membangun decorator pada Chapter functools.wraps — tetapi module functools berisi lebih dari sekadar itu. Module ini adalah kotak perkakas untuk mengelola function sebagai objek: menyimpan hasilnya, mengunci sebagian argumennya, dan memilih implementasi berdasarkan tipe data. Pada Chapter ini kita bedah enam alat yang paling sering muncul di kode produksi.

lru_cache: Memoization Satu Baris #

Function rekursif Fibonacci yang naif menghitung ulang nilai yang sama berkali-kali. Decorator @lru_cache menyimpan setiap hasil pemanggilan sehingga perhitungan sebelumnya tinggal dibaca ulang:

python
from functools import lru_cache


@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)


print(fib(35))
print(fib.cache_info())

Output yang Dihasilkan:

text
9227465
CacheInfo(hits=33, misses=36, maxsize=None, currsize=36)

Mari kita bedah kode di atas:

  1. Baris 4 (@lru_cache(maxsize=None)): membungkus fib dengan cache tanpa batas. Setiap hasil pemanggilan disimpan berdasarkan argumennya.
  2. Baris 11 (fib(35)): tanpa cache, panggilan ini mengeksekusi puluhan juta pemanggilan rekursif; dengan cache, hanya 36 nilai berbeda yang dihitung sekali.
  3. Baris 12 (fib.cache_info()): decorator menempelkan method ini ke function yang dibungkusnya. Nilai misses=36 berarti 36 nilai dihitung sungguhan, sedangkan hits=33 berarti 33 pemanggilan dijawab langsung dari cache.

Sejak Python 3.9 tersedia alias yang lebih ringkas: @functools.cache setara dengan @lru_cache(maxsize=None). Gunakan lru_cache(maxsize=128) saat data besar dan kita ingin cache membuang entri yang lama tidak dipakai.

partial dan reduce: Membekukan Argumen dan Melipat Data #

Function partial() membuat function baru dengan sebagian argumen yang sudah terisi — teknik yang disebut currying sebagian:

python
from functools import partial


def diskon(harga, persen):
    return harga - (harga * persen / 100)


diskon_member = partial(diskon, persen=20)

print(diskon_member(150000))
print(diskon(150000, persen=20))

Output yang Dihasilkan:

text
120000.0
120000.0

Variabel diskon_member adalah function diskon dengan argumen persen yang permanen bernilai 20; kita cukup menyuplai harga saat memanggilnya. Pola ini sangat berguna saat sebuah function butuh banyak argumen tetapi API yang memanggilnya hanya mengizinkan satu argumen — misalnya sorted(key=...) atau handler add_done_callback().

Sementara reduce() melipat sebuah iterable menjadi satu nilai dengan function penggabung, melanjutkan materi pada Chapter map, filter, dan reduce:

python
from functools import reduce

penjualan = [120, 80, 200, 150]
total = reduce(lambda a, b: a + b, penjualan)
print(total)

Output yang Dihasilkan:

text
550

Function lambda dijalankan berantai: ((120 + 80) + 200) + 150 = 550. Untuk kasus penjumlahan, sum() tetap lebih jelas; reduce() unggul saat operasi penggabungannya khusus, dan akan lebih rapi lagi dipasangkan dengan operator.mul seperti pada Chapter Module operator.

cached_property: Property yang Dihitung Sekali #

Decorator @cached_property menyimpan hasil perhitungan property ke dalam instance, sehingga perhitungan mahal hanya terjadi pada akses pertama:

python
from functools import cached_property


class Katalog:
    def __init__(self, daftar_harga):
        self._daftar_harga = daftar_harga

    @cached_property
    def harga_termahal(self):
        print("menghitung harga termahal...")
        return max(self._daftar_harga.values())


katalog = Katalog({"buku": 95000, "laptop": 15000000})
print(katalog.harga_termahal)
print(katalog.harga_termahal)

Output yang Dihasilkan:

text
menghitung harga termahal...
15000000
15000000

Kalimat "menghitung harga termahal..." hanya tercetak satu kali meskipun property diakses dua kali — akses kedua membaca hasil tersimpan di instance. Gunakan cached_property untuk turunan data yang mahal dan jarang berubah, bukan untuk nilai yang bisa berubah selama program berjalan.

singledispatch: Pemilihan Implementasi Berdasarkan Tipe #

Decorator @singledispatch memisahkan logika per tipe argumen pertama. Kita daftarkan satu function umum lalu implementasi khusus per tipe:

python
from functools import singledispatch


@singledispatch
def tampilkan(nilai):
    print(f"umum: {nilai!r}")


@tampilkan.register
def _(nilai: int):
    print(f"integer: {nilai}")


@tampilkan.register
def _(nilai: str):
    print(f"string: {nilai}")


tampilkan(42)
tampilkan("Budi")
tampilkan(3.14)

Output yang Dihasilkan:

text
integer: 42
string: Budi
umum: 3.14

Interpreter memanggil implementasi yang cocok dengan tipe argumen: 42 masuk ke versi int, "Budi" ke versi str, dan 3.14 yang tidak punya versi khusus jatuh ke function generik. Pola ini menggantikan rantai if isinstance(...) panjang dengan pendaftaran yang bisa disebar ke beberapa module.

Alat-alat functools berbagi satu tema besar: memindahkan pekerjaan yang berulang — menghitung ulang, mengulang argumen, mencocokkan tipe manual — keluar dari kode kita dan menyerahkannya ke interpreter Python.