Decorator adalah fungsi yang membungkus fungsi
Kalimat itu terdengar abstrak, tapi cara kerjanya sederhana. Decorator menerima sebuah fungsi dan mengembalikan sebuah fungsi. Fungsi yang dikembalikannya biasanya memanggil fungsi aslinya, dengan perilaku tambahan yang membungkus pemanggilan itu.
Contoh paling singkat:
shout adalah decorator-nya. Ia menerima sebuah fungsi (greet), membuat fungsi baru (wrapper) yang memanggil fungsi aslinya dan mengubah hasilnya menjadi huruf besar, lalu mengembalikannya. Assignment ulang greet = shout(greet) menukar fungsi aslinya dengan versi yang sudah dibungkus.
Pola assignment ulang itu begitu umum sampai Python memberinya sintaks khusus.
@ adalah pemanis untuk assignment ulang
@name di baris tepat di atas sebuah def setara dengan name = name(...) tepat setelah fungsinya didefinisikan:
@shout dibaca sebagai "pasang decorator shout pada fungsi ini." Python menjalankan greet = shout(greet) langsung setelah def: mekanismenya sama seperti tadi, hanya lebih sedikit ketikan.
Setiap kali melihat @name, ganti saja dalam kepalamu dengan function = name(function). Hanya itu arti sintaksnya.
Menangani argumen
Kebanyakan fungsi menerima argumen. Decorator yang bisa dipakai harus meneruskannya. Idiomnya adalah *args, **kwargs, cara Python untuk menerima argumen apa pun, karena wrapper tidak perlu peduli apa yang diharapkan fungsi yang dibungkusnya:
*args menangkap semua argumen posisional. **kwargs menangkap semua argumen keyword. Wrapper meneruskan semuanya ke fungsi yang dibungkus tanpa diubah, lalu melakukan pekerjaan tambahan yang menjadi tujuan decorator itu, di sini mengubah hasilnya menjadi huruf besar.
Seperti inilah bentuk kebanyakan decorator sungguhan.
Contoh yang lebih berguna: mengukur waktu
Cetak berapa lama sebuah fungsi berjalan:
Polanya, yaitu menjalankan sesuatu sebelum pemanggilan dan sesuatu sesudahnya, adalah apa yang akhirnya dilakukan kebanyakan decorator. Logging, pengecekan autentikasi, retry, dan validasi input semuanya mengikuti bentuk yang sama.
Mempertahankan identitas asli: functools.wraps
Memasang decorator pada sebuah fungsi berarti menggantinya, sehingga fungsi yang dibungkus kehilangan atribut __name__ dan __doc__ aslinya:
Sekarang greet.__name__ bernilai "wrapper" dan docstring-nya hilang. Itu merusak help(), traceback, dan alat apa pun yang memeriksa fungsi tersebut.
Perbaikannya cuma satu baris: @functools.wraps(func) pada fungsi dalam menyalin metadata-nya.
Selalu tambahkan @wraps(func) pada fungsi dalam. Tidak ada ruginya, dan kamu terhindar dari sesi debugging yang mengejutkan di kemudian hari.
Decorator dengan argumen
Kadang decorator itu sendiri butuh konfigurasi, misalnya "coba ulang fungsi ini sampai 3 kali" atau "catat log di level DEBUG". Artinya ada satu lapisan lagi: fungsi luar yang menerima argumen lalu mengembalikan sebuah decorator.
Tiga lapisan terdengar banyak. Baca dari luar ke dalam:
repeat(times=3)adalah pemanggilan fungsi. Ia mengembalikandecorator.decoratoradalah decorator yang sebenarnya: ia menerima fungsi dan mengembalikan fungsi yang sudah dibungkus.wrapperadalah fungsi terbungkus yang berjalan saat dipanggil.
Bentuk ini yang menjalankan @retry(times=5), @cache(maxsize=100), dan decorator framework seperti @app.route("/users"). Begitu kamu mengenali pola tiga lapis ini, semua keluarga decorator itu bisa dibaca dengan cara yang sama.
Menumpuk decorator
Kamu bisa memasang lebih dari satu decorator pada sebuah fungsi. Decorator ditumpuk dari bawah ke atas: yang paling dekat dengan def berjalan lebih dulu:
add_exclaim membungkus lebih dulu dan menambahkan !. Lalu shout membungkus hasilnya dan mengubah semuanya menjadi huruf besar. Output-nya HI, ROSA!.
Urutan itu penting. Membalik tumpukannya memberimu HI, ROSA! dengan tanda seru yang ditambahkan setelah diubah ke huruf besar. Di sini hasilnya terlihat sama, tapi bayangkan decorator yang memformat JSON: menjalankannya sebelum atau sesudah decorator yang mencatat input bisa memberi hasil yang sangat berbeda.
Decorator bawaan yang akan kamu lihat
Python dan standard library-nya punya beberapa decorator yang akan kamu temui di kode sungguhan:
@propertymengubah method menjadi atribut yang dihitung.@staticmethodmenandai method yang tidak memakaiselfmaupuncls.@classmethodmenerima class sebagaicls, bukan instance, sangat cocok untuk constructor alternatif.@functools.lru_cachemenyimpan hasil (memoization), sehingga pemanggilan berulang dengan argumen yang sama langsung diambil dari cache.
Decorator framework (@app.route, @pytest.fixture, @dataclass) memakai mekanisme yang sama. Tidak ada yang istimewa: hanya fungsi yang membungkus fungsi.
Kapan menulisnya, kapan tidak
Tulis decorator kalau kamu ingin memasang perilaku yang sama pada banyak fungsi: mengukur waktu, logging, retry, pengecekan otorisasi. Intinya, perilaku itu tetap berada di luar badan fungsi.
Lewati decorator kalau:
- Perilakunya hanya milik satu fungsi tertentu. Taruh saja di fungsi itu.
- Kamu hanya membutuhkannya untuk testing. Fixture atau parameter lebih jelas.
- Kamu tergoda menumpuk empat atau lima decorator. Saat itu, alur kontrolnya tersembunyi di rantai decorator, dan pembaca harus membongkar setiap lapisan untuk melihat apa yang sebenarnya berjalan. Fungsi bantu yang lugas bisa lebih mudah dibaca.
Decorator adalah alat yang tajam. Kalau dipakai dengan baik, kode tetap DRY dan maksudnya jelas. Kalau dipakai dengan buruk, decorator menyembunyikan apa yang sedang dilakukan program. Saat memutuskan apakah akan memakainya, condonglah ke pilihan yang "jelas".
Berikutnya: type hint
Decorator adalah tempat yang umum untuk bertemu type hint di kode sungguhan, karena fungsi wrapper sering memberi anotasi pada signature-nya. Type hint adalah fitur kecil yang cepat terasa manfaatnya, dan itulah pembahasan berikutnya.
Pertanyaan yang sering diajukan
Apa itu decorator di Python?
Decorator adalah fungsi yang menerima fungsi lain lalu mengembalikan fungsi baru, biasanya fungsi yang membungkus fungsi aslinya dengan perilaku tambahan. Kamu memasangnya dengan @decorator_name di baris tepat di atas sebuah def. Sintaks @ adalah singkatan dari func = decorator_name(func).
Untuk apa decorator dipakai di Python?
Untuk menambahkan perilaku di sekitar sebuah fungsi tanpa mengubah badannya: logging, mengukur waktu, caching, pengecekan autentikasi, validasi input, dan retry. Framework banyak memakainya: @app.route(...) di Flask, @pytest.fixture di pytest, serta @property dan @staticmethod yang sudah bawaan.
Bisakah aku menulis decorator sendiri?
Bisa. Decorator hanyalah fungsi yang menerima fungsi dan mengembalikan fungsi. Kebanyakan decorator buatan sendiri membungkus pemanggilan aslinya dalam fungsi dalam kecil yang melakukan sesuatu sebelum, sesudah, atau di sekitarnya. Pakai functools.wraps pada fungsi dalam itu untuk mempertahankan nama dan docstring fungsi aslinya.