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.
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.
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.
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.
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.
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.
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.
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.
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.