Python OOP

Encapsulation

Melindungi data dengan access modifiers dan property decorators

Apa itu Encapsulation?

Encapsulation adalah prinsip OOP yang menyembunyikan detail implementasi internal dari pengguna eksternal. Dengan encapsulation, kita dapat:

  • Melindungi data dari akses atau modifikasi yang tidak sah
  • Mengontrol bagaimana data diakses dan dimodifikasi
  • Menyediakan interface yang bersih untuk berinteraksi dengan object
  • Mengurangi coupling antar komponen

Analogi

Encapsulation seperti ATM. Kalian tidak perlu tahu bagaimana ATM bekerja secara internal (koneksi database, mekanisme pengeluaran uang, dll). Kalian hanya berinteraksi melalui interface yang disediakan (tombol dan layar).

Access Modifiers di Python

Python tidak memiliki access modifiers strict seperti Java (public, private, protected). Sebaliknya, Python menggunakan naming conventions untuk mengindikasikan tingkat akses:

ConventionAksesContohDeskripsi
No underscorePublicnameDapat diakses dari mana saja
Single underscoreProtected_nameKonvensi untuk internal use (masih bisa diakses)
Double underscorePrivate__nameName mangling - sulit diakses dari luar

Python Philosophy

Python mengikuti prinsip "We are all consenting adults here". Access modifiers lebih merupakan konvensi daripada enforcement. Developer dipercaya untuk mengikuti konvensi yang ada.

Public Attributes

Atribut public dapat diakses dan dimodifikasi dari mana saja:

public.py
class Student:
    def __init__(self, name, nim):
        self.name = name  # Public attribute
        self.nim = nim    # Public attribute

student = Student("Budi", "TI12345")

# Akses public attribute
print(student.name)  # Budi

# Modifikasi public attribute
student.name = "Budi Santoso"
print(student.name)  # Budi Santoso

Protected Attributes

Atribut protected ditandai dengan single underscore _. Ini adalah konvensi yang menandakan atribut untuk internal use, tapi masih bisa diakses:

protected.py
class Student:
    def __init__(self, name, nim):
        self.name = name
        self._program = "Teknik"  # Protected attribute

    def get_program(self):
        return self._program

student = Student("Budi", "TI12345")

# Bisa diakses tapi tidak disarankan
print(student._program)  # Teknik

# Cara yang disarankan
print(student.get_program())  # Teknik

Konvensi Protected

Protected attributes dapat diakses, tetapi dengan konvensi single underscore, developer lain tahu bahwa ini untuk internal use dan tidak seharusnya diakses langsung dari luar class.

Private Attributes

Atribut private ditandai dengan double underscore __. Python melakukan "name mangling" sehingga atribut ini sulit diakses dari luar class:

private.py
class Student:
    def __init__(self, name, nim):
        self.name = name
        self.__id = "2023-" + nim  # Private attribute

    def get_id(self):
        return self.__id

student = Student("Budi", "12345")

# Akses melalui method public
print(student.get_id())  # 2023-12345

# Akses langsung akan error
try:
    print(student.__id)
except AttributeError as e:
    print(f"Error: {e}")

# Name mangling - bisa diakses tapi tidak disarankan
print(student._Student__id)  # 2023-12345

Property Decorators

Property decorators menyediakan cara Pythonic untuk membuat getter dan setter:

Getter dengan @property

Property decorator mengubah method menjadi attribute yang dapat dibaca:

property_getter.py
class Student:
    def __init__(self, name, birth_year):
        self.name = name
        self._birth_year = birth_year

    @property
    def age(self):
        from datetime import datetime
        current_year = datetime.now().year
        return current_year - self._birth_year

    @property
    def birth_year(self):
        return self._birth_year

student = Student("Budi", 2000)

# Memanggil seperti attribute, bukan method
print(student.age)  # 25 (tergantung tahun saat ini)
print(student.birth_year)  # 2000

Setter dengan @property.setter

Setter memungkinkan kita mengontrol bagaimana attribute dimodifikasi:

property_setter.py
class Student:
    def __init__(self, name, nim):
        self.name = name
        self._nim = nim
        self._program = "Teknik"

    @property
    def program(self):
        return self._program

    @program.setter
    def program(self, value):
        valid_programs = ["Teknik", "Sains", "Bisnis"]
        if value in valid_programs:
            self._program = value
        else:
            raise ValueError(f"Program harus salah satu dari {valid_programs}")

student = Student("Budi", "12345")

# Menggunakan setter
student.program = "Sains"  # OK
print(student.program)  # Sains

try:
    student.program = "Hukum"  # Error
except ValueError as e:
    print(f"Error: {e}")

Deleter dengan @property.deleter

Deleter mengontrol perilaku ketika attribute dihapus:

property_deleter.py
class Student:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("Name cannot be empty")
        self._name = value

    @name.deleter
    def name(self):
        print("Deleting name...")
        self._name = None

student = Student("Budi")
print(student.name)  # Budi

del student.name  # Deleting name...
print(student.name)  # None

Contoh Lengkap: Bank Account

Mari lihat contoh lengkap encapsulation pada class BankAccount:

bank_account.py
class BankAccount:
    """Class untuk merepresentasikan akun bank dengan encapsulation"""

    def __init__(self, account_number, owner, initial_balance=0):
        self.account_number = account_number  # Public
        self._owner = owner  # Protected
        self.__balance = initial_balance  # Private
        self.__transaction_history = []  # Private

    # Property untuk balance (read-only)
    @property
    def balance(self):
        """Getter untuk balance - read only"""
        return self.__balance

    # Property untuk owner
    @property
    def owner(self):
        return self._owner

    @owner.setter
    def owner(self, new_owner):
        if not new_owner or len(new_owner) < 3:
            raise ValueError("Owner name must be at least 3 characters")
        self._owner = new_owner

    # Method untuk deposit
    def deposit(self, amount):
        """Public method untuk deposit uang"""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")

        self.__balance += amount
        self.__add_transaction("Deposit", amount)
        return f"Deposit successful. New balance: Rp{self.__balance:,.0f}"

    # Method untuk withdraw
    def withdraw(self, amount):
        """Public method untuk withdraw uang"""
        if amount <= 0:
            raise ValueError("Withdraw amount must be positive")

        if amount > self.__balance:
            raise ValueError("Insufficient balance")

        self.__balance -= amount
        self.__add_transaction("Withdraw", amount)
        return f"Withdraw successful. New balance: Rp{self.__balance:,.0f}"

    # Method untuk transfer
    def transfer(self, target_account, amount):
        """Public method untuk transfer ke akun lain"""
        if amount <= 0:
            raise ValueError("Transfer amount must be positive")

        if amount > self.__balance:
            raise ValueError("Insufficient balance")

        self.__balance -= amount
        target_account.__balance += amount

        self.__add_transaction(f"Transfer to {target_account.account_number}", amount)
        target_account.__add_transaction(f"Transfer from {self.account_number}", amount)

        return f"Transfer successful. New balance: Rp{self.__balance:,.0f}"

    # Private method untuk menambah transaksi
    def __add_transaction(self, type, amount):
        """Private method - hanya untuk internal use"""
        from datetime import datetime
        transaction = {
            'type': type,
            'amount': amount,
            'timestamp': datetime.now(),
            'balance_after': self.__balance
        }
        self.__transaction_history.append(transaction)

    # Public method untuk melihat history
    def get_transaction_history(self, last_n=5):
        """Public method untuk mendapatkan transaction history"""
        return self.__transaction_history[-last_n:]

    # Method untuk display info
    def display_info(self):
        print(f"\n{'='*50}")
        print(f"Account Number: {self.account_number}")
        print(f"Owner: {self._owner}")
        print(f"Balance: Rp{self.__balance:,.0f}")
        print(f"{'='*50}")

# Penggunaan
print("=== Creating Bank Accounts ===")
acc1 = BankAccount("001", "Budi Santoso", 1000000)
acc2 = BankAccount("002", "Ani Wijaya", 500000)

print("\n=== Account Info ===")
acc1.display_info()

print("\n=== Deposit ===")
print(acc1.deposit(500000))

print("\n=== Withdraw ===")
print(acc1.withdraw(200000))

print("\n=== Transfer ===")
print(acc1.transfer(acc2, 300000))

print("\n=== Check Balance (using property) ===")
print(f"Acc1 Balance: Rp{acc1.balance:,.0f}")
print(f"Acc2 Balance: Rp{acc2.balance:,.0f}")

print("\n=== Transaction History ===")
for trans in acc1.get_transaction_history():
    print(f"{trans['type']}: Rp{trans['amount']:,.0f} "
          f"- Balance: Rp{trans['balance_after']:,.0f}")

print("\n=== Try to access private attribute (will error) ===")
try:
    print(acc1.__balance)
except AttributeError as e:
    print(f"Error: {e}")

print("\n=== Access via name mangling (not recommended) ===")
print(f"Balance via name mangling: Rp{acc1._BankAccount__balance:,.0f}")

print("\n=== Try invalid withdraw ===")
try:
    acc1.withdraw(10000000)
except ValueError as e:
    print(f"Error: {e}")

Kapan Menggunakan Access Modifiers

Gunakan Public

class Product:
    def __init__(self, name, price):
        self.name = name  # Public - data yang umum diakses
        self.price = price  # Public

Gunakan Protected

class DatabaseConnection:
    def __init__(self, host):
        self._host = host  # Protected - untuk subclass
        self._connection = None  # Protected

    def _connect(self):  # Protected method
        # Internal implementation
        pass

Gunakan Private

class PasswordManager:
    def __init__(self, password):
        self.__password = password  # Private - data sensitif

    def verify(self, input_password):
        return input_password == self.__password

Praktik Terbaik

1. Gunakan Properties untuk Validasi

class Student:
    def __init__(self, name, gpa):
        self.name = name
        self._gpa = None
        self.gpa = gpa  # Using setter for validation

    @property
    def gpa(self):
        return self._gpa

    @gpa.setter
    def gpa(self, value):
        if not 0.0 <= value <= 4.0:
            raise ValueError("GPA must be between 0.0 and 4.0")
        self._gpa = value

2. Encapsulate Complex Logic

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @property
    def diameter(self):
        return self._radius * 2

    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2

    @property
    def circumference(self):
        import math
        return 2 * math.pi * self._radius

3. Don't Expose Mutable Collections

# Bad - exposes internal list
class Classroom:
    def __init__(self):
        self.students = []

# Good - provides controlled access
class Classroom:
    def __init__(self):
        self.__students = []

    def add_student(self, student):
        self.__students.append(student)

    def get_students(self):
        return self.__students.copy()  # Return copy

Latihan

  1. Buat class Employee dengan:

    • Private attribute untuk salary
    • Property untuk membaca salary
    • Method untuk menaikkan salary dengan validasi
  2. Buat class ShoppingCart dengan:

    • Private list untuk items
    • Methods untuk add_item, remove_item, get_total
    • Property untuk item_count
  3. Implementasikan class User dengan:

    • Private password
    • Method untuk verify_password
    • Property untuk email dengan validasi format

Langkah Selanjutnya

Setelah memahami encapsulation, kita akan mempelajari Polymorphism untuk membuat objek yang berbeda merespons method yang sama dengan cara berbeda.