Kelas #
Kelas adalah cetak biru untuk membuat objek — menggabungkan data (atribut) dan perilaku (metode) ke dalam satu unit yang terstruktur. Python mendukung pemrograman berorientasi objek (OOP) secara penuh, tapi pendekatannya lebih fleksibel dibanding bahasa seperti Java: tidak ada kata kunci private atau public, enkapsulasi diimplementasikan melalui konvensi, dan Python mendukung multiple inheritance. Memahami cara kerja kelas di Python — termasuk kapan sebaiknya tidak menggunakannya — adalah kunci menulis kode yang terorganisir dengan baik.
Mendefinisikan Kelas dan Instance #
class Mahasiswa:
"""Merepresentasikan data seorang mahasiswa."""
def __init__(self, nama: str, nim: str, jurusan: str):
"""Inisialisasi atribut instance mahasiswa."""
self.nama = nama
self.nim = nim
self.jurusan = jurusan
def tampilkan_info(self) -> str:
return f"{self.nim} - {self.nama} ({self.jurusan})"
def __repr__(self) -> str:
return f"Mahasiswa(nama={self.nama!r}, nim={self.nim!r})"
# Membuat instance (objek) dari kelas
mhs1 = Mahasiswa("Budi Santoso", "2021001", "Informatika")
mhs2 = Mahasiswa("Ani Rahayu", "2021002", "Sistem Informasi")
print(mhs1.tampilkan_info()) # → 2021001 - Budi Santoso (Informatika)
print(mhs2.nama) # → Ani Rahayu
print(repr(mhs1)) # → Mahasiswa(nama='Budi Santoso', nim='2021001')
self adalah referensi ke instance saat ini — Python meneruskannya secara otomatis saat memanggil metode instance. Nama self adalah konvensi, bukan keyword — tapi jangan ganti namanya tanpa alasan kuat.
Atribut Instance vs Atribut Kelas #
Perbedaan penting yang sering diabaikan: atribut instance dimiliki oleh setiap objek secara terpisah, sementara atribut kelas dishare oleh semua instance.
class AkunBank:
# Atribut kelas — dishare semua instance
SUKU_BUNGA = 0.05
jumlah_akun = 0
def __init__(self, pemilik: str, saldo_awal: float = 0):
# Atribut instance — unik per objek
self.pemilik = pemilik
self.saldo = saldo_awal
AkunBank.jumlah_akun += 1 # akses atribut kelas via nama kelas
def tambah_bunga(self):
self.saldo += self.saldo * AkunBank.SUKU_BUNGA
akun1 = AkunBank("Budi", 1_000_000)
akun2 = AkunBank("Ani", 2_500_000)
print(AkunBank.jumlah_akun) # → 2 (dishare semua instance)
print(akun1.jumlah_akun) # → 2 (bisa diakses via instance)
print(akun1.saldo) # → 1000000 (unik per instance)
print(akun2.saldo) # → 2500000 (unik per instance)
# ANTI-PATTERN: atribut kelas mutable — jebakan yang sering diabaikan
class Tim:
anggota = [] # ← BERBAHAYA! list ini dishare semua instance
def tambah(self, nama):
self.anggota.append(nama)
tim1 = Tim()
tim2 = Tim()
tim1.tambah("Budi")
print(tim2.anggota) # → ['Budi'] ← tim2 ikut terpengaruh!
# BENAR: inisialisasi mutable di __init__
class Tim:
def __init__(self):
self.anggota = [] # list baru untuk setiap instance
def tambah(self, nama):
self.anggota.append(nama)
Enkapsulasi dan property
#
Python tidak memiliki private sungguhan, melainkan menggunakan konvensi prefix underscore dan mekanisme property untuk mengontrol akses atribut.
class Suhu:
"""Konversi suhu dengan validasi via property."""
def __init__(self, celsius: float):
self._celsius = celsius # _prefix = konvensi "protected"
@property
def celsius(self) -> float:
"""Getter — akses via suhu.celsius"""
return self._celsius
@celsius.setter
def celsius(self, nilai: float) -> None:
"""Setter dengan validasi — suhu.celsius = 25"""
if nilai < -273.15:
raise ValueError(f"Suhu {nilai}°C di bawah nol mutlak (-273.15°C)")
self._celsius = nilai
@property
def fahrenheit(self) -> float:
"""Computed property — hanya getter, tidak ada setter"""
return self._celsius * 9/5 + 32
@property
def kelvin(self) -> float:
return self._celsius + 273.15
s = Suhu(100)
print(s.celsius) # → 100 (akses seperti atribut biasa)
print(s.fahrenheit) # → 212.0 (dihitung otomatis)
print(s.kelvin) # → 373.15
s.celsius = 0 # setter dipanggil, validasi berjalan
print(s.fahrenheit) # → 32.0
s.celsius = -300 # → ValueError: Suhu -300°C di bawah nol mutlak
# Prefix dua underscore — name mangling (bukan benar-benar private)
class AkunRahasia:
def __init__(self, pin: str):
self.__pin = pin # → diubah menjadi _AkunRahasia__pin
def verifikasi(self, pin_input: str) -> bool:
return self.__pin == pin_input
akun = AkunRahasia("1234")
print(akun.verifikasi("1234")) # → True
# print(akun.__pin) # AttributeError — tidak bisa akses langsung
print(akun._AkunRahasia__pin) # → 1234 (masih bisa jika tahu name mangling)
Metode Kelas dan Metode Statis #
class Pengguna:
_registry: list = []
def __init__(self, nama: str, email: str):
self.nama = nama
self.email = email
Pengguna._registry.append(self)
# Instance method — akses self (data instance)
def sapa(self) -> str:
return f"Halo, saya {self.nama}"
# Class method — akses cls (kelas itu sendiri), bukan instance
@classmethod
def dari_string(cls, data: str) -> "Pengguna":
"""Factory method — buat instance dari format 'nama:email'."""
nama, email = data.split(":")
return cls(nama.strip(), email.strip())
@classmethod
def jumlah_pengguna(cls) -> int:
return len(cls._registry)
# Static method — tidak akses self atau cls
@staticmethod
def validasi_email(email: str) -> bool:
"""Validasi sederhana format email."""
return "@" in email and "." in email.split("@")[-1]
# Instance method
p1 = Pengguna("Budi", "[email protected]")
print(p1.sapa()) # → Halo, saya Budi
# Class method sebagai factory
p2 = Pengguna.dari_string("Ani : [email protected]")
print(p2.nama) # → Ani
# Class method untuk data kelas
print(Pengguna.jumlah_pengguna()) # → 2
# Static method — tidak butuh instance atau kelas
print(Pengguna.validasi_email("[email protected]")) # → True
print(Pengguna.validasi_email("bukan-email")) # → False
Kapan menggunakan masing-masing:
instance method → perlu akses/modifikasi data instance (self)
class method → perlu akses data kelas, atau sebagai factory constructor
static method → fungsi yang terkait secara konseptual dengan kelas
tapi tidak butuh self maupun cls
Pewarisan (Inheritance) #
Pewarisan memungkinkan kelas baru mewarisi atribut dan metode dari kelas induk, lalu menambah atau mengubah perilakunya.
class Hewan:
"""Kelas dasar untuk semua hewan."""
def __init__(self, nama: str, umur: int):
self.nama = nama
self.umur = umur
def bersuara(self) -> str:
raise NotImplementedError("Subkelas harus mengimplementasikan bersuara()")
def info(self) -> str:
return f"{self.nama} ({self.umur} tahun): {self.bersuara()}"
class Anjing(Hewan):
"""Subkelas Hewan — khusus untuk anjing."""
def __init__(self, nama: str, umur: int, ras: str):
super().__init__(nama, umur) # panggil __init__ kelas induk
self.ras = ras
def bersuara(self) -> str:
return "Guk guk!"
def info(self) -> str:
return f"{super().info()} [Ras: {self.ras}]"
class Kucing(Hewan):
def bersuara(self) -> str:
return "Meow!"
# Penggunaan
anjing = Anjing("Rex", 3, "Golden Retriever")
kucing = Kucing("Mimi", 5)
print(anjing.info()) # → Rex (3 tahun): Guk guk! [Ras: Golden Retriever]
print(kucing.info()) # → Mimi (5 tahun): Meow!
# isinstance() untuk cek tipe termasuk pewarisan
print(isinstance(anjing, Anjing)) # → True
print(isinstance(anjing, Hewan)) # → True (Anjing adalah Hewan)
print(isinstance(anjing, Kucing)) # → False
super() — Memanggil Metode Kelas Induk
#
class Karyawan:
def __init__(self, nama: str, gaji: float):
self.nama = nama
self.gaji = gaji
def hitung_bonus(self) -> float:
return self.gaji * 0.10
class Manager(Karyawan):
def __init__(self, nama: str, gaji: float, tim_size: int):
super().__init__(nama, gaji) # ← panggil __init__ Karyawan
self.tim_size = tim_size
def hitung_bonus(self) -> float:
# Bonus manager = bonus dasar + bonus per anggota tim
bonus_dasar = super().hitung_bonus() # ← panggil hitung_bonus Karyawan
return bonus_dasar + (self.tim_size * 500_000)
mgr = Manager("Budi", 15_000_000, 5)
print(mgr.hitung_bonus()) # → 1_500_000 + 2_500_000 = 4_000_000
Multiple Inheritance dan MRO #
Python mendukung multiple inheritance — sebuah kelas bisa mewarisi dari lebih dari satu kelas induk. Urutan pencarian metode ditentukan oleh MRO (Method Resolution Order) menggunakan algoritma C3 linearization.
Setiap kali Anda mengakses atribut atau metode dari suatu objek (misalnya obj.nama), Python tidak langsung mencari di satu tempat. Python mengikuti jalur pencarian resolusi atribut yang terdefinisi dengan ketat melalui kamus namespace internal (__dict__) dan urutan kelas dalam MRO.
Perhatikan diagram alur resolusi pencarian atribut berikut:
flowchart TD
Start["Mulai Pencarian Atribut: obj.name"] --> CheckInstance{"Apakah 'name' ada di obj.__dict__?"}
CheckInstance -->|Ya| CheckDescriptorInstance{"Apakah itu Data Descriptor (misal: property)?"}
CheckDescriptorInstance -->|Ya| CallDescriptor["Panggil getter Descriptor"]
CheckDescriptorInstance -->|Tidak| ReturnInstance["Kembalikan Nilai dari obj.__dict__"]
CheckInstance -->|Tidak| LookupMRO["Cari 'name' di Kelas & Base Classes (MRO)"]
LookupMRO --> CheckClass{"Ditemukan di MRO?"}
CheckClass -->|Ya| CheckDescriptorClass{"Apakah itu Descriptor?"}
CheckDescriptorClass -->|Ya| CallDescriptorClass["Panggil getter Descriptor"]
CheckDescriptorClass -->|Tidak| ReturnClass["Kembalikan Nilai dari class.__dict__"]
CheckClass -->|Tidak| CheckGetAttr{"Apakah __getattr__ didefinisikan?"}
CheckGetAttr -->|Ya| CallGetAttr["Panggil obj.__getattr__('name')"]
CheckGetAttr -->|Tidak| RaiseError["Lempar AttributeError"]
Melalui alur ini, Python dapat mendukung fitur dinamis seperti penimpaan (overriding), properti (property), metode khusus, serta pencarian fallback dinamis menggunakan metode __getattr__.
Berikut adalah contoh multiple inheritance beserta MRO-nya:
class Terbang:
def bergerak(self) -> str:
return "terbang"
class Berenang:
def bergerak(self) -> str:
return "berenang"
class Bebek(Terbang, Berenang):
"""Bebek bisa terbang DAN berenang."""
pass
bebek = Bebek()
print(bebek.bergerak()) # → "terbang" (Terbang lebih dulu dalam MRO)
# Lihat urutan MRO
print(Bebek.__mro__)
# → (<class 'Bebek'>, <class 'Terbang'>, <class 'Berenang'>, <class 'object'>)
# Mixin — pola umum multiple inheritance yang bersih
class JSONMixin:
"""Tambahkan kemampuan serialisasi JSON ke kelas apapun."""
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class LogMixin:
"""Tambahkan kemampuan logging ke kelas apapun."""
def log(self, pesan: str) -> None:
print(f"[{self.__class__.__name__}] {pesan}")
class Produk(JSONMixin, LogMixin):
def __init__(self, nama: str, harga: float):
self.nama = nama
self.harga = harga
p = Produk("Laptop", 15_000_000)
print(p.to_json()) # → {"nama": "Laptop", "harga": 15000000.0}
p.log("Produk dibuat") # → [Produk] Produk dibuat
Magic Methods (Dunder Methods) #
Magic methods memungkinkan objek berperilaku seperti tipe bawaan Python — mendukung operator, representasi string, iterasi, dan lainnya.
class Vektor:
"""Vektor 2D dengan dukungan operator matematika."""
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
"""Representasi debug — dipanggil oleh repr() dan di REPL."""
return f"Vektor({self.x}, {self.y})"
def __str__(self) -> str:
"""Representasi human-readable — dipanggil oleh str() dan print()."""
return f"({self.x}, {self.y})"
def __add__(self, other: "Vektor") -> "Vektor":
"""Mendukung operator +"""
return Vektor(self.x + other.x, self.y + other.y)
def __sub__(self, other: "Vektor") -> "Vektor":
"""Mendukung operator -"""
return Vektor(self.x - other.x, self.y - other.y)
def __mul__(self, skalar: float) -> "Vektor":
"""Mendukung operator * dengan skalar"""
return Vektor(self.x * skalar, self.y * skalar)
def __eq__(self, other: object) -> bool:
"""Mendukung operator =="""
if not isinstance(other, Vektor):
return NotImplemented
return self.x == other.x and self.y == other.y
def __abs__(self) -> float:
"""Mendukung abs() — panjang vektor"""
return (self.x ** 2 + self.y ** 2) ** 0.5
def __len__(self) -> int:
"""Mendukung len() — dimensi vektor"""
return 2
v1 = Vektor(3, 4)
v2 = Vektor(1, 2)
print(v1) # → (3, 4) __str__
print(repr(v1)) # → Vektor(3, 4) __repr__
print(v1 + v2) # → (4, 6) __add__
print(v1 - v2) # → (2, 2) __sub__
print(v1 * 2) # → (6, 8) __mul__
print(v1 == v2) # → False __eq__
print(abs(v1)) # → 5.0 __abs__
print(len(v1)) # → 2 __len__
Magic methods yang paling sering digunakan:
__init__ → konstruktor
__repr__ → representasi debug (gunakan selalu)
__str__ → representasi human-readable
__eq__ → operator ==
__lt__ → operator < (aktifkan sorting)
__hash__ → hashing (wajib jika override __eq__)
__len__ → len()
__contains__ → operator 'in'
__iter__ → membuat objek iterable
__getitem__ → akses via []
__enter__ / __exit__ → context manager (with statement)
dataclass — Kelas Data Tanpa Boilerplate
#
dataclass (Python 3.7+) menghasilkan __init__, __repr__, dan __eq__ secara otomatis berdasarkan anotasi tipe — menghilangkan kode boilerplate yang berulang:
from dataclasses import dataclass, field
from typing import List
# ANTI-PATTERN: kelas data dengan banyak boilerplate manual
class ProdukManual:
def __init__(self, nama: str, harga: float, stok: int = 0):
self.nama = nama
self.harga = harga
self.stok = stok
def __repr__(self):
return f"Produk(nama={self.nama!r}, harga={self.harga}, stok={self.stok})"
def __eq__(self, other):
return (self.nama, self.harga, self.stok) == (other.nama, other.harga, other.stok)
# BENAR: dataclass — jauh lebih ringkas
@dataclass
class Produk:
nama: str
harga: float
stok: int = 0
tag: List[str] = field(default_factory=list) # mutable default via field()
def diskon(self, persen: float) -> float:
return self.harga * (1 - persen / 100)
p1 = Produk("Laptop", 15_000_000, stok=10)
p2 = Produk("Laptop", 15_000_000, stok=10)
p3 = Produk("Mouse", 250_000)
print(p1) # → Produk(nama='Laptop', harga=15000000, stok=10, tag=[])
print(p1 == p2) # → True (__eq__ otomatis)
print(p1 == p3) # → False
print(p1.diskon(10)) # → 13500000.0
# frozen=True — buat dataclass immutable (seperti namedtuple tapi lebih kuat)
@dataclass(frozen=True)
class Koordinat:
lat: float
lon: float
k = Koordinat(-6.2, 106.8)
# k.lat = 0 # FrozenInstanceError — tidak bisa diubah
Komposisi vs Pewarisan #
Pewarisan yang berlebihan adalah salah satu penyebab kode yang sulit dipelihara. Sering kali komposisi (menyimpan objek lain sebagai atribut) adalah pilihan yang lebih baik.
# Gunakan pewarisan jika:
# ✓ Hubungan IS-A yang jelas: Anjing IS-A Hewan
# ✓ Subkelas perlu override atau extend perilaku kelas induk
# ✓ Kamu perlu polimorfisme (fungsi yang bekerja pada semua subkelas)
# Gunakan komposisi jika:
# ✓ Hubungan HAS-A: Mobil HAS-A Mesin
# ✓ Ingin menggabungkan perilaku dari beberapa sumber
# ✓ Relasi bisa berubah saat runtime
# Contoh komposisi
class Mesin:
def __init__(self, cc: int, tenaga: int):
self.cc = cc
self.tenaga = tenaga
def info(self) -> str:
return f"{self.cc}cc, {self.tenaga}hp"
class Transmisi:
def __init__(self, tipe: str, gigi: int):
self.tipe = tipe
self.gigi = gigi
class Mobil:
def __init__(self, merek: str, mesin: Mesin, transmisi: Transmisi):
self.merek = merek
self.mesin = mesin # HAS-A Mesin
self.transmisi = transmisi # HAS-A Transmisi
def spesifikasi(self) -> str:
return (
f"{self.merek}: "
f"Mesin {self.mesin.info()}, "
f"Transmisi {self.transmisi.tipe} {self.transmisi.gigi} gigi"
)
mesin_v6 = Mesin(3500, 280)
transmisi_otomatis = Transmisi("Otomatis", 8)
mobil = Mobil("Toyota Camry", mesin_v6, transmisi_otomatis)
print(mobil.spesifikasi())
# → Toyota Camry: Mesin 3500cc, 280hp, Transmisi Otomatis 8 gigi
Ringkasan #
- Atribut mutable jangan di level kelas — list dan dict sebagai atribut kelas dishare semua instance dan menimbulkan bug tersembunyi. Inisialisasikan di
__init__.- Gunakan
propertyuntuk validasi saat set atribut dan untuk computed attribute (nilai yang dihitung dari atribut lain) tanpa mengubah cara penggunaan dari luar.@classmethoduntuk factory constructor — cara idiomatis membuat instance dari format data yang berbeda (Pengguna.dari_string("nama:email")).@staticmethoduntuk utilitas yang terkait secara konseptual dengan kelas tapi tidak butuh akses keselfataucls.- Selalu panggil
super().__init__()di__init__subkelas agar atribut kelas induk terinisialisasi dengan benar.- Selalu implementasikan
__repr__— sangat membantu saat debugging dan di REPL interaktif.dataclassmenghilangkan boilerplate__init__,__repr__,__eq__untuk kelas data. Gunakanfield(default_factory=list)untuk atribut mutable.- Komposisi (HAS-A) sering lebih baik dari pewarisan (IS-A) — gunakan pewarisan hanya jika ada hubungan IS-A yang jelas dan polimorfisme dibutuhkan.