Decorator #
Di banyak aplikasi, ada perilaku yang perlu diterapkan ke banyak fungsi sekaligus: mencatat log setiap kali fungsi dipanggil, mengukur waktu eksekusi, memeriksa autentikasi pengguna sebelum menjalankan logika utama, atau meng-cache hasil komputasi. Tanpa decorator, kamu harus menyalin kode yang sama ke setiap fungsi — repetitif dan rawan inkonsistensi. Decorator memungkinkan kamu membungkus fungsi dengan perilaku tambahan tanpa menyentuh implementasi aslinya, menjaga kode tetap DRY dan terstruktur.
Fungsi sebagai First-Class Object #
Decorator bisa ada karena Python memperlakukan fungsi sebagai first-class object — fungsi bisa disimpan dalam variabel, dikirim sebagai argumen, dan dikembalikan dari fungsi lain. Ini fondasi yang perlu dipahami sebelum menulis decorator.
def sapa():
print("Halo!")
# Fungsi bisa disimpan dalam variabel
aksi = sapa
aksi() # Halo!
# Fungsi bisa dikirim sebagai argumen
def jalankan(fungsi):
fungsi()
jalankan(sapa) # Halo!
# Fungsi bisa dikembalikan dari fungsi lain
def buat_sapa(nama):
def sapa_nama():
print(f"Halo, {nama}!")
return sapa_nama # kembalikan fungsi, bukan hasil panggilannya
sapa_budi = buat_sapa("Budi")
sapa_budi() # Halo, Budi!
Decorator pada dasarnya adalah fungsi yang menerima fungsi sebagai input dan mengembalikan fungsi baru sebagai output.
Decorator Dasar #
Bentuk paling sederhana dari decorator: sebuah fungsi yang membungkus fungsi lain dengan perilaku tambahan.
def log_panggilan(func):
def wrapper(*args, **kwargs):
print(f"[LOG] Memanggil '{func.__name__}'")
hasil = func(*args, **kwargs)
print(f"[LOG] '{func.__name__}' selesai")
return hasil
return wrapper
@log_panggilan
def hitung_total(a, b):
return a + b
total = hitung_total(3, 7)
print(f"Total: {total}")
# [LOG] Memanggil 'hitung_total'
# [LOG] 'hitung_total' selesai
# Total: 10
Sintaks @log_panggilan di atas setara dengan menulis:
def hitung_total(a, b):
return a + b
hitung_total = log_panggilan(hitung_total) # ekuivalen dengan @log_panggilan
Masalah Identity Fungsi dan Solusinya: @wraps
#
Ada jebakan umum saat membuat decorator: fungsi asli kehilangan identitasnya.
# ANTI-PATTERN: decorator tanpa @wraps merusak metadata fungsi
def log_panggilan(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_panggilan
def hitung_total(a, b):
"""Menghitung penjumlahan dua angka."""
return a + b
print(hitung_total.__name__) # 'wrapper' — bukan 'hitung_total'!
print(hitung_total.__doc__) # None — docstring hilang!
# BENAR: gunakan @wraps untuk menjaga metadata fungsi asli
from functools import wraps
def log_panggilan(func):
@wraps(func) # salin metadata dari func ke wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_panggilan
def hitung_total(a, b):
"""Menghitung penjumlahan dua angka."""
return a + b
print(hitung_total.__name__) # 'hitung_total' ✓
print(hitung_total.__doc__) # 'Menghitung penjumlahan dua angka.' ✓
Selalu gunakan@functools.wraps(func)di dalam setiap decorator yang kamu buat. Tanpanya, tools seperti debugger, dokumentasi otomatis, danhelp()akan menampilkan informasi yang salah — bisa sangat membingungkan saat debugging.
Decorator Praktis #
Berikut beberapa decorator yang sering dibutuhkan dalam aplikasi nyata:
Timer — Mengukur Waktu Eksekusi #
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
mulai = time.perf_counter()
hasil = func(*args, **kwargs)
durasi = time.perf_counter() - mulai
print(f"[TIMER] '{func.__name__}' selesai dalam {durasi:.4f} detik")
return hasil
return wrapper
@timer
def proses_data(n):
return sum(range(n))
proses_data(10_000_000)
# [TIMER] 'proses_data' selesai dalam 0.3142 detik
Retry — Ulangi Saat Gagal #
import time
from functools import wraps
def retry(maks_coba=3, jeda=1.0):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for percobaan in range(1, maks_coba + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if percobaan == maks_coba:
raise
print(f"[RETRY] Percobaan {percobaan} gagal: {e}. Coba lagi dalam {jeda}s...")
time.sleep(jeda)
return wrapper
return decorator
@retry(maks_coba=3, jeda=0.5)
def ambil_data_api(url):
import random
if random.random() < 0.7: # simulasi 70% kemungkinan gagal
raise ConnectionError("Koneksi terputus")
return f"Data dari {url}"
try:
hasil = ambil_data_api("https://api.example.com/data")
print(hasil)
except ConnectionError:
print("Gagal setelah 3 percobaan.")
Decorator Berargumen #
Saat decorator perlu dikonfigurasi, kamu butuh satu lapisan fungsi tambahan — fungsi pembuat decorator.
from functools import wraps
def batasi_akses(peran_diizinkan):
"""Decorator yang membatasi akses berdasarkan peran pengguna."""
def decorator(func):
@wraps(func)
def wrapper(pengguna, *args, **kwargs):
if pengguna.get("peran") not in peran_diizinkan:
raise PermissionError(
f"Peran '{pengguna.get('peran')}' tidak diizinkan mengakses '{func.__name__}'"
)
return func(pengguna, *args, **kwargs)
return wrapper
return decorator
@batasi_akses(peran_diizinkan=["admin", "manajer"])
def hapus_pengguna(pengguna, target_id):
print(f"Pengguna {target_id} dihapus oleh {pengguna['nama']}.")
# Berhasil
hapus_pengguna({"nama": "Budi", "peran": "admin"}, target_id=42)
# Gagal
try:
hapus_pengguna({"nama": "Sari", "peran": "user"}, target_id=42)
except PermissionError as e:
print(e)
Struktur decorator berargumen selalu tiga lapis:
def decorator_berargumen(argumen): ← lapisan 1: terima konfigurasi
def decorator(func): ← lapisan 2: terima fungsi
@wraps(func)
def wrapper(*args, **kwargs): ← lapisan 3: jalankan logika
# sebelum
hasil = func(*args, **kwargs)
# sesudah
return hasil
return wrapper
return decorator
Stacking Decorator #
Beberapa decorator bisa ditumpuk pada satu fungsi. Urutan eksekusinya dari dalam ke luar — decorator paling dekat ke fungsi dieksekusi pertama.
from functools import wraps
def decorator_a(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("A: sebelum")
hasil = func(*args, **kwargs)
print("A: sesudah")
return hasil
return wrapper
def decorator_b(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("B: sebelum")
hasil = func(*args, **kwargs)
print("B: sesudah")
return hasil
return wrapper
@decorator_a
@decorator_b
def fungsi_ku():
print("Fungsi utama")
fungsi_ku()
# B: sebelum ← decorator_b lebih dekat ke fungsi, dieksekusi lebih dulu
# A: sebelum
# Fungsi utama
# A: sesudah
# B: sesudah
Urutan stacking @decorator_a @decorator_b def f():
setara dengan: decorator_a(decorator_b(f))
Urutan eksekusi:
decorator_a.wrapper mulai
decorator_b.wrapper mulai
f() dijalankan
decorator_b.wrapper selesai
decorator_a.wrapper selesai
Decorator untuk Kelas #
Decorator pada Metode #
from functools import wraps
def validasi_positif(func):
"""Pastikan semua argumen numerik bernilai positif."""
@wraps(func)
def wrapper(self, *args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(f"Argumen harus positif, dapat: {arg}")
return func(self, *args, **kwargs)
return wrapper
class Kalkulator:
@validasi_positif
def akar(self, n):
import math
return math.sqrt(n)
@validasi_positif
def log(self, n):
import math
return math.log(n)
kalk = Kalkulator()
print(kalk.akar(16)) # 4.0
print(kalk.log(100)) # 4.605...
try:
kalk.akar(-5)
except ValueError as e:
print(e) # Argumen harus positif, dapat: -5
Decorator Bawaan Python #
Python memiliki tiga decorator built-in yang sering digunakan dalam definisi kelas:
class Lingkaran:
PI = 3.14159
def __init__(self, jari_jari):
self._jari_jari = jari_jari
@property
def jari_jari(self):
"""Getter — akses seperti atribut biasa."""
return self._jari_jari
@jari_jari.setter
def jari_jari(self, nilai):
"""Setter — validasi sebelum menyimpan."""
if nilai <= 0:
raise ValueError("Jari-jari harus positif")
self._jari_jari = nilai
@property
def luas(self):
"""Computed property — tidak perlu setter."""
return self.PI * self._jari_jari ** 2
@classmethod
def dari_diameter(cls, diameter):
"""Factory method — buat instance dari parameter alternatif."""
return cls(diameter / 2)
@staticmethod
def adalah_valid(nilai):
"""Utility method — tidak butuh akses ke instance atau kelas."""
return nilai > 0
# Penggunaan
lingkaran = Lingkaran(5)
print(lingkaran.luas) # 78.53975
lingkaran.jari_jari = 10 # memanggil setter
print(lingkaran.luas) # 314.159
l2 = Lingkaran.dari_diameter(20) # classmethod
print(l2.jari_jari) # 10.0
print(Lingkaran.adalah_valid(-1)) # False
Ringkasan #
- Decorator adalah fungsi yang membungkus fungsi lain — menerima fungsi sebagai argumen dan mengembalikan fungsi baru dengan perilaku tambahan.
- Selalu gunakan
@functools.wraps(func)— tanpanya, metadata fungsi asli (__name__,__doc__) akan hilang dan menyulitkan debugging.- Gunakan
*args, **kwargsdi wrapper — agar decorator kompatibel dengan fungsi yang memiliki argumen apapun.- Decorator berargumen butuh tiga lapisan fungsi — lapisan terluar menerima konfigurasi, lapisan tengah menerima fungsi, lapisan dalam menjalankan logika.
- Stacking decorator dieksekusi dari dalam ke luar — decorator paling dekat ke fungsi dieksekusi pertama.
@propertyuntuk computed attribute dengan validasi di setter;@classmethoduntuk factory method;@staticmethoduntuk utility method yang tidak butuh akses keselfataucls.- Gunakan decorator untuk cross-cutting concerns — logging, timing, retry, autentikasi, caching — agar logika bisnis tetap bersih dari kode infrastruktur.