Dasar

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

Diperbarui: · Skillaria.Top

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

KesalahanGejalaSolusi
Lupa self di metodeTypeError: ... takes 0 positional arguments but 1 was givenParameter pertama metode selalu self
Field di __init__ tanpa self.Nilai hilang setelah __init__Tulis self.amount = amount
List sebagai atribut kelasData «mengalir» antarobjekBuat list di __init__ atau lewat default_factory
Tidak ada super().__init__()AttributeError pada field indukPanggil inisialisator induk
Properti dipanggil dengan tanda kurungTypeError: 'int' object is not callableBaca 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.

Pertanyaan umum

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.

Pelajari topik ini lewat video

Topik ini dibahas mendalam dengan praktik di kursus “Python 3.15: от новичка до профи”.

Ke kursus