Pertama kali lihat kode Python yang pakai @decorator, saya bingung banget. Ada simbol apa ini di atas fungsi? Kenapa ada @staticmethod, @classmethod, @property yang selalu muncul di kode orang? Rasanya kayak ada rahasia besar yang cuma diketahui developer senior.
Ternyata, decorator itu konsep yang sederhana banget begitu kamu paham dasarnya. Intinya: decorator adalah fungsi yang membungkus fungsi lain. Kamu kasih fungsi "kekuatan baru" tanpa mengubah kode aslinya. Bayangkan kayak HP yang dikasih case - HP-nya tetap sama, tapi ada lapisan pelindung tambahan.
Di tutorial ini, saya akan bongkar Python decorator dari nol. Mulai dari konsep fungsi sebagai first-class object, bikin decorator pertama, sampai ke advanced pattern yang sering dipakai di production. Semua contoh bisa langsung kamu coba.
Decorator dipakai di mana-mana di ekosistem Python. Framework web seperti Flask dan FastAPI pakai decorator untuk routing:
@app.route('/api/users')
def get_users():
return jsonify(users)
Library testing pakai decorator untuk marking test:
@pytest.mark.parametrize("input,expected", [(1,2), (2,4)])
def test_double(input, expected):
assert input * 2 == expected
Bahkan caching pun pakai decorator:
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Kalau kamu nggak paham decorator, kamu nggak akan bisa baca kode library populer dengan benar. Dan yang lebih penting, kamu kehilangan salah satu tool paling powerful di Python untuk menulis kode yang bersih dan reusable.
Sebelum masuk ke decorator, kamu harus paham dulu satu konsep kunci: di Python, fungsi adalah first-class object. Artinya fungsi bisa disimpan di variabel, dikirim sebagai argumen, dan dikembalikan dari fungsi lain.
Coba lihat ini:
def sapa(nama):
return f"Halo, {nama}!"
# Simpan fungsi di variabel
fungsi_sapa = sapa
print(fungsi_sapa("Budi")) # Output: Halo, Budi!
# Fungsi sebagai argumen
def jalankan(fungsi, argumen):
return fungsi(argumen)
print(jalankan(sapa, "Andi")) # Output: Halo, Andi!
Yang terjadi di sini: fungsi_sapa bukan hasil dari sapa() (tanpa tanda kurung), tapi referensi ke fungsi itu sendiri. Ini mirip kayak pointer di C, tapi lebih mudah dipahami.
Python juga mendukung nested function - fungsi di dalam fungsi:
def buat_salam(prefix):
def salam(nama):
return f"{prefix}, {nama}!"
return salam
salam_pagi = buat_salam("Selamat pagi")
salam_malam = buat_salam("Selamat malam")
print(salam_pagi("Budi")) # Output: Selamat pagi, Budi!
print(salam_malam("Andi")) # Output: Selamat malam, Andi!
buat_salam mengembalikan fungsi salam yang "mengingat" nilai prefix bahkan setelah buat_salam selesai dijalankan. Ini disebut closure, dan ini adalah fondasi dari decorator.
Decorator pada dasarnya adalah fungsi yang menerima fungsi, mengembalikan fungsi baru yang biasanya menambahkan perilaku sebelum atau sesudah fungsi asli dijalankan.
def log_fungsi(func):
def wrapper(*args, **kwargs):
print(f"[LOG] Menjalankan {func.__name__}...")
hasil = func(*args, **kwargs)
print(f"[LOG] {func.__name__} selesai.")
return hasil
return wrapper
def tambah(a, b):
return a + b
# Cara manual (tanpa decorator syntax)
tambah_dengan_log = log_fungsi(tambah)
print(tambah_dengan_log(3, 5))
Outputnya:
[LOG] Menjalankan tambah...
[LOG] tambah selesai.
8
Sekarang pakai syntax @:
@log_fungsi
def tambah(a, b):
return a + b
print(tambah(3, 5)) # Hasil sama persis
@log_fungsi di atas def tambah itu sebenarnya sama persis dengan tambah = log_fungsi(tambah). Syntax @ cuma shortcut yang lebih cantik.
Kamu perhatikan wrapper pakai *args, **kwargs? Ini supaya decorator bisa dipakai di fungsi dengan argumen berapa pun:
@log_fungsi
def kali(a, b):
return a * b
@log_fungsi
def sapa_lengkap(nama, pesan="Selamat datang"):
return f"{pesan}, {nama}!"
print(kali(4, 6)) # Bekerja dengan 2 positional args
print(sapa_lengkap("Budi")) # Bekerja dengan default arg
print(sapa_lengkap("Andi", pesan="Hai")) # Bekerja dengan keyword arg
Tanpa *args, **kwargs, setiap decorator harus dibuat ulang untuk setiap signature fungsi. Dengan ini, satu decorator bisa dipakai di mana saja.
Kadang kamu butuh decorator yang bisa dikonfigurasi. Misalnya, decorator yang nge-limit berapa kali fungsi bisa dipanggil:
def batasi_panggilan(maks):
def decorator(func):
count = 0
def wrapper(*args, **kwargs):
nonlocal count
if count >= maks:
print(f"[WARNING] {func.__name__} sudah dipanggil {maks} kali!")
return None
count += 1
return func(*args, **kwargs)
return wrapper
return decorator
@batasi_panggilan(3)
def ambil_data():
print("Mengambil data dari API...")
return {"status": "ok"}
# Test
for i in range(5):
hasil = ambil_data()
Outputnya:
Mengambil data dari API...
Mengambil data dari API...
Mengambil data dari API...
[WARNING] ambil_data sudah dipanggil 3 kali!
[WARNING] ambil_data sudah dipanggil 3 kali!
Ini disebut decorator factory - fungsi yang menghasilkan decorator. Polanya: satu level ekstra untuk argumen. @batasi_panggilan(3) pertama memanggil batasi_panggilan(3) yang mengembalikan decorator, lalu decorator yang benar-benar membungkus ambil_data.
Sekarang contoh decorator yang benar-benar berguna di project nyata:
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
hasil = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[TIMER] {func.__name__} selesai dalam {elapsed:.4f} detik")
return hasil
return wrapper
@timer
def proses_data(n):
total = sum(i**2 for i in range(n))
return total
proses_data(1000000)
# Output: [TIMER] proses_data selesai dalam 0.0823 detik
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"[RETRY] Percobaan {attempt}/{max_attempts} gagal: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise Exception(f"{func.__name__} gagal setelah {max_attempts} percobaan")
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def ambil_data_api():
import random
if random.random() < 0.7:
raise ConnectionError("Server timeout")
return {"data": "berhasil"}
from functools import wraps
def cache(func):
_cache = {}
@wraps(func)
def wrapper(*args):
if args not in _cache:
_cache[args] = func(*args)
print(f"[CACHE MISS] {func.__name__}{args} dihitung ulang")
else:
print(f"[CACHE HIT] {func.__name__}{args} dari cache")
return _cache[args]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 55
from functools import wraps
def require_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
token = request.headers.get('Authorization')
if not token or not validasi_token(token):
return {"error": "Unauthorized", "status": 401}
return func(request, *args, **kwargs)
return wrapper
@require_auth
def get_profile(request):
user = get_user_from_token(request.headers['Authorization'])
return {"nama": user.nama, "email": user.email}
Kamu perhatikan saya pakai @wraps(func) di dalam decorator? Tanpa ini, metadata fungsi asli hilang:
# Tanpa @wraps
def log_fungsi(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_fungsi
def tambah(a, b):
"""Menjumlahkan dua angka."""
return a + b
print(tambah.__name__) # wrapper (bukan tambah!)
print(tambah.__doc__) # None (docstring hilang!)
# Dengan @wraps
from functools import wraps
def log_fungsi_benar(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_fungsi_benar
def kali(a, b):
"""Mengalikan dua angka."""
return a * b
print(kali.__name__) # kali
print(kali.__doc__) # Mengalikan dua angka.
@wraps menyalin __name__, __doc__, __module__, dan atribut lain dari fungsi asli ke wrapper. Tanpa ini, debugging jadi susah karena stack trace menunjukkan wrapper alih-alih nama fungsi asli.
Python mendukung multiple decorator pada satu fungsi. Eksekusinya dari bawah ke atas:
@timer
@log_fungsi
def hitung_luas(panjang, lebar):
return panjang * lebar
# Sama dengan: hitung_luas = timer(log_fungsi(hitung_luas))
# Eksekusi: timer -> log_fungsi -> hitung_luas
Tips: urutkan decorator dari yang paling "luar" ke yang paling "dalam". Kalau ada @timer dan @retry, taruh @retry di luar supaya waktu retry juga terukur:
@retry(max_attempts=3)
@timer
def ambil_data_berat():
# ...
pass
Selain fungsi, decorator juga bisa dibuat pakai class. Ini berguna kalau kamu butuh state yang lebih kompleks:
from functools import wraps
class RateLimiter:
def __init__(self, max_calls, period):
self.max_calls = max_calls
self.period = period
self.calls = []
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
self.calls = [t for t in self.calls if now - t < self.period]
if len(self.calls) >= self.max_calls:
wait = self.period - (now - self.calls[0])
print(f"[RATE LIMIT] Tunggu {wait:.1f} detik")
time.sleep(wait)
self.calls.append(time.time())
return func(*args, **kwargs)
return wrapper
@RateLimiter(max_calls=3, period=10)
def fetch_api(endpoint):
print(f"Fetching {endpoint}...")
return {"data": "ok"}
# 3 panggilan pertama langsung, panggilan ke-4 akan menunggu
Method __call__ membuat instance class bisa dipanggil seperti fungsi, sehingga bisa dipakai sebagai decorator.
Python decorator itu pada dasarnya cuma fungsi yang membungkus fungsi lain. Begitu kamu paham konsep closure dan first-class function, decorator jadi sangat intuitif. Mulai dari logging, caching, authentication, rate limiting - semuanya bisa dibikin lebih elegan dengan decorator.
Hal yang perlu diingat: selalu pakai @wraps dari functools supaya metadata fungsi nggak hilang, dan fahami perbedaan antara decorator tanpa argumen (@deco) dan decorator factory dengan argumen (@deco(args)).
Sekarang coba bikin decorator sendiri di project kamu. Misalnya decorator untuk validasi input, decorator untuk logging ke file, atau decorator untuk rate limit API call. Percaya deh, begitu kamu mulai pakai decorator, kamu akan menemukan banyak use case yang sebelumnya bikin kode kamu berantakan.
Punya pertanyaan atau mau sharing decorator yang kamu bikin? Tulis di komentar ya!