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 property untuk validasi saat set atribut dan untuk computed attribute (nilai yang dihitung dari atribut lain) tanpa mengubah cara penggunaan dari luar.
  • @classmethod untuk factory constructor — cara idiomatis membuat instance dari format data yang berbeda (Pengguna.dari_string("nama:email")).
  • @staticmethod untuk utilitas yang terkait secara konseptual dengan kelas tapi tidak butuh akses ke self atau cls.
  • 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.
  • dataclass menghilangkan boilerplate __init__, __repr__, __eq__ untuk kelas data. Gunakan field(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.

← Sebelumnya: Fungsi   Berikutnya: Interface →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact