Bab 16 — OOP: Membuat Cetakan untuk Data Lapangan
Seri I1 — Kode yang Rapi dan Terawat
Semua skrip bab ini ada di folder kode/bab16/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Di Seri B, data lapangan Anda tersimpan dalam dict dan list, dan perhitungannya ada di fungsi-fungsi terpisah. Itu berhasil, tetapi semakin besar programnya, semakin sulit menjawab pertanyaan sederhana: fungsi mana yang cocok untuk data yang mana? Fungsi biomassa_kg hanya masuk akal untuk kolam, tetapi tidak ada yang mencegah Anda memberinya data sapi.
Pemrograman berorientasi objek (object-oriented programming, disingkat OOP) menyatukan data dan fungsi yang cocok untuknya dalam satu paket. Analoginya cetakan kue: class adalah cetakannya, objek adalah kue yang dicetak. Satu cetakan bisa menghasilkan banyak kue, dan tiap kue punya isi sendiri.

Class dan objek
01_kelas_pohon.py
# Bab 16 - Class dan objek: cetakan dan hasil cetakannya
import math
class Pohon:
"""Cetakan untuk satu pohon di plot."""
def __init__(self, jenis, diameter_cm, tinggi_m):
self.jenis = jenis
self.diameter_cm = diameter_cm
self.tinggi_m = tinggi_m
def luas_penampang_cm2(self):
return math.pi * (self.diameter_cm / 2) ** 2
def kategori(self):
if self.diameter_cm >= 40:
return "besar"
if self.diameter_cm >= 30:
return "sedang"
return "kecil"
jati = Pohon("Jati", 32.5, 18.0)
meranti = Pohon("Meranti", 45.2, 27.5)
print(jati.jenis, jati.diameter_cm, jati.kategori())
print(meranti.jenis, meranti.diameter_cm, meranti.kategori())
print(f"Luas penampang jati: {jati.luas_penampang_cm2():.1f} cm2")
print(type(jati))Jati 32.5 sedang
Meranti 45.2 besar
Luas penampang jati: 829.6 cm2
<class '__main__.Pohon'>
class Pohon:membuat cetakan. Nama kelas ditulis dengan huruf besar di awal tiap kata (PlotHutan).__init__adalah metode khusus yang berjalan otomatis saat objek dibuat. Di sinilah data awal dipasang. (Dibaca “dunder init”: double underscore, dua garis bawah di tiap sisi.)selfadalah objek yang sedang dikerjakan.self.jenis = jenisartinya “simpan nilai ini di dalam objek ini”.- Fungsi di dalam kelas disebut metode (method).
jati.kategori()memanggil metode untuk objekjati. Python mengisiselfsendiri. Pohon("Jati", 32.5, 18.0)membuat satu objek (juga disebut instance). Luas penampang 829,6 cm² dihitung dari diameter 32,5 cm.
Hasil type(jati) menunjukkan bahwa jati memang objek kelas Pohon.
Tiap objek punya data sendiri
02_objek_mandiri.py
# Bab 16 - Setiap objek punya data sendiri
class Kolam:
jenis_unit = "Kolam" # milik class: sama untuk semua objek
def __init__(self, nama, jumlah_ikan):
self.nama = nama # milik objek: beda-beda
self.jumlah_ikan = jumlah_ikan
def tebar(self, tambahan):
self.jumlah_ikan = self.jumlah_ikan + tambahan
kolam_a = Kolam("A", 800)
kolam_b = Kolam("B", 500)
kolam_a.tebar(200)
print(kolam_a.nama, kolam_a.jumlah_ikan)
print(kolam_b.nama, kolam_b.jumlah_ikan)
print(kolam_a.jenis_unit, kolam_b.jenis_unit)
print(isinstance(kolam_a, Kolam), isinstance(kolam_a, str))A 1000
B 500
Kolam Kolam
True False
Kolam A diisi 800 lalu ditambah 200 ikan, jadi 1000. Kolam B tidak berubah: tetap 500. Dua objek dari cetakan yang sama tidak saling mengganggu. Perhatikan juga jenis_unit: ia ditulis di tingkat kelas, jadi dimiliki bersama oleh semua objek.
Jebakan paling sering saat pertama membuat metode: lupa menulis self.
03_lupa_self.py
# Bab 16 - Jebakan: lupa menulis self
class Pohon:
def __init__(self, jenis):
self.jenis = jenis
def sapa(): # lupa self!
print("Halo, saya pohon")
jati = Pohon("Jati")
jati.sapa()Traceback (most recent call last):
File "03_lupa_self.py", line 11, in <module>
jati.sapa()
~~~~~~~~~^^
TypeError: Pohon.sapa() takes 0 positional arguments but 1 was given
Pesannya membingungkan: “takes 0 positional arguments but 1 was given” (menerima 0 argumen tetapi diberi 1). Argumen yang dimaksud adalah objek itu sendiri, yang Python kirim otomatis. Tanpa self di daftar parameter, tidak ada tempat untuk menerimanya.
Inheritance: anak mewarisi sifat induk
Plot hutan, kebun, kolam, dan kandang sama-sama punya nama dan luas. Menulis ulang itu di tiap kelas itu boros. Solusinya inheritance: buat kelas induk berisi yang sama, lalu kelas anak yang mewarisinya.
04_pewarisan.py
# Bab 16 - Inheritance: anak mewarisi sifat induk
class Unit:
"""Induk: semua unit punya nama dan luas."""
def __init__(self, nama, luas_ha):
self.nama = nama
self.luas_ha = luas_ha
def info(self):
return f"{self.nama} ({self.luas_ha} ha)"
class PlotHutan(Unit):
def __init__(self, nama, luas_ha, tegakan):
super().__init__(nama, luas_ha) # urus bagian induk dulu
self.tegakan = tegakan # lalu bagian khusus anak
class Kebun(Unit):
pass # tidak menambah apa-apa
plot = PlotHutan("Plot A", 2.5, "Jati")
kebun = Kebun("Kebun Kopi", 1.2)
print(plot.info(), "| tegakan:", plot.tegakan)
print(kebun.info())
print(isinstance(plot, Unit), issubclass(Kebun, Unit), isinstance(kebun, PlotHutan))Plot A (2.5 ha) | tegakan: Jati
Kebun Kopi (1.2 ha)
True True False
class PlotHutan(Unit):artinya “PlotHutanadalah anak dariUnit”.super().__init__(...)memanggil__init__milik induk, supaya bagian induk ikut dikerjakan. Anak lalu menambah bagiannya sendiri (tegakan).Kebuntidak menambah apa-apa, tetapi tetap punyanama,luas_ha, daninfo()dariUnit.isinstance(plot, Unit)bernilaiTrue: sebuah plot hutan adalah sebuah unit. Sebaliknya, kebun bukan plot hutan (False).
Polymorphism: satu perintah, banyak cara
Kata ini terdengar menakutkan, padahal idenya sederhana: anak boleh mengerjakan perintah yang sama dengan cara berbeda. Anda cukup bilang ringkasan(), dan tiap unit tahu sendiri cara menghitungnya.
05_polimorfisme.py
# Bab 16 - Polymorphism: satu perintah, banyak cara
class Unit:
def __init__(self, nama):
self.nama = nama
def hasil(self):
raise NotImplementedError("anak harus mengisi hasil()")
def ringkasan(self):
return f"{self.nama}: {self.hasil()}"
class Kolam(Unit):
def __init__(self, nama, jumlah, bobot_g):
super().__init__(nama)
self.jumlah, self.bobot_g = jumlah, bobot_g
def hasil(self):
return f"biomassa {self.jumlah * self.bobot_g / 1000:.1f} kg"
class Kandang(Unit):
def __init__(self, nama, bobot_sapi_kg):
super().__init__(nama)
self.bobot_sapi_kg = bobot_sapi_kg
def hasil(self):
return f"total bobot {sum(self.bobot_sapi_kg):.1f} kg"
daftar = [Kolam("Kolam A", 800, 540.5), Kandang("Kandang 1", [350, 412.5, 389.0])]
for unit in daftar:
print(unit.ringkasan()) # sama perintahnya, hasilnya menyesuaikan
try:
Unit("Polos").ringkasan()
except NotImplementedError as e:
print("Induk saja tidak cukup:", e)Kolam A: biomassa 432.4 kg
Kandang 1: total bobot 1151.5 kg
Induk saja tidak cukup: anak harus mengisi hasil()
Perulangan for memanggil unit.ringkasan() tanpa peduli apakah isinya kolam atau kandang. Hitungan kolam (800 × 540,5 g = 432,4 kg) dan kandang (350 + 412,5 + 389,0 = 1151,5 kg) berbeda, tetapi pemanggilnya sama. Induk Unit menyerahkan hasil() kepada anak dengan raise NotImplementedError: “bagian ini wajib diisi anak”. Itu sebabnya Unit("Polos") gagal dengan pesan yang jelas.
Encapsulation: menjaga data
Encapsulation artinya data dijaga di dalam objek, dan akses dari luar lewat pintu yang punya penjaga. Di Python, awalan garis bawah (_ph_air) adalah kesepakatan: “ini urusan dalam, jangan disentuh dari luar”. Penjaga pintunya adalah property.
06_enkapsulasi.py
# Bab 16 - Encapsulation: data dijaga di dalam objek
class Kolam:
def __init__(self, nama, ph_air):
self.nama = nama
self._ph_air = None # awalan _ : "jangan disentuh dari luar"
self.ph_air = ph_air # lewat property, sehingga ikut diperiksa
@property
def ph_air(self):
return self._ph_air
@ph_air.setter
def ph_air(self, nilai):
if not 0 <= nilai <= 14:
raise ValueError(f"pH harus 0 sampai 14, bukan {nilai}")
self._ph_air = nilai
kolam = Kolam("A", 7.2)
print("pH awal:", kolam.ph_air)
kolam.ph_air = 6.8
print("pH baru:", kolam.ph_air)
try:
kolam.ph_air = 17
except ValueError as e:
print("Ditolak:", e)
print("pH tetap:", kolam.ph_air)pH awal: 7.2
pH baru: 6.8
Ditolak: pH harus 0 sampai 14, bukan 17
pH tetap: 6.8
Dari luar, kolam.ph_air terlihat seperti variabel biasa. Namun tiap kali nilainya diubah, Python menjalankan pemeriksaan di setter. pH 17 ditolak dengan ValueError, dan nilai lama (6,8) tetap aman.
Python tidak benar-benar mengunci data. Awalan _ hanya kesepakatan antar-programmer. Kekuatan property ada pada pemeriksaan: data yang salah tidak bisa masuk lewat pintu resmi.
Dunder method: mengajari objek bergaul
Metode ber-garis-bawah-ganda seperti __init__ disebut dunder method. Python memanggilnya otomatis pada saat tertentu. Dengan mengisinya, objek Anda ikut bekerja dengan print, ==, sorted, dan len.
07_dunder.py
# Bab 16 - Dunder method: mengajari objek bergaul dengan Python
class Pohon:
def __init__(self, jenis, diameter_cm):
self.jenis = jenis
self.diameter_cm = diameter_cm
def __repr__(self): # tampilan "teknis"
return f"Pohon({self.jenis!r}, {self.diameter_cm})"
def __str__(self): # tampilan untuk manusia
return f"{self.jenis} {self.diameter_cm} cm"
def __eq__(self, lain):
return (self.jenis, self.diameter_cm) == (lain.jenis, lain.diameter_cm)
def __lt__(self, lain): # supaya bisa diurutkan
return self.diameter_cm < lain.diameter_cm
class Plot:
def __init__(self, pohon):
self.pohon = list(pohon)
def __len__(self):
return len(self.pohon)
a, b, c = Pohon("Jati", 32.5), Pohon("Meranti", 45.2), Pohon("Mahoni", 28.0)
print(a)
print(repr(a))
print(a == Pohon("Jati", 32.5), a == b)
print(sorted([a, b, c]))
print(len(Plot([a, b, c])), "pohon")Jati 32.5 cm
Pohon('Jati', 32.5)
True False
[Pohon('Mahoni', 28.0), Pohon('Jati', 32.5), Pohon('Meranti', 45.2)]
3 pohon
| Metode | Dipanggil ketika | Contoh |
|---|---|---|
__str__ |
print(objek) |
tampilan untuk pembaca |
__repr__ |
repr(objek), tampilan di daftar |
tampilan untuk programmer |
__eq__ |
a == b |
membandingkan isi, bukan alamat |
__lt__ |
a < b, sorted(...) |
mengurutkan menurut diameter |
__len__ |
len(objek) |
jumlah isi |
Hasil sorted berurutan dari diameter 28,0 ke 32,5 ke 45,2 karena __lt__ membandingkan diameter.
dataclass: kelas data tanpa banyak tulisan
Kelas yang isinya hanya data (seperti Pohon) memaksa kita menulis __init__, __repr__, dan __eq__ berulang-ulang. Modul bawaan dataclasses membuatkannya otomatis dari daftar kolom bertipe.
08_dataclass.py
# Bab 16 - dataclass: kelas data tanpa banyak tulisan
from dataclasses import dataclass, field
@dataclass(order=True)
class Pohon:
jenis: str
diameter_cm: float
sehat: bool = True
@dataclass
class Plot:
nama: str
pohon: list = field(default_factory=list) # list baru untuk tiap plot
a = Pohon("Jati", 32.5)
b = Pohon("Mahoni", 28.0, sehat=False)
print(a) # __repr__ dibuatkan otomatis
print(a == Pohon("Jati", 32.5)) # __eq__ dibuatkan otomatis
print(a < b) # order=True: dibandingkan berurutan per kolom
plot_a, plot_b = Plot("A"), Plot("B")
plot_a.pohon.append(a)
print(len(plot_a.pohon), len(plot_b.pohon))Pohon(jenis='Jati', diameter_cm=32.5, sehat=True)
True
True
1 0
- Tulis kolom dengan petunjuk tipe (
jenis: str). Nilai bawaan dipasang dengan=(sehat: bool = True). @dataclassmembuatkan__init__,__repr__, dan__eq__. Opsiorder=Truemenambah pembanding urutan. Pohon “Jati” lebih kecil daripada “Mahoni” karena kolom pertama (jenis) dibandingkan dulu secara abjad.- Untuk nilai bawaan berupa list, pakai
field(default_factory=list)agar tiap objek mendapat list baru. Hasil akhir1 0membuktikan plot A dan B tidak berbagi satu list.
Kebiasaan industri adalah memberi nama kelas, fungsi, dan variabel dalam bahasa Inggris. Di buku ini versi Indonesia memakai nama Indonesia supaya mudah dibaca, tetapi di tim sungguhan, ikuti kesepakatan tim Anda.
Proyek Bersambung I1: data lapangan menjadi kelas
Sekarang semua bahan bekerja bersama. Empat dataclass menyimpan data (Pohon, Tanaman, Ikan, Ternak), induk Unit menyimpan yang sama bagi semua unit, dan lima anak (PlotHutan, Kebun, Sawah, Kolam, Kandang) mengisi hasil() dengan caranya sendiri. Sawah adalah anak dari Kebun: sawah itu kebun yang juga dihitung per hektare.
09_proyek_unit.py
# Bab 16 - Proyek bersambung I1: data lapangan menjadi kelas
from dataclasses import dataclass
@dataclass
class Pohon:
jenis: str
diameter_cm: float
tinggi_m: float
sehat: bool = True
@dataclass
class Tanaman:
komoditas: str
blok: str
umur_hari: int
hasil_kg: float
@dataclass
class Ikan:
jenis: str
jumlah: int
bobot_rata_g: float
ph_air: float
def biomassa_kg(self):
return self.jumlah * self.bobot_rata_g / 1000
@dataclass
class Ternak:
jenis: str
umur_bulan: int
bobot_kg: float
class Unit:
"""Induk semua unit lahan atau usaha."""
jenis_unit = "Unit"
satuan = ""
def __init__(self, nama, luas_ha):
if luas_ha <= 0:
raise ValueError("luas_ha harus lebih dari 0")
self.nama = nama
self.luas_ha = luas_ha
self._isi = []
def tambah(self, barang):
self._isi.append(barang)
@property
def isi(self):
return tuple(self._isi)
def hasil(self):
raise NotImplementedError
def ringkasan(self):
return (f"{self.jenis_unit} {self.nama}: {len(self._isi)} catatan, "
f"{self.hasil():.1f} {self.satuan}")
def __len__(self):
return len(self._isi)
class PlotHutan(Unit):
jenis_unit, satuan = "Plot hutan", "pohon sehat"
def hasil(self):
return sum(1 for p in self._isi if p.sehat)
class Kebun(Unit):
jenis_unit, satuan = "Kebun", "kg"
def hasil(self):
return sum(t.hasil_kg for t in self._isi)
class Sawah(Kebun): # sawah = kebun yang juga dihitung per hektare
jenis_unit = "Sawah"
def produktivitas(self):
return self.hasil() / self.luas_ha
def ringkasan(self):
return super().ringkasan() + f" ({self.produktivitas():.0f} kg/ha)"
class Kolam(Unit):
jenis_unit, satuan = "Kolam", "kg biomassa"
def hasil(self):
return sum(i.biomassa_kg() for i in self._isi)
class Kandang(Unit):
jenis_unit, satuan = "Kandang", "kg bobot"
def hasil(self):
return sum(t.bobot_kg for t in self._isi)
plot = PlotHutan("A", 2.5)
for p in [Pohon("Jati", 32.5, 18.0), Pohon("Meranti", 45.2, 27.5),
Pohon("Mahoni", 28.0, 15.0, sehat=False)]:
plot.tambah(p)
kebun = Kebun("Kopi", 1.2)
for blok, kg in [("A", 245.5), ("B", 310.0)]:
kebun.tambah(Tanaman("kopi", blok, 400, kg))
sawah = Sawah("Hilir", 0.5)
sawah.tambah(Tanaman("padi", "1", 110, 1250.0))
kolam = Kolam("A", 0.04)
kolam.tambah(Ikan("lele", 800, 540.5, 7.2))
kandang = Kandang("1", 0.1)
for jenis, bulan, kg in [("sapi", 30, 350), ("sapi", 36, 412.5), ("sapi", 33, 389.0)]:
kandang.tambah(Ternak(jenis, bulan, kg))
for unit in [plot, kebun, sawah, kolam, kandang]:
print(unit.ringkasan())Plot hutan A: 3 catatan, 2.0 pohon sehat
Kebun Kopi: 2 catatan, 555.5 kg
Sawah Hilir: 1 catatan, 1250.0 kg (2500 kg/ha)
Kolam A: 1 catatan, 432.4 kg biomassa
Kandang 1: 3 catatan, 1151.5 kg bobot
Telusuri angkanya: plot A punya 3 pohon, 1 tidak sehat, sehingga 2 pohon sehat. Kebun kopi menjumlahkan 245,5 + 310,0 = 555,5 kg. Sawah 1250 kg di lahan 0,5 ha berarti 2500 kg/ha. Kolam 432,4 kg dan kandang 1151,5 kg sama dengan hitungan Seri B. Angka-angkanya fiktif.
Perhatikan bahwa daftar [plot, kebun, sawah, kolam, kandang] berisi lima jenis objek, tetapi satu perulangan dan satu perintah ringkasan() menangani semuanya. Menambah jenis unit baru (misalnya Pembenihan) cukup menulis satu anak baru. Bab-bab berikut memasang proyek ini di environment sendiri, mengujinya, merapikannya, dan menyimpannya di git.
Latihan
- Tambahkan metode
info_lengkap()pada kelasPohondi01_kelas_pohon.pyyang mengembalikan teks seperti"Jati, 32.5 cm, kategori sedang". - Buat kelas
Kebunsendiri (bukan turunan) dengannamadan daftarpanen_kg, lalu isi__len__dan__str__. Cetak satu objek dan panggillen()padanya. - Pada
04_pewarisan.py, tambahkan kelasKolam(Unit)dengan atributph_air. Buat satu objek dan periksaisinstance(kolam, Unit). - Tantangan: tambahkan kelas
Kandangpada05_polimorfisme.pyyang mengisihasil()dengan rata-rata bobot, bukan total. Tambahkan juga kelasPembenihan(Unit)baru; pastikan perulangan lama tidak perlu diubah.
Ringkasan Bab
- Class = cetakan, objek = hasil cetakan.
__init__mengisi data awal;selfadalah objek yang sedang dikerjakan. - Inheritance:
class Anak(Induk);super().__init__(...)memanggil induk. - Polymorphism: perintah sama, hasil menyesuaikan jenis objek.
- Encapsulation: awalan
_sebagai kesepakatan,propertysebagai penjaga yang memeriksa. - Dunder method (
__str__,__repr__,__eq__,__lt__,__len__) membuat objek akrab dengan Python. @dataclassmembuatkan__init__,__repr__, dan__eq__dari daftar kolom.
Selanjutnya, di Bab 17: environment dan dependency, cara memberi tiap proyek “kamar” Python sendiri dan mengatur paket yang dibutuhkannya.