Decorator adalah callable yang menerima sebuah fungsi dan mengembalikan fungsi pengganti, sehingga perilaku tambahan dapat diselipkan tanpa mengubah badan fungsi aslinya. Penulisan dengan tanda @ membuat pola ini ringkas dan seragam, dan pola ini dipakai luas pada pustaka standar maupun framework untuk pencatatan, pengukuran, pendaftaran, dan caching. Topik ini membahas pembungkusan manual, sintaks @, pemulihan metadata dengan functools.wraps, decorator yang menerima argumen, urutan penumpukan, beserta dua kesalahan yang lazim.
Membungkus fungsi dengan decorator#
Decorator tidak lebih dari fungsi biasa yang polanya berupa tiga langkah, yaitu menerima fungsi, mendefinisikan fungsi pembungkus (wrapper) yang memanggil fungsi aslinya, lalu mengembalikan pembungkus itu. Tanda @nama di atas sebuah def hanya gula sintaks untuk penugasan f = nama(f). Contoh berikut mendefinisikan decorator pencatat pemanggilan, lalu memasangnya pada dua fungsi.
def cetak_pemanggilan(fungsi):
def pembungkus(*args, **kwargs):
print("->", fungsi.__name__)
hasil = fungsi(*args, **kwargs)
print("<-", fungsi.__name__)
return hasil
return pembungkus
@cetak_pemanggilan
def jumlah(a, b):
return a + b
@cetak_pemanggilan
def sapa(nama):
return "Hai " + nama
print(jumlah(2, 3))
print(sapa("Dewi"))-> jumlah
<- jumlah
5
-> sapa
<- sapa
Hai DewiNama jumlah setelah dekorasi tidak lagi menunjuk fungsi aslinya, melainkan objek pembungkus yang meneruskan seluruh argumen melalui *args dan **kwargs sebelum mengembalikan hasilnya. Karena pembungkus bersifat umum, satu decorator dapat dipasang pada fungsi dengan tanda tangan apa pun. Kesetaraan dengan penugasan biasa juga dapat ditunjukkan tanpa tanda @ sama sekali. Fungsi apa pun yang menerima fungsi dan mengembalikan fungsi dapat dipakai sebagai decorator melalui penugasan manual.
def cetak_pemanggilan(fungsi):
def pembungkus(*args, **kwargs):
print("->", fungsi.__name__)
hasil = fungsi(*args, **kwargs)
print("<-", fungsi.__name__)
return hasil
return pembungkus
def ganda(n):
return n * 2
ganda = cetak_pemanggilan(ganda)
print(ganda(4))
print(type(ganda).__name__)-> ganda
<- ganda
8
functionKeluarannya sama dengan versi ber-@, sedangkan cetakan function menegaskan bahwa nama ganda kini menunjuk objek fungsi baru, yaitu pembungkusnya. Tanda @ dengan demikian tidak menambah kemampuan apa pun, melainkan memangkas satu baris penugasan dan menaruh decorator pada tempat yang paling dekat dengan definisinya. Diagram berikut menggambarkan perjalanan pemanggilan dari pemanggil sampai kembali ke pemanggil.
Mempertahankan metadata dengan functools.wraps#
Pembungkus adalah fungsi berbeda dari fungsi aslinya, sehingga metadata seperti __name__ dan __doc__ ikut tertukar. Kondisi ini menyulitkan alat introspeksi, mulai dari dokumentasi otomatis sampai debugger, karena semuanya membaca nama pembungkus.
def tanpa_wraps(fungsi):
def pembungkus(*args, **kwargs):
return fungsi(*args, **kwargs)
return pembungkus
@tanpa_wraps
def bagi(a, b):
"""Membagi dua angka."""
return a / b
print(bagi.__name__)pembungkusModul functools menyediakan decorator wraps untuk menyalin metadata fungsi asli ke pembungkusnya. Decorator ini dipasang pada definisi pembungkus dengan fungsi asli sebagai argumennya, dan hanya itu perbedaannya dari versi tanpa wraps.
from functools import wraps
def cetak_pemanggilan(fungsi):
@wraps(fungsi)
def pembungkus(*args, **kwargs):
return fungsi(*args, **kwargs)
return pembungkus
@cetak_pemanggilan
def bagi(a, b):
"""Membagi dua angka."""
return a / b
print(bagi.__name__)
print(bagi.__doc__)
print(bagi(10, 2))bagi
Membagi dua angka.
5.0Nama dan docstring bagi kembali seperti aslinya, padahal objek yang dipanggil tetap pembungkus. Selain __name__ dan __doc__, wraps juga memulihkan atribut lain seperti __module__ dan __wrapped__, sehingga fungsi terdekorasi tetap dapat diperiksa sebagaimana fungsi biasa.
Decorator dengan argumen#
Decorator yang menerima argumen sendiri membutuhkan satu lapis fungsi lagi, karena argumennya harus ditangkap lebih dahulu sebelum fungsi yang didekorasi diterima. Bentuknya berupa tiga tingkat, yaitu pabrik decorator, decorator, dan pembungkus.
from functools import wraps
def ulang(jumlah):
def dekorator(fungsi):
@wraps(fungsi)
def pembungkus(*args, **kwargs):
hasil = None
for _ in range(jumlah):
hasil = fungsi(*args, **kwargs)
return hasil
return pembungkus
return dekorator
@ulang(3)
def sapa(nama):
print("Halo", nama)
return nama
sapa("Budi")Halo Budi
Halo Budi
Halo BudiEkspresi @ulang(3) dievaluasi lebih dahulu, sehingga Python memperoleh decorator sesungguhnya dari pemanggilan ulang(3) dan baru kemudian menerapkannya pada sapa. Nilai 3 tersimpan di lingkup ulang dan tetap terjangkau oleh pembungkus melalui aturan closure, sehingga tiga tingkat fungsi tersebut saling terhubung tanpa variabel global.
Urutan penumpukan decorator#
Beberapa decorator dapat dipasang pada satu fungsi, dan penerapannya bergerak dari bawah ke atas. Decorator yang paling dekat dengan def diterapkan lebih dahulu, sedangkan decorator di atasnya membungkus hasilnya.
from functools import wraps
def tandai(awalan):
def dekorator(fungsi):
@wraps(fungsi)
def pembungkus(*args, **kwargs):
return f"[{awalan}] " + str(fungsi(*args, **kwargs))
return pembungkus
return dekorator
@tandai("atas")
@tandai("bawah")
def data():
return "isi"
print(data())[atas] [bawah] isiKeluaran [atas] [bawah] isi memperlihatkan urutan itu secara langsung, karena tandai("bawah") menyentuh data lebih dahulu, lalu tandai("atas") membungkus hasilnya. Pada saat pemanggilan, kontrol mengalir dari pembungkus terluar ke dalam, sehingga urutan penulisan dan urutan eksekusi saling berkebalikan. Pemahaman ini penting ketika decorator menyimpan state atau mengubah urutan argumen.
Kesalahan umum#
Kesalahan pertama adalah lupa mengembalikan hasil fungsi asli dari pembungkus. Pembungkus yang hanya memanggil fungsi tanpa return mengembalikan None, dan pemanggil menerima None tanpa pesan error apa pun.
def catat(fungsi):
def pembungkus(*args, **kwargs):
fungsi(*args, **kwargs)
return pembungkus
@catat
def jumlah(a, b):
return a + b
print(jumlah(2, 3))NonePerbaikannya adalah menugaskan hasil pemanggilan ke variabel lalu mengembalikannya, sebagaimana pola hasil = fungsi(*args, **kwargs) yang dipakai sepanjang topik ini.
Kesalahan kedua adalah mendefinisikan pembungkus tanpa parameter variadik seperti *args, **kwargs, sehingga fungsi yang memiliki argumen gagal dipanggil melalui pembungkusnya.
def catat(fungsi):
def pembungkus():
print("memanggil", fungsi.__name__)
return fungsi()
return pembungkus
@catat
def jumlah(a, b):
return a + b
try:
jumlah(2, 3)
except TypeError as e:
print(type(e).__name__, str(e))TypeError catat.<locals>.pembungkus() takes 0 positional arguments but 2 were givenPesan errornya menunjuk pembungkus, bukan jumlah, karena pemanggilan benar-benar masuk ke pembungkus lebih dahulu. Penambahan *args, **kwargs pada definisi dan pemanggilan pembungkus menjadikan decorator ini aman dipasang pada fungsi apa pun.
Ringkasan#
- Decorator menerima fungsi dan mengembalikan penggantinya, dengan tanda
@sebagai singkatan penugasan ulang nama fungsi. - Pembungkus meneruskan argumen melalui
*argsdan**kwargs, mengembalikan hasil fungsi asli, dan sebaiknya dilengkapi@functools.wrapsagar metadata fungsi asli pulih. - Decorator dengan argumen berbentuk tiga tingkat fungsi, dan dekorasi berlaku sekali pada saat
defdieksekusi. - Penumpukan decorator diterapkan dari bawah ke atas, sedangkan alur eksekusinya bergerak dari pembungkus terluar ke dalam.
Referensi#
- Decorators (Glosarium) — Python Software Foundation — Definisi resmi decorator sebagai fungsi yang mengembalikan fungsi lain.
- functools.wraps — Python Software Foundation — Referensi pembantu wraps untuk memulihkan metadata fungsi terdekorasi.
- PEP 318 — Decorators for Functions and Methods — Python Software Foundation — Usulan asal sintaks decorator pada Python beserta contoh motivasinya.