# Kelas dan OOP di Python: __init__, self, metode, pewarisan, dan dataclass

URL: https://skillaria.top/id/guides/python/kelas-python-oop

OOP di Python dibangun di atas kelas: kelas mendeskripsikan field dan metode, sedangkan objek (instance) dibuat dengan memanggil kelas; __init__ mengisi field, dan self menunjuk ke objek yang sedang dipakai. Pewarisan memungkinkan memakai perilaku induk sambil mengganti sebagian metode, dan dekorator @dataclass membuat __init__, __repr__, dan __eq__ otomatis untuk kelas yang sebagian besar menyimpan data.

Kelas di Python adalah cetak biru: ia mendeskripsikan data apa yang disimpan objek dan apa yang bisa dilakukan terhadapnya. Objek (instance) adalah produk konkret yang dibuat dari cetak biru itu. OOP di Python bertumpu pada tiga hal: kelas dengan metode, pewarisan, dan metode khusus seperti `__init__` dan `__repr__`. Semuanya dibahas berurutan dengan satu contoh berjalan: pencatatan pengeluaran.

## Mengapa butuh kelas kalau sudah ada dict

Data bisa disimpan dalam dict: `{"title": "Kopi", "amount": 250}`. Masalahnya, dict tidak tahu kunci mana yang benar. Salah ketik `expense.get("amout", 0)` tidak menimbulkan error — yang kembali adalah nol, dan laporan diam-diam menjadi salah.

Pada objek kelas, field dideklarasikan sejak awal. Mengakses atribut yang tidak ada langsung gagal dengan `AttributeError`, dan Python modern bahkan menyarankan nama yang benar. Selain itu, kelas menyatukan data dan aksi terhadap data di satu tempat: pengeluaran bisa menghitung sendiri jumlah dengan pajak, dan laporan menghitung totalnya sendiri.

## Cara membuat kelas di Python

Kelas dideklarasikan dengan kata `class`, dan namanya menurut PEP 8 ditulis dengan gaya CamelCase. Contoh kelas paling sederhana:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("Kopi", 250)
taxi = Expense("Taksi", 300)
print(coffee.title, coffee.amount)   # Kopi 250
```

Pemanggilan `Expense("Kopi", 250)` membuat instance baru. `coffee` dan `taxi` adalah dua objek berbeda dari tipe yang sama, masing-masing dengan nilai field sendiri. Untuk memeriksa asal objek gunakan `isinstance(coffee, Expense)` — hasilnya `True`.

### Konstruktor __init__: inisialisasi kelas

`__init__` sering disebut konstruktor kelas, meskipun lebih tepat disebut inisialisator. Objek itu sendiri dibuat oleh metode `__new__`, sedangkan `__init__` dipanggil sesudahnya dan menempatkan argumen ke dalam field. Ia tidak perlu mengembalikan apa pun (lebih tepatnya, harus mengembalikan `None`).

Di dalam `__init__` sangat berguna untuk memeriksa input, agar program tidak pernah memiliki objek yang jelas-jelas salah:

```
class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("Jumlah tidak boleh negatif")
        self.title = title
        self.amount = amount
```

### Apa itu self di Python

`self` adalah referensi ke objek yang sedang diproses oleh metode. Saat Anda menulis `coffee.with_tax()`, Python sebenarnya memanggil `Expense.with_tax(coffee)` dan mengirim objek sebagai argumen pertama. Karena itu, `self` ditulis pertama dalam deklarasi metode, dan saat pemanggilan tidak ditulis.

`self` bukan kata kunci, melainkan kebiasaan: secara teknis parameter itu boleh dinamai bebas, tetapi tidak ada yang melakukannya. Kesalahan umum pemula adalah lupa menulis `self` di deklarasi metode. Akibatnya, pemanggilan gagal dengan `TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given`.

## Metode kelas dan metode khusus

Metode adalah fungsi yang dideklarasikan di dalam kelas. Ia melihat field objek melalui `self`:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("Kopi", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('Kopi', 250)
```

Tanpa `__repr__`, output akan tampak seperti `<__main__.Expense object at 0x7f...>` — tidak berguna untuk debugging. Metode yang diapit dua garis bawah disebut metode khusus (dunder): Python yang memanggilnya. `__str__` mengatur tampilan yang ramah pengguna, `__eq__` mengatur perbandingan dengan `==`, dan `__len__` mengatur `len(obj)`.

### @classmethod dan @staticmethod

Metode biasa bekerja dengan instance. Metode kelas dengan dekorator `@classmethod` menerima kelas itu sendiri (`cls`) sebagai argumen pertama — sering dipakai sebagai konstruktor alternatif. Metode statis `@staticmethod` tidak menerima objek maupun kelas: ia fungsi biasa yang secara logis berhubungan dengan kelas.

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("Makan siang;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False
```

### Destruktor kelas: __del__

Python punya metode `__del__` yang dipanggil sebelum objek dihancurkan oleh pengumpul sampah. Jangan mengandalkannya untuk melepaskan sumber daya: waktu pemanggilannya tidak dijamin. Untuk berkas dan koneksi, gunakan manajer konteks `with` atau metode `close()` secara eksplisit.

## Atribut kelas dan atribut instance

Field yang diberikan lewat `self.name = ...` adalah milik objek tertentu. Variabel yang dideklarasikan langsung di badan kelas adalah atribut kelas: dipakai bersama oleh semua instance. Biasanya di sinilah konstanta kelas disimpan.

```
class Expense:
    CURRENCY = "IDR"          # atribut kelas, konstanta

    def __init__(self, title, amount):
        self.title = title    # atribut instance
        self.amount = amount

a = Expense("Kopi", 250)
b = Expense("Teh", 150)
print(a.CURRENCY, Expense.CURRENCY)   # IDR IDR
```

Jebakan yang berbahaya adalah atribut kelas yang bisa diubah. Jika di badan kelas Anda menulis `rows = []` lalu menambah isinya lewat `self.rows.append(...)`, list itu akan dipakai bersama oleh semua objek. Field yang bisa diubah selalu dibuat di dalam `__init__`.

Isi field objek bisa dilihat dengan `vars(obj)` atau `obj.__dict__`, sedangkan atribut dengan nama dari string dibaca lewat `getattr(obj, "amount")`.

## Properti kelas: @property

Properti adalah metode yang dibaca dari luar seperti field biasa, tanpa tanda kurung. Ini cocok untuk nilai yang dihitung: total laporan tidak perlu disimpan terpisah dan tidak akan lupa diperbarui.

```
class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("Kopi", 250))
r.add(Expense("Taksi", 300))
print(r.total)   # 550
```

Jika penulisan nilai perlu dikontrol, tambahkan setter dengan `@total.setter`. Tanpa setter, penugasan `r.total = 0` akan memunculkan `AttributeError`.

Tentang enkapsulasi: di Python tidak ada field privat dalam arti ketat. Satu garis bawah (`_cache`) adalah kesepakatan «jangan sentuh dari luar»; dua garis bawah (`__secret`) mengaktifkan name mangling, sehingga field tidak tertimpa secara tidak sengaja oleh pewaris.

## Pewarisan kelas di Python

Pewarisan memungkinkan membuat kelas turunan dari kelas induk: turunan mendapatkan semua field dan metode, lalu bisa mengganti yang diperlukan. Kelas induk ditulis dalam tanda kurung.

```
class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # panggil __init__ induk
        self.month = month

    def add(self, expense):       # mengganti metode
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # pertahankan perilaku induk

m = MonthlyReport("Oktober")
m.add(Expense("Kopi", 250))   # [Oktober] + Expense('Kopi', 250)
print(m.total)                # 250 — properti ikut diwarisi
```

`super()` mengembalikan objek perantara untuk memanggil metode induk. Lupa menulis `super().__init__()` di `__init__` turunan adalah kesalahan klasik: field `rows` tidak dibuat, dan `add` pertama langsung gagal dengan `AttributeError`.

Hubungan pewarisan bisa diperiksa dengan `issubclass(MonthlyReport, Report)`. Semua kelas di Python 3 secara implisit mewarisi `object` — dari situlah metode dasar seperti `__repr__` dan `__eq__` berasal. Python juga mendukung pewarisan ganda (`class C(A, B)`); urutan pencarian metode terlihat di `C.__mro__`.

### Kelas abstrak di Python

Kelas abstrak menetapkan antarmuka: metode apa saja yang wajib diimplementasikan turunannya. Objek dari kelas abstrak tidak bisa dibuat. Untuk itu ada modul `abc`:

```
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...
```

Turunan yang belum mengimplementasikan semua metode abstrak juga tetap abstrak, dan saat objeknya dibuat akan menghasilkan error yang sama.

## Komposisi sebagai pengganti hierarki yang dalam

Setelah mengenal pewarisan, orang cenderung membangun tangga: transaksi dasar, transaksi berbentuk uang, transaksi dalam rupiah — lima tingkat ke bawah. Di Python ini jarang dilakukan. Pewarisan menggambarkan hubungan «adalah», sedangkan komposisi «memiliki»: laporan bukanlah daftar pengeluaran, melainkan memuatnya dalam field `rows`.

Aturan praktisnya: jika Anda hanya perlu memakai kemampuan objek lain, simpan objek itu dalam sebuah field, jangan mewarisi. Untuk data, dataclass sering sudah cukup, sementara logikanya bisa ditaruh di fungsi biasa.

## Dataclass di Python: kelas tanpa kode berulang

Untuk kelas yang sebagian besar menyimpan data, menulis `__init__`, `__repr__`, dan `__eq__` secara manual itu melelahkan. Dekorator `@dataclass` dari modul `dataclasses` membuatnya otomatis berdasarkan anotasi field:

```
from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Jumlah tidak boleh negatif")

e = Expense("Kopi", 250)
print(e)                           # Expense(title='Kopi', amount=250, tags=[])
print(e == Expense("Kopi", 250))   # True
```

Pemeriksaan nilai yang dulu ditulis di `__init__` dipindahkan ke `__post_init__` — metode ini dipanggil tepat setelah inisialisator yang dihasilkan. Nilai bawaan yang bisa diubah hanya boleh ditulis lewat `field(default_factory=list)`: penulisan `tags: list = []` ditolak oleh dataclass dengan `ValueError`.

### frozen dan slots

Parameter `frozen=True` membuat objek tidak bisa diubah, sedangkan `slots=True` (sejak Python 3.10) membuat `__slots__` dan menghemat memori. Mengubah field objek yang dibekukan memunculkan `FrozenInstanceError`. Untuk mendapatkan objek dengan nilai baru, buat salinannya:

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("Kopi", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='Kopi', amount=250) Expense(title='Kopi', amount=300)
```

Sejak Python 3.13, fungsi umum `copy.replace()` melakukan hal yang sama. Objek dataclass yang dibekukan bisa dimasukkan ke set dan dipakai sebagai kunci dict — karena ia punya `__hash__`.

## Hal baru pada kelas: Self, @override, __slots__

Untuk tipe pada kelas ada dua alat yang berguna. `typing.Self` (Python 3.11, PEP 673) berarti «akan dikembalikan objek dari kelas yang sama» — ini juga berlaku pada metode yang diwarisi. Dekorator `@override` (Python 3.12, PEP 698) menandai metode yang wajib menimpa metode induk: jika Anda salah mengetik nama, mypy atau pyright akan melaporkan bahwa tidak ada metode dasar dengan nama itu.

```
from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # bisa dipanggil berantai

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)
```

Di Python 3.15 dicabut satu batasan khusus: turunan `tuple` boleh memakai `__slots__` yang tidak kosong, dan `__dict__` serta `__weakref__` bisa dicantumkan dalam `__slots__` kelas mana pun. Di 3.14, deklarasi seperti itu pada turunan tuple menghasilkan `TypeError: nonempty __slots__ not supported for subtype of 'tuple'`.

## Kesalahan umum saat bekerja dengan kelas

| Kesalahan | Gejala | Solusi |
| --- | --- | --- |

| Lupa `self` di metode | `TypeError: ... takes 0 positional arguments but 1 was given` | Parameter pertama metode selalu `self` |

| Field di `__init__` tanpa `self.` | Nilai hilang setelah `__init__` | Tulis `self.amount = amount` |

| List sebagai atribut kelas | Data «mengalir» antarobjek | Buat list di `__init__` atau lewat `default_factory` |

| Tidak ada `super().__init__()` | `AttributeError` pada field induk | Panggil inisialisator induk |

| Properti dipanggil dengan tanda kurung | `TypeError: 'int' object is not callable` | Baca properti tanpa kurung: `r.total` |

## Ringkasan

OOP di Python dimulai dari hal sederhana: kelas mendeskripsikan field dan metode, `__init__` mengisi field, `self` menunjuk objek yang sedang dipakai. Pewarisan dan `super()` memperluas perilaku, kelas abstrak menetapkan antarmuka, dan `@property` membuat field terhitung. Untuk kelas data gunakan `@dataclass`, dan untuk hierarki yang dalam lebih baik pilih komposisi. Kelas dan dataclass dipraktikkan dalam pelajaran kursus Python dengan contoh pencatat pengeluaran.

## FAQ

**Apa bedanya kelas dan objek di Python?**

Kelas adalah deskripsi tipe: field dan metode apa saja yang dimilikinya. Objek atau instance dibuat dengan memanggil kelas, misalnya Expense("Kopi", 250), dan menyimpan nilai konkret.

**Apakah __init__ wajib ditulis di kelas?**

Tidak. Jika tidak dideklarasikan, yang dipakai adalah __init__ dari object yang tidak menerima argumen. Pada dataclass, metode ini dibuat oleh dekorator.

**Apa bedanya __str__ dan __repr__?**

__repr__ adalah representasi tidak ambigu untuk pengembang dan debugger, sedangkan __str__ adalah tampilan yang mudah dibaca pengguna lewat print dan f-string. Jika __str__ tidak ada, Python memakai __repr__.

**Kapan memilih dataclass dan kapan kelas biasa?**

Dataclass cocok jika kelas terutama menyimpan data dan Anda butuh __init__, __repr__, dan perbandingan yang sudah jadi. Kelas biasa lebih baik jika logika inisialisasinya rumit atau perilakunya lebih penting daripada datanya.

**Apakah ada field privat di Python?**

Tidak ada privasi yang ketat. Satu garis bawah di awal nama adalah kesepakatan «bukan untuk pemakaian luar», sedangkan dua garis bawah mengaktifkan name mangling yang melindungi field dari penimpaan tidak sengaja di turunan.


---
Source: Skillaria.Top (https://skillaria.top)
