Decorator @property mengubah metode menjadi atribut yang dibaca tanpa tanda kurung. Dengan property, sebuah nilai dapat dihitung dari atribut lain atau divalidasi sebelum disimpan, tanpa memaksa pemanggil mengubah cara aksesnya. Topik ini membahas atribut hasil komputasi, validasi melalui setter, serta pemindahan dari atribut biasa ke property tanpa merusak kode pemanggilnya.
Atribut hasil komputasi#
Sebuah metode yang diberi decorator @property dapat dipanggil seperti atribut, yaitu tanpa tanda kurung. Bentuk ini tepat untuk nilai yang diturunkan dari atribut lain, seperti luas dari sisi atau konversi satuan, karena pembaca kode tidak perlu menebak apakah sebuah nama berupa data atau hasil hitungan. Property menghitung ulang nilainya setiap kali dibaca, sehingga hasilnya selalu mengikuti nilai atribut terbaru.
class Persegi:
def __init__(self, sisi):
self.sisi = sisi
@property
def luas(self):
return self.sisi ** 2
p = Persegi(5)
print(p.luas)
p.sisi = 10
print(p.luas)25
100Ekspresi p.luas tidak memakai tanda kurung, padahal luas berupa metode yang nilainya dihitung setiap kali dibaca. Setelah sisi diubah menjadi 10, pembacaan berikutnya langsung menghasilkan 100 tanpa pembaruan manual. Apabila luas ditulis sebagai metode biasa, pemanggilnya harus menulis p.luas(), dan nilai turunan terasa seperti perilaku alih-alih seperti data.
Objek property milik kelasnya, bukan milik instansnya, sehingga definisi @property cukup satu kali untuk seluruh instans. Inilah sebabnya luas turunan tidak perlu disimpan, karena setiap instans menghitungnya dari sisi miliknya sendiri. Property juga tidak memerlukan penambahan atribut penyimpanan apa pun apabila hasilnya dapat diturunkan sepenuhnya dari atribut yang ada. Sebaliknya, atribut biasa tetap menjadi pilihan yang benar selama nilainya tidak memerlukan komputasi maupun pemeriksaan, karena property menambah satu lapis panggilan untuk setiap pembacaan.
Validasi melalui setter#
Property juga dapat mendefinisikan setter, yaitu metode yang dijalankan setiap kali atribut ditugaskan. Setter menangkap nilai baru, memeriksa isinya, lalu menyimpan nilai tersebut pada atribut internal, atau menimbulkan error apabila nilai baru itu tidak sah. Penugasan di dalam __init__ juga melewati setter, sehingga validasi berlaku sejak instans dibuat.
class Suhu:
def __init__(self, celsius=0):
self.celsius = celsius # melewati setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, nilai):
if nilai < -273.15:
raise ValueError("di bawah nol absolut")
self._celsius = nilai
s = Suhu(25)
print(s.celsius)
try:
s.celsius = -300
except ValueError as e:
print(type(e).__name__, str(e))25
ValueError di bawah nol absolutGetter mengembalikan isi atribut internal _celsius, sedangkan setter memeriksa nilai sebelum menyimpannya. Penugasan s.celsius = -300 memicu ValueError yang menyebut penyebabnya, sehingga objek tidak pernah menampung suhu yang tidak mungkin. Pola atribut internal berawalan garis bawah dipakai agar nama publik dan nama penyimpanannya tidak bertabrakan. Property sebenarnya masih menyediakan deleter melalui @nama.deleter, tetapi bentuk ini jarang dibutuhkan dan pemakaian getter serta setter saja sudah mencakup sebagian besar kebutuhan.
Setter juga tempat yang tepat untuk menormalkan nilai, misalnya membulatkan masukan atau mengonversi satuan sebelum disimpan, sehingga atribut internal selalu berada dalam bentuk baku. Konsekuensinya, getter menjadi sesederhana mungkin karena tidak perlu menebak bentuk data yang tersimpan. Semua jalur penugasan, baik dari __init__, dari metode lain, maupun dari luar kelas, melewati satu pintu validasi yang sama.
Mengubah atribut biasa menjadi property#
Salah satu alasan property disukai adalah antarmukanya yang stabil. Kode pemanggil menulis objek.nama baik untuk atribut biasa maupun untuk property, sehingga atribut sederhana dapat diubah menjadi property dengan validasi tanpa menyentuh satu baris pemanggil pun. Berkas berikut menampilkan kelas Tandon yang isinya divalidasi terhadap kapasitasnya.
class Tandon:
def __init__(self, kapasitas):
self.kapasitas = kapasitas
self.isi = 0
@property
def isi(self):
return self._isi
@isi.setter
def isi(self, nilai):
if nilai > self.kapasitas:
raise ValueError("isi melebihi kapasitas")
self._isi = nilai
t = Tandon(100)
t.isi = 60
print(t.isi)
try:
t.isi = 150
except ValueError as e:
print(type(e).__name__, str(e))60
ValueError isi melebihi kapasitasSimpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.
# menjalankan program kelas Tandon
python3 tandon.py60
ValueError isi melebihi kapasitasPerhatikan urutan penugasan di dalam __init__. Atribut kapasitas harus diisi terlebih dahulu, karena penugasan self.isi = 0 pada baris berikutnya langsung melewati setter yang membaca self.kapasitas. Penugasan t.isi = 60 lolos karena nilainya sah, sedangkan 150 ditolak setter dengan pesan yang jelas. Bagi pemanggil, sintaksnya identik dengan atribut biasa, dan itulah yang menjaga kode tetap sederhana ketika aturan bertambah. Pola ini berkebalikan dengan getter dan setter eksplisit semacam get_isi() dan set_isi() yang lazim pada bahasa lain, karena bentuk itu memaksa seluruh pemanggil disunting sejak hari pertama.
Perlu diperhatikan pula bahwa getter dijalankan setiap kali dibaca. Apabila perhitungannya murah, seperti pengali sederhana, biayanya dapat diabaikan. Apabila perhitungannya mahal, hitung sekali lalu simpan, atau sediakan metode bernama eksplisit agar pemanggil menyadari biayanya.
Kesalahan umum#
Kesalahan pertama adalah membuat getter membaca property-nya sendiri, yang menyebabkan rekursi tanpa henti.
class Rekursif:
@property
def nilai(self):
return self.nilai
r = Rekursif()
try:
print(r.nilai)
except RecursionError:
print("RecursionError")RecursionErrorEkspresi return self.nilai di dalam getter nilai memanggil getter yang sama lagi tanpa henti, sehingga muncul error RecursionError. Perbaikannya adalah membaca atribut penyimpan dengan nama berbeda, misalnya self._nilai, sesuai pola pada contoh Suhu dan Tandon.
Kesalahan kedua adalah menugaskan nilai pada property yang tidak memiliki setter. Apabila hanya getter yang didefinisikan, penugasan dari luar ditolak dengan AttributeError, sebagaimana pesan berikut pada kelas dengan property hanya_baca.
class Baca:
@property
def hanya_baca(self):
return 42
b = Baca()
try:
b.hanya_baca = 7
except AttributeError as e:
print(type(e).__name__, str(e))AttributeError property 'hanya_baca' of 'Baca' object has no setterPerilaku ini berguna untuk atribut yang benar-benar tidak boleh diubah dari luar, tetapi dapat mengejutkan apabila setter memang sengaja belum ditulis. Tambahkan setter dengan decorator @nama.setter apabila penugasan perlu didukung. Mekanisme di balik property adalah protokol descriptor yang menjalankan getter dan setter pada saat atribut diakses, sehingga perilakunya konsisten untuk semua instans kelas tersebut.
Ringkasan#
- Decorator
@propertymenjadikan metode dapat dibaca sebagai atribut tanpa tanda kurung, cocok untuk nilai hasil komputasi. - Setter yang didefinisikan dengan
@nama.setterdijalankan pada setiap penugasan, termasuk penugasan di dalam__init__. - Nilai disimpan pada atribut internal berawalan garis bawah agar nama publik dan penyimpanannya tidak bertabrakan.
- Antarmuka
objek.namatetap sama antara atribut biasa dan property, sehingga validasi dapat ditambahkan tanpa merusak pemanggil.
Referensi#
- property — Python Software Foundation
— Dokumentasi bawaan
propertybeserta bentuk getter, setter, dan deleter. - Properties — Python Software Foundation
— Bagian Descriptor HowTo yang menjelaskan cara kerja
@property.