Decorator adalah salah satu fitur Python yang paling elegan, sekaligus paling sering membingungkan pemula. Kamu pasti pernah melihat simbol @ di atas definisi fungsi dan bertanya-tanya apa fungsinya. Kabar baiknya: decorator sebenarnya bukan sihir — ia hanyalah sebuah fungsi yang membungkus fungsi lain untuk menambahkan perilaku tanpa mengubah kode aslinya. Artikel ini akan membimbingmu memahami cara kerja decorator dari nol hingga mampu menulis decorator sendiri untuk kebutuhan nyata seperti mengukur waktu eksekusi, caching, dan retry otomatis.

Apa Itu Decorator?

Secara formal, decorator adalah fungsi yang menerima fungsi lain sebagai argumen, lalu mengembalikan fungsi baru (biasanya dengan perilaku tambahan). Dengan decorator, kamu bisa menambahkan fungsionalitas ke banyak fungsi sekaligus — misalnya logging, validasi, atau pengukuran performa — tanpa menulis ulang kode yang sama berulang kali. Ini sejalan dengan prinsip DRY (Don't Repeat Yourself) dan separation of concerns.

Prasyarat: Fungsi Adalah First-Class Object

Di Python, fungsi adalah objek biasa. Artinya, kamu bisa menyimpannya ke variabel, mengirimkannya sebagai argumen ke fungsi lain, dan mengembalikannya dari fungsi lain. Inilah fondasi yang membuat decorator bisa bekerja.

python
def sapa(nama):
    return f"Halo, {nama}!"

# 1. Fungsi bisa disimpan ke dalam variabel
ucapan = sapa
print(ucapan("Budi"))   # Halo, Budi!

# 2. Fungsi bisa menjadi argumen fungsi lain
def jalankan(func, arg):
    return func(arg)

print(jalankan(sapa, "Siti"))   # Halo, Siti!

# 3. Fungsi bisa dikembalikan dari fungsi lain
def pembuat_pengali(n):
    def pengali(x):
        return x * n
    return pengali

kali_3 = pembuat_pengali(3)
print(kali_3(10))   # 30

Membuat Decorator Pertamamu

Sekarang kita tulis decorator sederhana bernama logger yang mencetak pesan sebelum dan sesudah fungsi target dijalankan. Perhatikan pola dasarnya: decorator menerima func, lalu mendefinisikan dan mengembalikan wrapper yang memanggil func.

python
def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Memanggil {func.__name__}...")
        hasil = func(*args, **kwargs)
        print(f"Selesai {func.__name__}.")
        return hasil
    return wrapper

@logger
def tambah(a, b):
    return a + b

print(tambah(3, 4))
# Memanggil tambah...
# Selesai tambah.
# 7

Selalu gunakan functools.wraps di dalam wrapper! Tanpa @wraps, wrapper akan kehilangan metadata fungsi asli seperti __name__ dan __doc__. Ini bisa merusak dokumentasi otomatis, introspection, dan alat debugging.

python
import functools

def logger(func):
    @functools.wraps(func)   # mempertahankan metadata fungsi asli
    def wrapper(*args, **kwargs):
        print(f"Memanggil {func.__name__}...")
        hasil = func(*args, **kwargs)
        print(f"Selesai {func.__name__}.")
        return hasil
    return wrapper

@logger
def tambah(a, b):
    """Menjumlahkan dua angka."""
    return a + b

print(tambah.__name__)   # tambah (bukan 'wrapper')
print(tambah.__doc__)    # Menjumlahkan dua angka.

Decorator dengan Argumen

Bagaimana jika decorator-mu butuh parameter, seperti @ulangi(3)? Kamu perlu tiga lapis fungsi: lapisan terluar menerima argumen decorator, lapisan tengah menerima fungsi target, dan lapisan terdalam adalah wrapper-nya. Lapisan terluar ini biasa disebut decorator factory.

python
import functools

def ulangi(kali):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(kali):
                hasil = func(*args, **kwargs)
            return hasil
        return wrapper
    return decorator

@ulangi(3)
def sapa():
    print("Halo!")

sapa()   # Mencetak "Halo!" sebanyak tiga kali

Contoh Nyata: Timer Decorator

Salah satu kegunaan decorator paling umum adalah mengukur waktu eksekusi sebuah fungsi. Dengan time.perf_counter() kita bisa mendapatkan pengukuran yang presisi.

python
import time
import functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        mulai = time.perf_counter()
        hasil = func(*args, **kwargs)
        selesai = time.perf_counter()
        print(f"{func.__name__} selesai dalam {selesai - mulai:.4f} detik")
        return hasil
    return wrapper

@timer
def hitung_lambat():
    total = 0
    for i in range(10_000_000):
        total += i
    return total

hitung_lambat()
# hitung_lambat selesai dalam 0.2911 detik

Contoh Nyata: Caching (Memoization)

Decorator juga bisa menyimpan hasil pemanggilan fungsi sehingga komputasi mahal tidak dihitung ulang. Teknik ini disebut memoization. Contoh klasiknya adalah deret Fibonacci.

python
import functools

def cache(func):
    penyimpanan = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in penyimpanan:
            penyimpanan[args] = func(*args)
        return penyimpanan[args]
    return wrapper

@cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))   # 12586269025 — langsung, tanpa hang

Caching hanya aman untuk pure function, yaitu fungsi yang selalu mengembalikan hasil sama untuk input yang sama dan tidak punya efek samping. Jangan gunakan decorator cache pada fungsi yang membaca data yang bisa berubah (misalnya dari database atau API), karena hasilnya bisa kedaluwarsa.

Contoh Nyata: Retry Decorator

Saat memanggil layanan eksternal seperti API, kegagalan sementara (timeout, network hiccup) sangat umum. Decorator retry bisa otomatis mengulang fungsi yang gagal setelah jeda tertentu.

python
import functools
import time

def retry(kali=3, jeda=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for percobaan in range(1, kali + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Percobaan {percobaan} gagal: {e}")
                    if percobaan == kali:
                        raise
                    time.sleep(jeda)
        return wrapper
    return decorator

@retry(kali=3, jeda=0.5)
def panggil_api():
    raise ConnectionError("Jaringan bermasalah")

panggil_api()
# Percobaan 1 gagal: Jaringan bermasalah
# Percobaan 2 gagal: Jaringan bermasalah
# Percobaan 3 gagal: Jaringan bermasalah
# ConnectionError: Jaringan bermasalah

Class-Based Decorator

Jika decorator-mu perlu menyimpan state yang kompleks, kamu bisa mengimplementasikannya sebagai class dengan method __call__. Contoh berikut menghitung berapa kali sebuah fungsi dipanggil.

python
import functools

class HitungPanggilan:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.hitung = 0

    def __call__(self, *args, **kwargs):
        self.hitung += 1
        print(f"{self.func.__name__} dipanggil {self.hitung} kali")
        return self.func(*args, **kwargs)

@HitungPanggilan
def halo():
    print("Halo!")

halo()
halo()
# halo dipanggil 1 kali
# Halo!
# halo dipanggil 2 kali
# Halo!

Kesimpulan

Decorator adalah alat yang sangat ampuh untuk menambahkan perilaku lintas-fungsi secara bersih dan reusable. Begitu kamu memahami polanya — fungsi yang membungkus fungsi — kamu akan mulai melihat decorator di mana-mana. Framework seperti FastAPI menggunakannya untuk mendefinisikan route (@app.get(...)), dan pytest memakainya untuk fixture (@pytest.fixture) serta penanda test. Cobalah tulis decorator kecil untuk kebutuhanmu sendiri, lalu kembangkan dengan argumen dan state — kamu akan kaget betapa banyak kode boilerplate yang bisa kamu hilangkan.