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