Pendahuluan — menjaga data di dalam object

Di Attribute, Method, dan __init__ (#42) kamu sudah punya self.stok dan method pinjam(). Masalahnya: siapa pun masih bisa menulis buku.stok = -99 dari luar — aturan bisnis dilompati.

Encapsulation adalah praktik menyembunyikan detail implementasi dan membuka akses lewat “pintu” yang kamu kendalikan. Di Python, itu bukan kunci gembok keras seperti bahasa lain — lebih ke konvensi + API (_, __, @property).

Prasyarat: Selesai #42 (attribute, method, __init__). Fondasi object: Class & Object (#41) · mental model: Mengenal OOP (#40).

Masalah: attribute publik terlalu terbuka

Tanpa penjaga, state bisa rusak dari mana saja:

class Buku:
    def __init__(self, judul, tahun, stok=1):
        self.judul = judul
        self.tahun = tahun
        self.stok = stok

    def pinjam(self):
        if self.stok <= 0:
            raise ValueError("stok habis")
        self.stok -= 1


buku = Buku("Belajar Python", 2024, stok=2)
buku.stok = -5          # lolos — pinjam() tidak dipanggil
buku.tahun = 1200       # tahun mustahil, tetap diterima
print(buku.stok, buku.tahun)

Method pinjam() sudah benar, tapi “pintu belakang” masih terbuka. Encapsulation menutup pintu itu — atau setidaknya membuatnya sengaja dan terlihat.

Tiga tingkat akses di Python

Python tidak punya private keyword seperti Java. Yang ada: kesepakatan nama:

Nama Arti konvensi Contoh
stok Publik — boleh diakses dari luar buku.stok
_stok “Internal” — jangan disentuh dari luar kecuali sadar self._stok
__stok Name-mangling — Python ubah jadi _Buku__stok hindari bentrok di inheritance
class Buku:
    def __init__(self, judul, stok=1):
        self.judul = judul      # publik OK untuk label
        self._stok = stok       # internal — pakai _ awalan
        self.__kode = "BK-01"   # name-mangling

buku = Buku("Belajar Python", stok=2)
print(buku.judul)          # OK
print(buku._stok)          # bisa, tapi “jangan” — sinyal internal
# print(buku.__kode)       # AttributeError
print(buku._Buku__kode)    # bisa, tapi jelek — hindari

Gunakan _ untuk state yang ingin kamu jaga. Cadangkan __ jika benar-benar khawatir bentrok nama di subclass (lihat Inheritance (#44)). Jangan mengira __ = keamanan mutlak.

@property — baca lewat API bersih

@property membuat method terlihat seperti attribute saat dibaca. Dari luar: buku.stok. Di dalam: kamu yang tentukan dari mana nilai itu datang:

class Buku:
    def __init__(self, judul, tahun, stok=1):
        self.judul = judul
        self._tahun = tahun
        self._stok = stok

    @property
    def stok(self):
        """Baca stok — dari luar seperti attribute."""
        return self._stok

    @property
    def tahun(self):
        return self._tahun


buku = Buku("Belajar Python", 2024, stok=2)
print(buku.stok)    # 2 — memanggil property, bukan field mentah
print(buku.tahun)   # 2024
# buku.stok = 9     # AttributeError: can't set attribute (belum ada setter)

Ini jembatan bagus dari #42: pembaca masih menulis buku.stok, tapi kamu sudah mengontrol implementasinya. Di contoh di atas kita isi _tahun/_stok langsung karena belum ada setter — setelah ada setter (bagian berikutnya), prefer self.tahun = tahun di __init__ supaya validasi ikut jalan.

@setter — validasi saat menulis

Tambah setter agar penulisan juga lewat pintu yang sama. Di sini kita jaga tahun dan stok:

class Buku:
    def __init__(self, judul, tahun, stok=1):
        self.judul = judul
        self.tahun = tahun   # lewat setter di bawah
        self.stok = stok

    @property
    def tahun(self):
        return self._tahun

    @tahun.setter
    def tahun(self, nilai):
        if nilai < 1900 or nilai > 2100:
            raise ValueError("tahun tidak masuk akal")
        self._tahun = nilai

    @property
    def stok(self):
        return self._stok

    @stok.setter
    def stok(self, nilai):
        if nilai < 0:
            raise ValueError("stok tidak boleh negatif")
        self._stok = nilai

    def pinjam(self):
        if self.stok <= 0:
            raise ValueError("stok habis")
        self.stok -= 1
        return self.stok


buku = Buku("Belajar Python", 2024, stok=2)
print(buku.stok)          # 2
buku.stok = 1             # OK lewat setter
print(buku.pinjam())      # 0
# buku.stok = -1          # ValueError: stok tidak boleh negatif
# Buku("Kuno", 1800)      # ValueError: tahun tidak masuk akal

Perhatikan: di __init__ kita menulis self.tahun = tahun dan self.stok = stok — itu memanggil setter, jadi validasi jalan sejak object lahir. Jangan assign langsung ke self._tahun di __init__ kalau kamu ingin aturan yang sama berlaku di mana-mana (kecuali kasus khusus yang kamu sadari).

Bonus kecil: di pinjam(), baris self.stok -= 1 juga lewat property — Python membaca lewat getter, lalu menulis lewat setter. Aturan “stok tidak negatif” tetap dijaga.

Tip: Method seperti pinjam() tetap berguna untuk aksi bisnis (“pinjam satu salinan”). Property menjaga invariant data (stok ≥ 0, tahun wajar). Keduanya saling melengkapi — bukan saling mengganti.

Jebakan paling sering di property: di dalam setter kamu menulis self.stok = ... lagi — itu memanggil setter yang sama, lalu RecursionError:

# SALAH — setter memanggil dirinya sendiri
class BukuSalah:
    def __init__(self, stok=1):
        self.stok = stok

    @property
    def stok(self):
        return self._stok

    @stok.setter
    def stok(self, nilai):
        self.stok = nilai  # memanggil setter lagi → RecursionError

# BukuSalah(2)  # RecursionError: maximum recursion depth exceeded

# BENAR — simpan ke attribute internal
class BukuBenar:
    def __init__(self, stok=1):
        self.stok = stok

    @property
    def stok(self):
        return self._stok

    @stok.setter
    def stok(self, nilai):
        if nilai < 0:
            raise ValueError("stok tidak boleh negatif")
        self._stok = nilai


b = BukuBenar(2)
print(b.stok)  # 2
Kode luar buku.stok = 3 @property / setter validasi & aturan pintu resmi tolak nilai aneh _stok internal Luar tidak menulis _stok langsung — lewat setter.
Encapsulation = satu pintu resmi ke state internal.

Kapan encapsulation terasa penting?

  • Proyek tumbuh — banyak file menyentuh object yang sama; satu pintu mengurangi bug “siapa yang set stok aneh?”
  • API publik class — kamu bisa ganti penyimpanan internal (_stok → hitung dari list pinjaman) tanpa mengubah pemanggilan buku.stok
  • Validasi terpusat — aturan tahun/stok tidak diduplikasi di 10 tempat

Untuk script 30 baris sekali pakai, attribute publik dari #42 sering cukup. Encapsulation mulai “wajib secara praktik” saat class dipakai orang lain (atau dirimu 3 bulan kemudian).

Pola Dasar — merancang encapsulation

  1. 1
    Tandai state sensitif dengan _ _stok, _tahun — sinyal “internal”.
  2. 2
    Buka baca lewat @property API luar tetap buku.stok, implementasi bisa berubah.
  3. 3
    Validasi di @setter Tolak nilai mustahil sedini mungkin.
  4. 4
    Init lewat property self.stok = stok di __init__ supaya aturan sama.
  5. 5
    Method untuk aksi bisnis pinjam() / kembalikan() — bukan hanya setter mentah.
Ringkas: _internal → property → setter → init lewat API → method aksi.

Kode lengkap — salin & jalankan

Simpan sebagai buku_terlindungi.py, lalu python buku_terlindungi.py:

class Buku:
    def __init__(self, judul, penulis, tahun, stok=1):
        self.judul = judul
        self.penulis = penulis
        self.tahun = tahun
        self.stok = stok

    @property
    def tahun(self):
        return self._tahun

    @tahun.setter
    def tahun(self, nilai):
        if nilai < 1900 or nilai > 2100:
            raise ValueError("tahun tidak masuk akal")
        self._tahun = nilai

    @property
    def stok(self):
        return self._stok

    @stok.setter
    def stok(self, nilai):
        if nilai < 0:
            raise ValueError("stok tidak boleh negatif")
        self._stok = nilai

    def info(self):
        return f"{self.judul} · {self.penulis} ({self.tahun}) · stok {self.stok}"

    def pinjam(self):
        if self.stok <= 0:
            raise ValueError("stok habis")
        self.stok -= 1
        return self.stok

    def kembalikan(self):
        self.stok += 1
        return self.stok


a = Buku("Belajar Python", "Sari", 2024, stok=2)
print(a.info())
print("pinjam:", a.pinjam())
print("stok:", a.stok)
a.stok = 5
print("set stok:", a.stok)
# a.stok = -1  # ValueError

Kesalahan umum

Gejala Penyebab tipikal Perbaikan
AttributeError: property 'stok' of 'Buku' object has no setter Hanya ada @property, belum @stok.setter Tambah setter, atau jangan assign dari luar
RecursionError di property Di dalam getter/setter menulis self.stok lagi (bukan self._stok) Simpan di self._stok; property hanya pintu
Validasi tidak jalan di __init__ Assign langsung self._stok = stok tanpa lewat setter Pakai self.stok = stok di init
AttributeError: ... has no attribute '_Buku__x' bingung Mengira __x “rahasia absolut”, lalu akses salah nama Lebih sering cukup _x; pahami mangling sebelum pakai __
Setter terlalu ketat / longgar Aturan bisnis tidak jelas Tulis invariant dulu (stok ≥ 0), baru kode

Latihan singkat

  1. Tambah property tersedia (read-only) yang mengembalikan True jika stok > 0.
  2. Buat setter judul yang menolak string kosong.
  3. Coba sengaja tulis self.stok = self.stok di dalam setter (tanpa _) — amati RecursionError, lalu perbaiki.

FAQ singkat

Apakah _ benar-benar mencegah akses?
Tidak. Itu sinyal sosial untuk programmer. Python percaya dewasa: kamu bisa menyentuh _stok, tapi sebaiknya tidak.

Haruskah semua attribute jadi property?
Tidak. Mulai dari data yang punya aturan (stok, tahun, email). Label sederhana seperti judul sering aman publik dulu.

Apa bedanya property dan method info()?
Property terasa seperti data (buku.stok). Method terasa seperti aksi atau ringkasan (buku.info()). Pilih yang paling jujur dibaca orang lain.

Kesimpulan & langkah berikutnya

Encapsulation di Python = konvensi nama + pintu resmi (@property / setter) supaya invariant tetap hidup. Kamu tidak mengunci dunia — kamu merapikan kontrak class.

Artikel ini adalah #43 (ini) — langkah keempat Seri 3 setelah Attribute & __init__ (#42).

Lanjut ke Inheritance & super() (#44): class Ebook(Buku), super().__init__, dan override method tanpa menduplikasi kode.

Seri 3 progress: 5/10 artikel live. Kamu di langkah #43 (ini). Prasyarat: #42 · #41 · #40.