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.

python
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:

text
153.93791
43.98226
AttributeError: property 'luas' of 'Lingkaran' object has no setter

Mari kita telaah baris-baris pentingnya:

  1. Baris 5 (@property): mendaftarkan luas sebagai property pada class Lingkaran. Sejak baris ini, ekspresi lingkaran.luas menjalankan method tanpa tanda kurung.
  2. Baris 7 (return 3.14159 * self.radius ** 2): luas selalu dihitung ulang dari radius terkini — ubah lingkaran.radius, dan lingkaran.luas ikut berubah tanpa perlu disinkronkan.
  3. Baris 21 (lingkaran.luas = 100): property yang hanya punya getter menolak penugasan. luas memang turunan dari radius, 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:

python
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:

text
350000
299000
ValueError: harga tidak boleh negatif

Alur datanya patut diamati dengan teliti:

  1. Baris 4 (self.harga = harga): baris ini di dalam __init__ terlihat seperti penugasan biasa, tetapi karena harga adalah property, interpreter mengalihkannya ke setter — validasi berjalan sejak objek pertama kali dibuat.
  2. Baris 10 (@harga.setter): nama decorator harus sama dengan property-nya; method ini menerima nilai baru melalui parameter nilai.
  3. Baris 14 (self._harga = nilai): setter menyimpan nilai sebenarnya di atribut self._harga — awalan satu garis bawah adalah konvensi “internal” yang dibahas pada Chapter Encapsulation.
  4. Baris 24 (sepatu.harga = -1): penugasan invalid berhenti di setter dan ValueError naik 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:

python
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 berubah

Output yang Dihasilkan:

text
150000
166500

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