Di Python, atribut instance biasanya langsung terbaca-tertulis dari luar class: p.harga = 5000 sah tanpa method khusus. Lalu bagaimana bila sebuah nilai perlu dihitung setiap kali dibaca, atau penulisan atribut perlu divalidasi? Banyak bahasa memaksa kita menulis getHarga() dan setHarga() sejak awal. Python memilih jalan berbeda: mulailah dengan atribut biasa, dan naikkan atribut tersebut menjadi property hanya ketika dibutuhkan — tanpa mengubah satu baris pun di kode pemanggil. Chapter ini membahas mekanisme decorator @property tersebut.
Atribut yang Dihitung #
Decorator @property menandai sebuah method agar diakses layaknya atribut: tanpa tanda kurung. Method seperti ini disebut computed attribute — nilainya dihitung setiap kali dibaca.
class Lingkaran:
def __init__(self, radius):
self.radius = radius
@property
def luas(self):
return 3.14159 * self.radius ** 2
@property
def keliling(self):
return 2 * 3.14159 * self.radius
lingkaran = Lingkaran(7)
print(lingkaran.luas)
print(lingkaran.keliling)
# property tanpa setter bersifat read-only
try:
lingkaran.luas = 100
except AttributeError as e:
print("AttributeError:", e)Output yang Dihasilkan:
153.93791
43.98226
AttributeError: property 'luas' of 'Lingkaran' object has no setterMari kita telaah baris-baris pentingnya:
- Baris 5 (
@property): mendaftarkanluassebagai property pada classLingkaran. Sejak baris ini, ekspresilingkaran.luasmenjalankan method tanpa tanda kurung. - Baris 7 (
return 3.14159 * self.radius ** 2): luas selalu dihitung ulang dariradiusterkini — ubahlingkaran.radius, danlingkaran.luasikut berubah tanpa perlu disinkronkan. - Baris 21 (
lingkaran.luas = 100): property yang hanya punya getter menolak penugasan.luasmemang turunan dariradius, bukan data mandiri yang boleh ditulis terpisah.
Mekanismenya bekerja lewat protokol descriptor: @property membuat objek khusus yang mencegat pembacaan atribut pada level class — detailnya dibahas pada Chapter Descriptors.
Validasi di dalam Setter #
Property juga bisa punya setter — method kedua yang mencegat penugasan. Kita mendefinisikannya dengan decorator @<nama>.setter, dan di sinilah validasi ditempatkan:
class Produk:
def __init__(self, nama, harga):
self.nama = nama
self.harga = harga # penugasan ini menelusuri setter di bawah
@property
def harga(self):
return self._harga
@harga.setter
def harga(self, nilai):
if nilai < 0:
raise ValueError("harga tidak boleh negatif")
self._harga = nilai
sepatu = Produk("Sepatu Lari", 350000)
print(sepatu.harga)
sepatu.harga = 299000 # diskon: validasi tetap berjalan
print(sepatu.harga)
try:
sepatu.harga = -1
except ValueError as e:
print("ValueError:", e)Output yang Dihasilkan:
350000
299000
ValueError: harga tidak boleh negatifAlur datanya patut diamati dengan teliti:
- Baris 4 (
self.harga = harga): baris ini di dalam__init__terlihat seperti penugasan biasa, tetapi karenahargaadalah property, interpreter mengalihkannya ke setter — validasi berjalan sejak objek pertama kali dibuat. - Baris 10 (
@harga.setter): nama decorator harus sama dengan property-nya; method ini menerima nilai baru melalui parameternilai. - Baris 14 (
self._harga = nilai): setter menyimpan nilai sebenarnya di atributself._harga— awalan satu garis bawah adalah konvensi “internal” yang dibahas pada Chapter Encapsulation. - Baris 24 (
sepatu.harga = -1): penugasan invalid berhenti di setter danValueErrornaik ke pemanggil — tidak ada objek dengan harga negatif yang sempat tercipta.
Tanpa property, pembaca atribut harus memanggil produk.get_harga() dan setiap penulisan harus diingat untuk memanggil produk.set_harga(nilai). Dengan property, kedua arah tetap memakai sintaks atribut yang bersih.
Evolusi API Tanpa Merusak Pemanggil #
Kekuatan terbesar property muncul saat program bertumbuh. Misalkan class Tagihan memulai hidupnya dengan atribut biasa total; ratusan baris kode lama memakai tagihan.total. Berbulan-bulan kemudian total harus dihitung dari subtotal dan pajak:
class Tagihan:
"""Versi 1: total adalah atribut biasa."""
def __init__(self, total):
self.total = total
tagihan = Tagihan(150_000)
print(tagihan.total) # pemanggil memakai sintaks atribut
class Tagihan:
"""Versi 2: total menjadi property yang dihitung."""
PAJAK = 0.11
def __init__(self, subtotal):
self.subtotal = subtotal
@property
def total(self):
return int(self.subtotal * (1 + self.PAJAK))
tagihan = Tagihan(150_000)
print(tagihan.total) # sintaks pemanggil TIDAK berubahOutput yang Dihasilkan:
150000
166500Pemanggil di versi 1 dan versi 2 menulis ekspresi yang sama: tagihan.total. Hanya definisi class yang berubah — atribut biasa naik kelas menjadi property, dan seluruh kode lama tetap berjalan. Inilah alasan budaya Python tidak menulis getter/setter untuk semua atribut sejak awal: pintu evolusi ini selalu terbuka.
Tiga varian decorator melengkapi keluarga property: @property untuk getter, @total.setter untuk penulisan, dan @total.deleter untuk menghapus atribut lewat del tagihan.total — varian ketiga jarang dibutuhkan tetapi tersedia. Semuanya tetap menempatkan satu nama total di class, bukan tiga method berbeda yang harus dihafal pemanggil.