Property

Decorator @property untuk atribut hasil komputasi dan validasi tanpa boilerplate.

Daftar isi

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.

python Contoh: luas sebagai property
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)
text Keluaran
25
100

Ekspresi 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.

python Contoh: validasi suhu dengan setter
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))
text Keluaran
25
ValueError di bawah nol absolut

Getter 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.

python tandon.py
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))
text Keluaran
60
ValueError isi melebihi kapasitas

Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.

bash
# menjalankan program kelas Tandon
python3 tandon.py
text Keluaran
60
ValueError isi melebihi kapasitas

Perhatikan 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.

python Contoh: getter yang membaca dirinya sendiri
class Rekursif:
    @property
    def nilai(self):
        return self.nilai

r = Rekursif()
try:
    print(r.nilai)
except RecursionError:
    print("RecursionError")
text Keluaran
RecursionError

Ekspresi 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.

python Contoh: property tanpa setter
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))
text Keluaran
AttributeError property 'hanya_baca' of 'Baca' object has no setter

Perilaku 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 @property menjadikan metode dapat dibaca sebagai atribut tanpa tanda kurung, cocok untuk nilai hasil komputasi.
  • Setter yang didefinisikan dengan @nama.setter dijalankan 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.nama tetap sama antara atribut biasa dan property, sehingga validasi dapat ditambahkan tanpa merusak pemanggil.

Referensi#