PBO 4 — Enkapsulasi: Private dan Public
Tujuan Pembelajaran:
Pada akhir pertemuan ini, mahasiswa diharapkan mampu:
- Menjelaskan konsep enkapsulasi dan mengapa hal itu penting dalam Pemrograman Berorientasi Objek.
- Memahami bagaimana Python menerapkan konsep private dan public pada atribut dan metode.
- Mengimplementasikan getter dan setter menggunakan konvensi dan
@property
Python untuk mengontrol akses data. - Menerapkan prinsip enkapsulasi pada studi kasus yang relevan.
A. Apa Itu Enkapsulasi?
Bayangkan sebuah kapsul obat. Di dalamnya, ada berbagai bahan aktif yang penting, tetapi Anda tidak perlu tahu komposisi pastinya atau bagaimana mereka bekerja sama. Anda cukup tahu bahwa minum kapsul itu akan membantu Anda sembuh. Anda hanya berinteraksi dengan “permukaan luar” kapsul.
Nah, enkapsulasi dalam PBO memiliki filosofi yang mirip:
Penggabungan Data dan Perilaku: Enkapsulasi berarti membungkus (menggabungkan) data (atribut) dan kode (metode) yang beroperasi pada data tersebut ke dalam satu unit tunggal, yaitu objek. Ini seperti yang sudah kita lakukan di pertemuan sebelumnya dengan kelas
Buku
atauMahasiswa
. Data buku (judul, penulis, status) digabungkan dengan perilaku buku (pinjam, kembalikan) dalam satu objekBuku
.Penyembunyian Informasi (Information Hiding): Ini adalah aspek terpenting dari enkapsulasi. Artinya, menyembunyikan detail implementasi internal suatu objek dari dunia luar. Pengguna objek tidak perlu tahu bagaimana data disimpan atau bagaimana metode bekerja di baliknya. Mereka hanya perlu tahu apa yang bisa dilakukan objek tersebut (melalui antarmuka publiknya).
Mengapa Enkapsulasi Penting?
- Proteksi Data (Data Protection): Mencegah akses langsung dan modifikasi yang tidak diinginkan terhadap data internal objek. Ini seperti melindungi mesin mobil agar tidak semua orang bisa langsung mengutak-atiknya.
- Modularitas: Setiap objek menjadi unit yang mandiri. Perubahan pada implementasi internal suatu objek tidak akan memengaruhi bagian lain dari program, selama antarmuka publiknya tetap sama.
- Kemudahan Pemeliharaan dan Debugging: Jika ada masalah, kita tahu bahwa masalah itu kemungkinan besar ada di dalam objek tersebut, sehingga lebih mudah untuk menemukan dan memperbaikinya.
- Fleksibilitas: Kita bisa mengubah bagaimana data disimpan atau diproses di dalam objek tanpa harus mengubah kode yang menggunakan objek tersebut, selama metode publiknya tetap sama.
- Kontrol Akses: Memungkinkan kita untuk menerapkan logika validasi atau aturan bisnis saat data diatur atau diakses. Misalnya, kita bisa memastikan usia tidak negatif, atau saldo bank tidak bisa menjadi minus tanpa persetujuan khusus.
B. Konsep Private dan Public di Python
Tidak seperti beberapa bahasa pemrograman lain (seperti Java atau C++) yang memiliki kata kunci eksplisit (private
, public
, protected
), Python mengadopsi pendekatan yang sedikit berbeda dan lebih fleksibel untuk enkapsulasi, yang dikenal sebagai “Konvensi Penamaan” dan “Name Mangling”.
1. Anggota Public
- Definisi: Atribut atau metode yang dapat diakses secara langsung dari luar kelas.
- Konvensi: Tidak ada underscore di awal nama. Ini adalah default di Python.
- Contoh:
self.nama
,self.tampilkan_info()
.
2. Anggota Protected (Konvensi Saja)
Definisi: Anggota yang dimaksudkan untuk tidak diakses secara langsung dari luar kelas, tetapi boleh diakses oleh kelas-kelas turunan (akan dibahas di Pewarisan).
Konvensi: Dimulai dengan satu underscore tunggal (
_
).Contoh:
_umur
,_hitung_internal()
.Penting: Ini hanyalah sebuah konvensi. Python tidak benar-benar mencegah Anda mengakses
_umur
dari luar kelas. Programmer lain diharapkan untuk menghormati konvensi ini dan tidak mengaksesnya secara langsung.1 2 3 4 5 6 7 8 9 10 11
class Karyawan: def __init__(self, nama, gaji): self.nama_public = nama # Atribut Public self._gaji_protected = gaji # Atribut Protected (konvensi) def tampil_gaji(self): # Metode Public print(f"Gaji {self.nama_public} (protected): {self._gaji_protected}") k = Karyawan("Budi", 5000000) print(k.nama_public) # Output: Budi (Akses langsung diperbolehkan) print(k._gaji_protected) # Output: 5000000 (Secara teknis bisa diakses, tapi tidak disarankan)
3. Anggota Private (Name Mangling)
Definisi: Anggota yang secara teknis ingin “disembunyikan” dari akses langsung di luar kelas dan bahkan dari kelas turunan.
Konvensi: Dimulai dengan dua underscore (
__
) di awal.Contoh:
__saldo
,__proses_rahasia()
.Mekanisme Python (Name Mangling): Ketika Anda menggunakan dua underscore di awal (
__nama_atribut
), Python melakukan “name mangling”. Ini berarti Python akan secara otomatis mengubah nama atribut tersebut menjadi_NamaKelas__nama_atribut
di balik layar. Jadi,__saldo
dalam kelasAkunBank
akan menjadi_AkunBank__saldo
. Hal ini membuat akses langsung dari luar kelas menjadi lebih sulit (tapi tidak sepenuhnya mustahil, hanya butuh usaha lebih). Ini adalah bentuk enkapsulasi yang lebih kuat di Python.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
class AkunBank: def __init__(self, nama, saldo_awal): self.pemilik = nama # Public self.__saldo = saldo_awal # Private (menggunakan name mangling) def setor(self, jumlah): if jumlah > 0: self.__saldo += jumlah print(f"Setor {jumlah}. Saldo baru: {self.__saldo}") else: print("Jumlah setor harus positif.") def tarik(self, jumlah): if 0 < jumlah <= self.__saldo: self.__saldo -= jumlah print(f"Tarik {jumlah}. Saldo baru: {self.__saldo}") else: print("Jumlah tarik tidak valid atau saldo tidak cukup.") my_account = AkunBank("Alice", 1000) my_account.setor(500) # Output: Setor 500. Saldo baru: 1500 my_account.tarik(200) # Output: Tarik 200. Saldo baru: 1300 # Coba akses atribut private secara langsung # print(my_account.__saldo) # Ini akan menghasilkan AttributeError! # Output: AttributeError: 'AkunBank' object has no attribute '__saldo' # Kita bisa mengaksesnya secara tidak langsung (melalui name mangling) print(my_account._AkunBank__saldo) # Output: 1300 (Tidak disarankan untuk diakses langsung)
Catatan Penting: Meskipun Python tidak memiliki kata kunci
private
yang “benar-benar” melarang akses, konvensi_
dan__
sangat penting dalam komunitas Python untuk menunjukkan maksud programmer. Hormati konvensi ini! Jika sebuah atribut atau metode diawali dengan_
atau__
, itu berarti Anda tidak boleh mengaksesnya atau memodifikasinya secara langsung dari luar kelas, melainkan melalui metode publik yang disediakan.
C. Getter dan Setter (Properti dengan @property
)
Meskipun kita bisa mengakses atribut public secara langsung, seringkali kita ingin memiliki kontrol lebih saat atribut diakses (di-get) atau diubah (di-set). Di sinilah konsep Getter dan Setter berperan.
- Getter (Accessor): Metode yang digunakan untuk mendapatkan (membaca) nilai dari sebuah atribut.
- Setter (Mutator): Metode yang digunakan untuk mengatur (mengubah) nilai dari sebuah atribut. Biasanya melibatkan validasi.
Di Python, kita dapat mengimplementasikan getter dan setter menggunakan properti dengan dekorator @property
. Ini adalah cara yang sangat “Pythonic” (sesuai gaya Python) dan elegan untuk enkapsulasi.
Contoh: Kelas Pegawai
dengan Getter dan Setter
Mari kita buat kelas Pegawai
di mana kita ingin memastikan gaji
selalu positif dan email
memiliki format yang valid.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Pegawai:
def __init__(self, nama, email, gaji):
self._nama = nama # Atribut 'protected' (konvensi)
self._email = email
self._gaji = gaji
# --- Getter untuk Nama (bisa diakses langsung, tapi ini contoh) ---
@property
def nama(self):
print("Mengakses nama...")
return self._nama
# --- Getter & Setter untuk Email ---
@property
def email(self):
print("Mengakses email...")
return self._email
@email.setter
def email(self, new_email):
print("Mengatur email...")
if "@" in new_email and "." in new_email:
self._email = new_email
print(f"Email berhasil diubah menjadi: {self._email}")
else:
print("Error: Format email tidak valid.")
# --- Getter & Setter untuk Gaji ---
@property
def gaji(self):
print("Mengakses gaji...")
return self._gaji
@gaji.setter
def gaji(self, new_gaji):
print("Mengatur gaji...")
if new_gaji >= 0:
self._gaji = new_gaji
print(f"Gaji berhasil diubah menjadi: {self._gaji}")
else:
print("Error: Gaji tidak boleh negatif.")
# --- Penggunaan Kelas Pegawai ---
pegawai1 = Pegawai("Dian", "dian@example.com", 7000000)
print(f"Nama Pegawai: {pegawai1.nama}") # Memanggil getter 'nama'
print(f"Email Awal: {pegawai1.email}") # Memanggil getter 'email'
pegawai1.email = "dian.baru@company.com" # Memanggil setter 'email'
pegawai1.email = "email_invalid" # Memanggil setter 'email' dengan data tidak valid
print(f"Gaji Awal: {pegawai1.gaji}") # Memanggil getter 'gaji'
pegawai1.gaji = 7500000 # Memanggil setter 'gaji'
pegawai1.gaji = -100000 # Memanggil setter 'gaji' dengan data tidak valid
# Perhatikan bagaimana kita memanggilnya seperti atribut biasa (pegawai1.gaji),
# tetapi di belakang layar, Python memanggil metode getter atau setter yang sesuai.
Penjelasan @property
:
@property
(getter): Diletakkan di atas metode yang berfungsi sebagai getter. Metode ini tidak menerima parameter selainself
dan harus mengembalikan nilai atribut. Nama metode ini akan menjadi nama properti yang bisa diakses.@nama_properti.setter
: Diletakkan di atas metode yang berfungsi sebagai setter. Metode ini harus memiliki nama yang sama dengan getter (nama_properti
) dan menerima satu parameter tambahan (nilai baru) selainself
.
Pendekatan @property
ini sangat bagus karena:
- Sintaks Bersih: Dari luar, Anda berinteraksi dengan
pegawai1.gaji
seperti atribut biasa, bukanpegawai1.get_gaji()
ataupegawai1.set_gaji(nilai)
. - Kontrol Penuh: Meskipun terlihat seperti akses langsung, sebenarnya Anda memiliki logika validasi di balik layar.
- Fleksibilitas Evolusi: Anda bisa memulai dengan atribut public biasa, dan jika nanti Anda membutuhkan validasi, Anda bisa dengan mudah mengubahnya menjadi properti dengan
@property
tanpa mengubah kode klien yang menggunakan atribut tersebut.
D. Studi Kasus: Meningkatkan Kelas Buku
dengan Enkapsulasi
Mari kita terapkan konsep enkapsulasi pada kelas Buku
kita. Kita ingin memastikan beberapa hal:
tahun_terbit
hanya bisa diatur sekali saat inisialisasi dan tidak bisa diubah sesudahnya.status
buku hanya bisa diubah melalui metodepinjam()
dankembalikan()
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import datetime
class Buku:
daftar_semua_buku = []
jumlah_buku_terdaftar = 0
def __init__(self, judul, penulis, tahun_terbit):
# Menggunakan _ di awal untuk menunjukkan ini adalah atribut internal
# yang akan diakses melalui property (jika perlu)
self._judul = judul
self._penulis = penulis
# Tahun terbit ini akan kita jadikan 'read-only' dari luar
if not Buku._validasi_tahun_internal(tahun_terbit):
raise ValueError("Tahun terbit tidak valid! Harus antara 1500 dan tahun sekarang.")
self.__tahun_terbit = tahun_terbit # Atribut private karena tidak boleh diubah
self.__status = "Tersedia" # Atribut private, hanya bisa diubah via metode pinjam/kembalikan
Buku.jumlah_buku_terdaftar += 1
Buku.daftar_semua_buku.append(self)
# --- Properti Getter untuk Judul (Contoh, bisa juga langsung public) ---
@property
def judul(self):
return self._judul
# --- Properti Getter untuk Penulis ---
@property
def penulis(self):
return self._penulis
# --- Properti Getter (read-only) untuk Tahun Terbit ---
@property
def tahun_terbit(self):
return self.__tahun_terbit # Mengakses atribut private
# --- Properti Getter (read-only) untuk Status ---
@property
def status(self):
return self.__status # Mengakses atribut private
# --- Metode untuk mengubah status (enkapsulasi perilaku) ---
def pinjam(self):
if self.__status == "Tersedia":
self.__status = "Dipinjam"
print(f"Buku '{self._judul}' berhasil dipinjam.")
else:
print(f"Maaf, buku '{self._judul}' sedang '{self.__status}'. Tidak bisa dipinjam.")
def kembalikan(self):
if self.__status == "Dipinjam":
self.__status = "Tersedia"
print(f"Buku '{self._judul}' berhasil dikembalikan.")
else:
print(f"Buku '{self._judul}' tidak dalam status dipinjam.")
def tampilkan_detail(self):
print(f"\n--- Detail Buku ({self.judul}) ---")
print(f"Judul: {self.judul}") # Memanggil properti
print(f"Penulis: {self.penulis}") # Memanggil properti
print(f"Tahun Terbit: {self.tahun_terbit}") # Memanggil properti
print(f"Status: {self.status}") # Memanggil properti
print("-------------------")
@classmethod
def tampilkan_total_buku(cls):
print(f"\nTotal buku yang terdaftar di sistem: {cls.jumlah_buku_terdaftar} buku.")
@staticmethod
def _validasi_tahun_internal(tahun): # Metode statis ini dibuat 'protected'
tahun_sekarang = datetime.datetime.now().year
return 1500 <= tahun <= tahun_sekarang
# --- Penggunaan Kelas Buku dengan Enkapsulasi ---
try:
b1 = Buku("Harry Potter", "J.K. Rowling", 1997)
b2 = Buku("Filosofi Teras", "Henry Manampiring", 2019)
# b_invalid = Buku("Buku Masa Depan", "X", 2030) # Ini akan memicu ValueError
except ValueError as e:
print(f"Error saat membuat buku: {e}")
b1.tampilkan_detail()
b2.tampilkan_detail()
# Coba mengubah status secara langsung (tidak disarankan)
# b1.__status = "Hilang" # Ini tidak akan bekerja seperti yang diharapkan karena name mangling
# Mengakses properti (seperti atribut biasa)
print(f"Judul buku 1: {b1.judul}")
print(f"Tahun terbit buku 1: {b1.tahun_terbit}")
# Coba mengubah tahun terbit (tidak bisa karena hanya getter)
# b1.tahun_terbit = 2000 # Ini akan menghasilkan AttributeError
# Mengubah status melalui metode yang dienkapsulasi
b1.pinjam()
b1.tampilkan_detail()
b1.kembalikan()
b1.tampilkan_detail()
Buku.tampilkan_total_buku()
Penjelasan:
- Perhatikan penggunaan
self.__tahun_terbit
danself.__status
untuk atribut yang ingin kita jadikan private. - Kita menyediakan properti
tahun_terbit
danstatus
hanya dengan getter, tanpa setter. Ini membuatnya menjadi properti read-only dari luar kelas. Pengguna bisa membaca nilainya, tetapi tidak bisa mengubahnya secara langsung. - Perubahan
status
hanya dapat terjadi melalui metodepinjam()
dankembalikan()
, yang berarti logika bisnis (misalnya, memastikan buku tersedia sebelum dipinjam) terkunci di dalam metode tersebut, tidak bisa diakali dari luar. - Metode
_validasi_tahun_internal
diberi awalan_
untuk menunjukkan bahwa ini adalah metode internal kelas yang tidak dimaksudkan untuk dipanggil langsung dari luar.
E. Tugas
Refactor Kelas
Produk
dengan Enkapsulasi:- Ambil kelas
Produk
dari tugas pertemuan 3. - Terapkan enkapsulasi pada atribut
harga
danstok
menggunakan@property
(getter dan setter). - Pastikan
harga
tidak bisa negatif. - Pastikan
stok
tidak bisa menjadi negatif saatkurangi_stok()
dipanggil (gunakan logika validasi di setterstok
atau di metodekurangi_stok
). - Jadikan
nama_produk
sebagai properti read-only (hanya getter, tanpa setter). - Pastikan semua akses dan modifikasi data dilakukan melalui properti atau metode yang sesuai.
- Tuliskan skenario penggunaan yang menunjukkan bagaimana Anda mengakses dan memodifikasi data, serta bagaimana validasi bekerja saat ada input yang tidak valid.
- Ambil kelas