Dasar

Decorator di Python: apa itu, cara kerjanya, dan cara menulis sendiri

Diperbarui: · Skillaria.Top

Decorator di Python adalah fungsi yang menerima fungsi lain dan mengembalikan fungsi baru, biasanya pembungkus yang menambahkan perilaku sebelum atau sesudah pemanggilan: pencatatan log, pengukuran waktu, cache, pemeriksaan hak akses. Penulisan @decorator di atas def adalah singkatan dari func = decorator(func); agar pembungkus tetap memiliki nama dan docstring fungsi asli, dipakai functools.wraps.

Decorator di Python adalah fungsi yang menerima fungsi lain dan mengembalikan fungsi baru, biasanya pembungkus yang menambahkan sesuatu di sekitar pemanggilan aslinya. Dengan cara ini perilaku tambahan — pencatatan log, pengukuran waktu, cache, percobaan ulang — bisa ditambahkan tanpa mengubah kode fungsinya. Decorator ditulis dengan awalan @nama di atas deklarasi fungsi, dan muncul di mana-mana: di Flask dan FastAPI, di pytest, dan di pustaka standar.

Apa itu decorator dengan bahasa sederhana

Bayangkan sebuah hadiah dengan kertas kado. Hadiahnya (fungsi) tetap sama, tetapi di luarnya ada lapisan yang bekerja lebih dulu. Decorator adalah bungkus seperti itu: saat dipanggil, kode pembungkus dijalankan dulu, lalu ia memanggil fungsi asli dan bisa mengolah hasilnya.

Cara ini bekerja karena di Python fungsi adalah objek biasa. Fungsi bisa dikirim ke fungsi lain, dikembalikan dari fungsi, dan dideklarasikan di dalam fungsi. Ketiga sifat inilah yang menjadi dasar decorator.

Cara kerja decorator: @ adalah gula sintaksis

Ambil decorator paling sederhana yang mencetak pesan sebelum dan sesudah pemanggilan:

def log_calls(func):
    def wrapper():
        print(f"Memanggil {func.__name__}")
        result = func()
        print(f"{func.__name__} selesai")
        return result
    return wrapper

@log_calls
def hello():
    print("Halo!")

hello()
Memanggil hello
Halo!
hello selesai

Penulisan @log_calls di atas def sama persis dengan baris yang ditulis setelah deklarasi:

def hello():
    print("Halo!")

hello = log_calls(hello)

Nama hello sekarang menunjuk ke wrapper, sedangkan fungsi asli hidup di dalam closure — wrapper mengingat variabel func dari fungsi luar. Yang penting: decorator diterapkan sekali, saat def dieksekusi, yaitu ketika modul diimpor; pembungkus dijalankan setiap kali fungsi dipanggil.

Closure dan decorator

Closure adalah fungsi bersarang yang mengingat variabel fungsi luarnya setelah fungsi luar itu selesai. Tanpa closure, decorator tidak akan ada: lewat mekanisme inilah wrapper bisa mengakses func. Di dalam closure, kita juga bisa menyimpan keadaan, misalnya penghitung pemanggilan:

def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: panggilan ke-{calls}")
        return func(*args, **kwargs)
    return wrapper

Cara menulis decorator di Python: pola umum

Contoh pertama punya dua kekurangan: pembungkusnya tidak menerima argumen, dan nama fungsi asli tertimpa. Pola kerja yang sebaiknya dijadikan titik awal untuk setiap decorator adalah:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # kode sebelum pemanggilan
        result = func(*args, **kwargs)
        # kode sesudah pemanggilan
        return result
    return wrapper

Tiga aturan pola ini:

  1. *args, **kwargs — pembungkus menerima argumen apa pun dan meneruskannya tanpa perubahan.
  2. return result — jangan lupa mengembalikan hasil, jika tidak fungsi yang didekorasi selalu mengembalikan None.
  3. @functools.wraps(func) — memindahkan metadata fungsi asli ke pembungkus.

Contoh decorator untuk mengukur waktu eksekusi:

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} memakan waktu {elapsed:.3f} dtk")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # slow_sum memakan waktu 0.142 dtk

Untuk apa functools.wraps

Tanpa wraps, fungsi yang didekorasi kehilangan nama, docstring, dan atribut lainnya — yang terlihat dari luar adalah wrapper:

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Menjumlahkan dua angka."""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None

Dengan @functools.wraps(func), pembungkus mendapat __name__, __doc__, __module__, __qualname__, dan __annotations__ dari fungsi asli, serta atribut __wrapped__ yang menyimpan referensi ke fungsi asli. Ini penting untuk debugging dan traceback, untuk help(), untuk framework yang mendaftarkan fungsi berdasarkan nama, dan untuk inspect.signature yang menampilkan signature yang sebenarnya.

Decorator dengan parameter (dengan argumen)

Kadang decorator perlu diatur, misalnya @retry(times=3) atau @app.route("/"). Untuk itu ditambahkan satu tingkat bersarang lagi. Fungsi luar menerima parameter dan mengembalikan decorator sungguhan — konstruksi ini disebut pabrik decorator.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — tiga baris

Urutan pemanggilannya: repeat(times=3) mengembalikan decorator, lalu itu diterapkan ke ping dan menghasilkan wrapper. Dengan kata lain, @repeat(times=3) sama dengan ping = repeat(times=3)(ping).

Contoh praktis — mengulang saat terjadi error:

import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...

Beberapa decorator pada satu fungsi

Decorator bisa ditumpuk. Penerapannya dari bawah ke atas: yang paling dekat dengan def diterapkan lebih dulu.

@timer
@log_calls
def report():
    ...

# sama dengan:
report = timer(log_calls(report))

Saat dipanggil, lapisan terluar (timer) berjalan lebih dulu, sehingga ia juga mengukur waktu kerja log_calls. Urutan penting, misalnya jika satu decorator memeriksa hak akses dan yang lain menyimpan hasil ke cache.

Decorator standar dan bawaan Python

Banyak decorator berguna sudah tersedia di bahasa dan pustaka standarnya:

DecoratorFungsi
@propertymengubah metode menjadi atribut baca saja; dengan @x.setter menjadi atribut yang terkontrol
@staticmethodmetode kelas tanpa self, berupa fungsi biasa dalam namespace kelas
@classmethodmetode menerima kelas cls alih-alih instance — dipakai untuk konstruktor alternatif
@functools.cache, @functools.lru_cachemenyimpan hasil berdasarkan argumen
@functools.wrapsmenyalin metadata fungsi ke pembungkus
@dataclasses.dataclassmembuat __init__, __repr__, __eq__ untuk kelas
@contextlib.contextmanagermengubah generator menjadi context manager untuk with

Cache: functools.cache dan lru_cache

Decorator cache menyimpan hasil untuk setiap kombinasi argumen dan mengembalikannya tanpa menghitung ulang saat dipanggil lagi. Contoh klasiknya adalah bilangan Fibonacci rekursif, yang tanpa cache butuh waktu eksponensial:

import functools

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

print(fib(100))   # 354224848179261915075

functools.cache (ada sejak Python 3.9) menyimpan semua hasil tanpa batas. @functools.lru_cache(maxsize=128) hanya menyimpan sejumlah entri terakhir dan membuang yang lama — lebih aman untuk layanan yang berjalan lama. Statistiknya dilihat dengan fib.cache_info(), dan cache dikosongkan dengan fib.cache_clear(). Argumen fungsi seperti ini harus bisa di-hash: list sebagai argumen akan memunculkan TypeError: unhashable type: 'list'. Cache hanya masuk akal untuk fungsi murni — yang selalu mengembalikan hasil sama untuk argumen sama dan tidak punya efek samping.

Apa yang dilakukan decorator property

@property memungkinkan metode diakses seperti atribut, tanpa tanda kurung, dan bila perlu mengontrol penugasan nilai:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Saldo tidak boleh negatif")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError

staticmethod dan classmethod

class Money:
    def __init__(self, kopecks):
        self.kopecks = kopecks

    @classmethod
    def from_rupiah(cls, rupiah):
        return cls(round(rupiah * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_rupiah(12.5)
print(m.kopecks, Money.is_valid(-1))   # 1250 False

Justru @classmethod yang memungkinkan metode bekerja dengan kelasnya sendiri, bukan dengan instance-nya: from_rupiah akan membuat objek subkelas dengan benar.

Decorator kelas dan kelas sebagai decorator

Decorator juga bisa diterapkan ke kelas: ia menerima kelas dan mengembalikan kelas (yang sama atau sudah diubah). Begitulah cara kerja @dataclass. Contoh sederhana — registri kelas:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': <class '__main__.CsvExporter'>}

Sebaliknya, kelas juga bisa dipakai sebagai decorator. Syaratnya cukup memiliki metode __call__; keadaan sebaiknya disimpan di atribut instance:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2

Decorator asinkron

Untuk fungsi async def, pembungkusnya juga harus berupa korutin dan menunggu hasilnya dengan await. Jika tidak, yang dikembalikan adalah objek korutin yang belum dijalankan:

import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"Mulai {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"Selesai {func.__name__}")
        return result
    return wrapper

Untuk apa decorator dipakai di Python

  • Logika lintas-fungsi — logging, metrik, pengukuran waktu tanpa menyalin kode ke setiap fungsi.
  • Caching — @functools.cache untuk perhitungan murni yang mahal.
  • Pemeriksaan — hak akses, validasi argumen, autentikasi di handler web.
  • Pendaftaran — rute di Flask dan FastAPI (@app.get("/")), fixture pytest.
  • Keandalan — percobaan ulang, batas waktu, penanganan error.

Keuntungan utamanya adalah pemisahan tanggung jawab: fungsi mengerjakan tugasnya sendiri, dan perilaku pendukung dipasang dengan satu baris, serta dilepas dengan sama mudahnya.

Kesalahan umum

  • Pembungkus tidak mengembalikan hasil — fungsi setelah didekorasi mengembalikan None.
  • Tidak ada *args, **kwargs — TypeError saat dipanggil dengan argumen.
  • Lupa wraps — __name__ dan docstring salah, log membingungkan.
  • Tanda kurung pada pabrik — @repeat ditulis tanpa (3): fungsi itu malah masuk ke parameter times.
  • Kode berat di badan decorator — ia dijalankan saat modul diimpor, bukan saat fungsi dipanggil.

Ringkasan

Decorator di Python adalah fungsi tingkat tinggi: ia menerima fungsi, mengembalikan pembungkus, dan @ hanyalah penulisan singkat untuk penugasan. Mulailah dari pola dengan *args, **kwargs, return result, dan functools.wraps; untuk pengaturan tambahkan lapisan pabrik. Fungsi, closure, dan anotasi yang menjadi dasar decorator dipraktikkan dalam kursus Python.

Pertanyaan umum

Bisakah decorator dilepas dari sebuah fungsi?

Jika decorator memakai functools.wraps, fungsi asli bisa diakses lewat atribut __wrapped__: func.__wrapped__(...) memanggil fungsi tanpa pembungkus. Jika tidak, satu-satunya cara adalah menghapus baris dengan @ dari kode.

Kapan kode decorator dijalankan?

Badan decorator itu sendiri dijalankan sekali, saat fungsi dideklarasikan, biasanya ketika modul diimpor. Kode di dalam wrapper dijalankan setiap kali fungsi yang didekorasi dipanggil.

Bisakah decorator dipakai pada metode kelas?

Bisa. Decorator biasa dengan *args, **kwargs juga bekerja pada metode: self hanya akan menjadi elemen pertama dari args.

Apa bedanya decorator dengan pewarisan?

Pewarisan mengubah perilaku kelas lewat hierarki, sedangkan decorator membungkus fungsi atau kelas tertentu dari luar dan mudah dikombinasikan dengan decorator lain tanpa mengubah kode aslinya.

Pelajari topik ini lewat video

Topik ini dibahas mendalam dengan praktik di kursus “Python 3.15: от новичка до профи”.

Ke kursus