Bab 64 — Metaprogramming: Kode yang Menulis dan Memeriksa Kode

Seri A5 — Python Tingkat Ahli

CatatanDi bab ini, Anda akan:
  • Memahami deskriptor: mesin di balik property, metode, dan banyak “keajaiban” Python
  • Memakai __init_subclass__ dan metaclass untuk mengatur cara kelas dibuat
  • Memeriksa program dari dalam dengan modul inspect (introspeksi)
  • Membaca dan mengubah kode sebagai pohon dengan modul ast
  • Menyusun kerangka skema mini untuk data lapangan: proyek Seri A5

Semua skrip bab ini ada di folder kode/bab64/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).

Sampai di sini Anda sudah membangun sistem lengkap dari lapangan sampai AI. Seri A5 mengajak Anda turun ke ruang mesin. Bayangkan seorang mandor kebun. Mandor biasa menanam dan memanen. Mandor ahli juga membuat aturan tanam: “setiap bibit baru wajib dicatat”, “setiap petak wajib diberi papan nama”. Itulah metaprogramming: kode yang mengatur, memeriksa, atau menulis kode lain. Anda sebenarnya sudah memakainya tiap hari: @property, @dataclass, dan model Pydantic semuanya hidup dari teknik di bab ini. Semua perilaku di bawah dijalankan di Python 3.14.8; aturan pencarian atribut dicocokkan dengan panduan resmi (docs.python.org, Descriptor HowTo, dibaca 2026-10-10).

Deskriptor: penjaga pintu atribut

Deskriptor adalah objek yang punya metode __get__, __set__, atau __delete__. Bila objek seperti itu dipasang sebagai atribut kelas, Python memanggil metodenya setiap kali atribut itu dibaca atau diisi. Ibarat penjaga pintu di gudang: setiap barang yang keluar-masuk diperiksa dulu.

Dua rantai yang perlu Anda tahu: urutan pencarian atribut dan rantai kelas.
01_deskriptor_dasar.py
# Bab 64 - Deskriptor: penjaga pintu untuk atribut
class Positif:
    """Deskriptor: nilai harus angka lebih dari nol."""

    def __set_name__(self, kelas, nama):
        self.nama = "_" + nama          # tempat penyimpanan di objek

    def __get__(self, objek, kelas=None):
        if objek is None:               # diakses lewat kelas: Pohon.diameter_cm
            return self
        return getattr(objek, self.nama)

    def __set__(self, objek, nilai):
        if not isinstance(nilai, (int, float)) or nilai <= 0:
            raise ValueError(f"{self.nama[1:]} harus angka positif, bukan {nilai!r}")
        setattr(objek, self.nama, nilai)


class Pohon:
    diameter_cm = Positif()
    tinggi_m = Positif()

    def __init__(self, jenis, diameter_cm, tinggi_m):
        self.jenis = jenis
        self.diameter_cm = diameter_cm   # memicu Positif.__set__
        self.tinggi_m = tinggi_m


jati = Pohon("Jati", 32.5, 18.0)
print(jati.jenis, jati.diameter_cm, jati.tinggi_m)

try:
    jati.diameter_cm = -5
except ValueError as e:
    print("Ditolak:", e)

try:
    Pohon("Meranti", 45.2, "tinggi")
except ValueError as e:
    print("Ditolak:", e)

print(type(Pohon.diameter_cm).__name__)   # lewat kelas, yang kembali deskriptornya
print(sorted(vars(jati)))                 # data tersimpan dengan nama berawalan _
Jati 32.5 18.0
Ditolak: diameter_cm harus angka positif, bukan -5
Ditolak: tinggi_m harus angka positif, bukan 'tinggi'
Positif
['_diameter_cm', '_tinggi_m', 'jenis']
  • __set_name__ dipanggil sekali saat kelas dibuat dan memberi tahu deskriptor nama atributnya (diameter_cm), sehingga satu deskriptor Positif bisa dipakai di banyak atribut.
  • __set__ memeriksa nilai. Karena itu jati.diameter_cm = -5 ditolak, juga saat nilai awal diberikan dari __init__.
  • __get__ mendapat objek=None bila atribut diakses lewat kelas (Pohon.diameter_cm), sehingga kita mengembalikan deskriptornya sendiri; itu sebabnya baris kedua dari bawah mencetak Positif.
  • Data disimpan di objek dengan nama berawalan _: deskriptor hidup di kelas dan dipakai bersama semua objek, jadi nilai per objek harus disimpan di objek.

Fungsi dan property juga deskriptor

02_fungsi_adalah_deskriptor.py
# Bab 64 - Fungsi dan property ternyata juga deskriptor
class Kolam:
    def __init__(self, jumlah_ikan, bobot_rata_g):
        self.jumlah_ikan = jumlah_ikan
        self.bobot_rata_g = bobot_rata_g

    def biomassa_kg(self):
        return self.jumlah_ikan * self.bobot_rata_g / 1000

    @property
    def padat_tebar(self):
        return self.jumlah_ikan / 100       # misal luas kolam 100 m2


kolam = Kolam(800, 540.5)

# 1. Fungsi di dalam kelas punya __get__: itulah yang membuat "metode terikat"
fungsi = Kolam.__dict__["biomassa_kg"]
print(type(fungsi).__name__, hasattr(fungsi, "__get__"))
terikat = fungsi.__get__(kolam, Kolam)      # persis yang dikerjakan kolam.biomassa_kg
print(type(terikat).__name__, terikat())

# 2. property adalah deskriptor data (punya __set__ walau tidak diisi)
print(type(Kolam.__dict__["padat_tebar"]).__name__, kolam.padat_tebar)
try:
    kolam.padat_tebar = 5
except AttributeError as e:
    print("AttributeError:", e)

# 3. Aturan urutan: deskriptor data MENANG atas isi __dict__ objek,
#    sedangkan fungsi biasa (non-data) KALAH dari __dict__ objek.
kolam.__dict__["biomassa_kg"] = lambda: "dari __dict__ objek"
print(kolam.biomassa_kg())                  # __dict__ objek mengalahkan fungsi
kolam.__dict__["padat_tebar"] = 999
print(kolam.padat_tebar)                    # property tetap menang
function True
method 432.4
property 8.0
AttributeError: property 'padat_tebar' of 'Kolam' object has no setter
dari __dict__ objek
8.0

Inilah rahasia “metode terikat”. Fungsi biasa di dalam kelas punya __get__; saat Anda menulis kolam.biomassa_kg, Python memanggil fungsi.__get__(kolam, Kolam) dan hasilnya adalah metode yang sudah “memegang” kolam sebagai self (baris method 432.4). property ternyata deskriptor yang lengkap (punya __set__), sehingga mengisinya menghasilkan AttributeError yang sama dengan pesan resmi.

Dari sini ada aturan urutan yang penting: deskriptor data (punya __set__) menang atas isi __dict__ objek; deskriptor non-data (hanya __get__, seperti fungsi biasa) kalah dari __dict__ objek. Dua baris terakhir keluaran membuktikannya: fungsi biomassa_kg bisa “ditimpa” lewat __dict__, tetapi padat_tebar (property) tetap menang.

CatatanINGAT

Deskriptor harus dipasang di kelas, bukan di objek. Deskriptor yang ditaruh di self.x = Positif() di dalam __init__ tidak dipanggil sama sekali.

__init_subclass__: kelas anak mendaftar sendiri

Sering kita ingin setiap kelas turunan otomatis terdaftar atau diperiksa. Dulu orang memakai metaclass; sejak Python 3.6 ada cara yang jauh lebih sederhana: metode __init_subclass__ di kelas induk dipanggil setiap kali ada kelas turunan dibuat.

03_init_subclass.py
# Bab 64 - __init_subclass__: kelas anak mendaftar sendiri
class Penyimpan:
    daftar = {}                              # ekstensi -> kelas penyimpan

    def __init_subclass__(cls, ekstensi=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if ekstensi is None:
            raise TypeError(f"{cls.__name__} wajib menyebut ekstensi=...")
        cls.ekstensi = ekstensi
        Penyimpan.daftar[ekstensi] = cls     # didaftarkan otomatis saat kelas dibuat

    @classmethod
    def untuk(cls, nama_berkas):
        ekstensi = nama_berkas.rsplit(".", 1)[-1]
        return cls.daftar[ekstensi]()


class PenyimpanCSV(Penyimpan, ekstensi="csv"):
    def simpan(self, baris):
        return "\n".join(",".join(str(x) for x in b) for b in baris)


class PenyimpanTSV(Penyimpan, ekstensi="tsv"):
    def simpan(self, baris):
        return "\n".join("\t".join(str(x) for x in b) for b in baris)


print(sorted(Penyimpan.daftar))
data = [("Jati", 32.5), ("Meranti", 45.2)]
print(Penyimpan.untuk("pohon.csv").simpan(data))
print(repr(Penyimpan.untuk("pohon.tsv").simpan(data)))

try:
    class PenyimpanRusak(Penyimpan):         # lupa menyebut ekstensi
        pass
except TypeError as e:
    print("TypeError:", e)
['csv', 'tsv']
Jati,32.5
Meranti,45.2
'Jati\t32.5\nMeranti\t45.2'
TypeError: PenyimpanRusak wajib menyebut ekstensi=...

Perhatikan class PenyimpanCSV(Penyimpan, ekstensi="csv"): argumen kata kunci di baris class diteruskan ke __init_subclass__. Begitu kelasnya selesai dibuat, ia sudah masuk Penyimpan.daftar, tanpa ada yang memanggil fungsi pendaftaran. Kelas yang lupa menyebut ekstensi ditolak saat didefinisikan, bukan saat dipakai. Pola “plugin yang mendaftar sendiri” ini lazim untuk format berkas, jenis sensor, atau jenis laporan.

Metaclass: pabrik pembuat kelas

Di Python, kelas juga objek, dan ada yang membuatnya: type. Ia disebut metaclass, “kelasnya kelas”.

Rantai kelas: objek, kelasnya, dan metaclass type.
04_metaclass.py
# Bab 64 - Metaclass: pabrik pembuat kelas
# 1. Kelas juga objek; pembuatnya bernama type
print(type(3).__name__, type(int).__name__, type(type).__name__)

# 2. type(nama, induk, isi) membuat kelas secara dinamis
Sapi = type("Sapi", (), {"jenis": "sapi", "suara": lambda self: "moo"})
print(Sapi.__name__, Sapi().suara())


# 3. Metaclass sendiri: mengatur cara SEMUA kelas turunannya dibuat
class WajibDocstring(type):
    def __new__(mcs, nama, induk, isi):
        for kunci, nilai in isi.items():
            if callable(nilai) and not kunci.startswith("_") and not nilai.__doc__:
                raise TypeError(f"metode {nama}.{kunci} belum punya docstring")
        isi["dibuat_oleh"] = mcs.__name__
        return super().__new__(mcs, nama, induk, isi)


class Sensor(metaclass=WajibDocstring):
    def baca(self):
        """Mengembalikan satu pembacaan."""
        return 27.5


print(Sensor().baca(), Sensor.dibuat_oleh, type(Sensor).__name__)

try:
    class SensorMalas(metaclass=WajibDocstring):
        def baca(self):
            return 0
except TypeError as e:
    print("TypeError:", e)
int type type
Sapi moo
27.5 WajibDocstring WajibDocstring
TypeError: metode SensorMalas.baca belum punya docstring
  • Baris pertama: type(3) adalah int, type(int) adalah type, dan type(type) adalah type sendiri: rantainya berhenti di situ.
  • type("Sapi", (), {...}) membuat kelas saat program berjalan, tanpa kata class.
  • WajibDocstring adalah metaclass: kodenya berjalan setiap kali ada kelas yang memakainya dibuat, dan boleh menolak atau mengubah isi kelas. Di sini ia menolak metode publik tanpa docstring dan menambahkan atribut dibuat_oleh.
TipTIPS

Urutan memilih alat: pakai dulu dekorator kelas atau __init_subclass__; pakai deskriptor untuk logika per atribut; baru metaclass bila Anda harus mengubah cara kelas dibuat (misalnya menyiapkan isi namespace sebelum badan kelas dijalankan). Metaclass kuat tetapi membuat kode sulit dibaca dan bisa bentrok bila dua pustaka memakai metaclass berbeda.

Introspeksi: program memeriksa dirinya sendiri

Introspeksi adalah kemampuan program menanyakan struktur dirinya: nama, parameter, dokumentasi, siapa yang memanggil. Modul inspect adalah kotak peralatannya.

05_introspeksi.py
# Bab 64 - Introspeksi: program memeriksa dirinya sendiri
import inspect


class Tanaman:
    """Tanaman kebun dengan hasil panen."""

    def __init__(self, komoditas: str, hasil_kg: float = 0.0):
        self.komoditas = komoditas
        self.hasil_kg = hasil_kg

    def panen(self, tambahan_kg: float, *, catatan: str = "") -> float:
        """Menambah hasil panen dan mengembalikan totalnya."""
        self.hasil_kg += tambahan_kg
        return self.hasil_kg


# 1. Tanda tangan (signature) dan anotasi
tanda = inspect.signature(Tanaman.panen)
print(tanda)
for nama, p in tanda.parameters.items():
    print(f"  {nama}: jenis={p.kind.name}, bawaan={p.default!r}")

# 2. Isi kelas
metode = [n for n, f in inspect.getmembers(Tanaman, inspect.isfunction)]
print(metode)
print(inspect.getdoc(Tanaman))

# 3. Kode sumber (hanya bila berkasnya ada)
print(inspect.getsource(Tanaman.panen).splitlines()[0].strip())

# 4. Memanggil dengan nama yang dibaca dari tanda tangan
bantu = inspect.signature(Tanaman).bind("kopi", hasil_kg=12.5)
print(bantu.arguments)

# 5. Siapa yang memanggil saya? (frame tumpukan panggilan)
def siapa_saya():
    return inspect.stack()[0].function, inspect.stack()[1].function


def pemanggil():
    return siapa_saya()


print(pemanggil())
(self, tambahan_kg: float, *, catatan: str = '') -> float
  self: jenis=POSITIONAL_OR_KEYWORD, bawaan=<class 'inspect._empty'>
  tambahan_kg: jenis=POSITIONAL_OR_KEYWORD, bawaan=<class 'inspect._empty'>
  catatan: jenis=KEYWORD_ONLY, bawaan=''
['__init__', 'panen']
Tanaman kebun dengan hasil panen.
def panen(self, tambahan_kg: float, *, catatan: str = "") -> float:
{'komoditas': 'kopi', 'hasil_kg': 12.5}
('siapa_saya', 'pemanggil')
  • inspect.signature memberi tanda tangan fungsi: tambahan_kg bisa diisi posisi atau nama, sedangkan catatan setelah tanda * hanya boleh lewat nama (KEYWORD_ONLY).
  • getmembers dengan penyaring isfunction menemukan __init__ dan panen; getdoc membaca docstring kelas.
  • getsource membaca kode sumber dari berkas (gagal bila kode dibuat dengan exec).
  • signature(...).bind mencocokkan argumen ke nama parameter. Pustaka seperti FastAPI memakai teknik ini untuk menerjemahkan isi permintaan web ke parameter fungsi.
  • inspect.stack() menelusuri tumpukan panggilan: siapa_saya mengetahui bahwa pemanggilnya adalah pemanggil.
PeringatanAWAS

inspect.stack() lambat karena membaca berkas sumber untuk tiap bingkai. Pakai di alat bantu debug, bukan di dalam perulangan yang dipanggil jutaan kali.

AST: membaca kode sebagai pohon

Sebelum dijalankan, Python mengubah teks kode menjadi pohon sintaks abstrak (Abstract Syntax Tree, AST): tiap operasi menjadi simpul pohon. Modul ast membuka pohon itu untuk kita.

06_ast_membaca.py
# Bab 64 - AST: membaca kode sebagai pohon
import ast

# 1. Satu rumus kecil menjadi pohon
pohon = ast.parse("biomassa = jumlah * bobot / 1000", mode="exec")
print(ast.dump(pohon, indent=2))

# 2. Menelusuri pohon: berapa fungsi dan panggilan di sebuah kode?
kode = '''
def hitung(a, b):
    return rata(a) + rata(b)

def rata(x):
    return sum(x) / len(x)

print(hitung([1, 2], [3, 4]))
'''


class Penghitung(ast.NodeVisitor):
    def __init__(self):
        self.fungsi = []
        self.panggilan = []

    def visit_FunctionDef(self, node):
        self.fungsi.append(node.name)
        self.generic_visit(node)             # tetap telusuri isinya

    def visit_Call(self, node):
        if isinstance(node.func, ast.Name):
            self.panggilan.append(node.func.id)
        self.generic_visit(node)


p = Penghitung()
p.visit(ast.parse(kode))
print("fungsi    :", p.fungsi)
print("panggilan :", sorted(p.panggilan))
Module(
  body=[
    Assign(
      targets=[
        Name(id='biomassa', ctx=Store())],
      value=BinOp(
        left=BinOp(
          left=Name(id='jumlah', ctx=Load()),
          op=Mult(),
          right=Name(id='bobot', ctx=Load())),
        op=Div(),
        right=Constant(value=1000)))])
fungsi    : ['hitung', 'rata']
panggilan : ['hitung', 'len', 'print', 'rata', 'rata', 'sum']

Rumus jumlah * bobot / 1000 menjadi pohon: akarnya pembagian (Div), anak kirinya perkalian (Mult), anak kanannya konstanta 1000. Susunan pohon itulah yang menjelaskan mengapa perkalian dikerjakan lebih dulu. NodeVisitor menelusuri pohon dan memanggil visit_<NamaSimpul> untuk tiap simpul; di sini ia menemukan dua fungsi dan enam pemanggilan (rata dua kali, sum, len, hitung, print). Alat seperti ruff, pytest (yang menulis ulang assert), dan pemeriksa tipe semuanya bekerja dengan AST.

Mengubah kode, dan menghitung rumus dengan aman

07_ast_mengubah_aman.py
# Bab 64 - AST: mengubah kode, dan menghitung rumus dengan aman
import ast
import operator

# 1. NodeTransformer: ganti semua angka 1000 (gram ke kg) menjadi nama GRAM_PER_KG
class GantiKonstanta(ast.NodeTransformer):
    def visit_Constant(self, node):
        if node.value == 1000:
            return ast.copy_location(ast.Name(id="GRAM_PER_KG", ctx=ast.Load()), node)
        return node


pohon = ast.parse("biomassa = jumlah * bobot / 1000", mode="exec")
pohon = ast.fix_missing_locations(GantiKonstanta().visit(pohon))
print(ast.unparse(pohon))

lingkungan = {"jumlah": 800, "bobot": 540.5, "GRAM_PER_KG": 1000}
exec(compile(pohon, "<ast>", "exec"), lingkungan)    # aman: kita sendiri yang membuat kodenya
print(lingkungan["biomassa"])

# 2. Rumus dari pengguna jangan pernah di-eval(); pakai daftar putih operasi
OPERASI = {ast.Add: operator.add, ast.Sub: operator.sub,
           ast.Mult: operator.mul, ast.Div: operator.truediv}


def hitung_aman(teks, variabel):
    def evaluasi(n):
        if isinstance(n, ast.Expression):
            return evaluasi(n.body)
        if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)):
            return n.value
        if isinstance(n, ast.Name) and n.id in variabel:
            return variabel[n.id]
        if isinstance(n, ast.BinOp) and type(n.op) in OPERASI:
            return OPERASI[type(n.op)](evaluasi(n.left), evaluasi(n.right))
        raise ValueError(f"tidak diizinkan: {type(n).__name__}")
    return evaluasi(ast.parse(teks, mode="eval"))


data = {"jumlah": 800, "bobot": 540.5}
print(hitung_aman("jumlah * bobot / 1000", data))
for nakal in ["__import__('os').getcwd()", "jumlah ** 99999", "bobot.real"]:
    try:
        print(hitung_aman(nakal, data))
    except ValueError as e:
        print("Ditolak:", e)
biomassa = jumlah * bobot / GRAM_PER_KG
432.4
432.4
Ditolak: tidak diizinkan: Call
Ditolak: tidak diizinkan: BinOp
Ditolak: tidak diizinkan: Attribute

Bagian pertama memakai NodeTransformer: simpul Constant bernilai 1000 diganti Name("GRAM_PER_KG"); ast.unparse mengubah pohon kembali menjadi teks, dan compile + exec menjalankannya (hasil 432.4, sama dengan versi aslinya).

Bagian kedua penting untuk keamanan. Bila pengguna boleh mengetik rumus (misalnya pada dasbor), jangan memakai eval(): ia menjalankan kode apa pun. Kita membaca rumusnya sebagai AST lalu hanya mengizinkan simpul dari daftar putih: angka, nama variabel yang kita sediakan, dan empat operasi hitung. Pemanggilan fungsi (__import__(...)), pemangkatan (**, bisa menghabiskan memori), dan akses atribut ditolak.

PeringatanAWAS

Dokumentasi resmi ast mengingatkan bahwa mengurai teks yang rumit atau sangat besar dapat membuat interpreter crash karena batas tumpukan, dan bahwa ast.literal_eval pun “tidak dianjurkan” untuk data tak tepercaya (sumber: docs.python.org/3/library/ast.html, dibaca 2026-10-10). Daftar putih saja belum cukup bila teksnya bisa berukuran besar: batasi panjang masukan.

Proyek Bersambung A5: kerangka skema mini

Di Seri A5 proyek kita menjadi lebih cepat, lebih terdistribusi, lalu dirangkum sebagai portofolio. Langkah pertamanya: memahami cara kerja pustaka validasi seperti Pydantic (Bab 34) dengan membuat versi mini sendiri dari semua yang baru Anda pelajari. Deskriptor (Angka, Pilihan) memeriksa tiap bidang; __init_subclass__ mengumpulkan bidang tiap model; tidak perlu metaclass.

08_proyek_skema.py
# Bab 64 - Proyek A5: kerangka skema mini untuk data lapangan
class Bidang:
    """Deskriptor dasar: menyimpan nilai dan memeriksanya lewat periksa()."""

    def __init__(self, wajib=True):
        self.wajib = wajib

    def __set_name__(self, kelas, nama):
        self.nama = nama

    def __get__(self, objek, kelas=None):
        return self if objek is None else objek.__dict__.get(self.nama)

    def __set__(self, objek, nilai):
        objek.__dict__[self.nama] = self.periksa(nilai)

    def periksa(self, nilai):
        return nilai


class Angka(Bidang):
    def __init__(self, minimum=None, maksimum=None, **kw):
        super().__init__(**kw)
        self.minimum, self.maksimum = minimum, maksimum

    def periksa(self, nilai):
        if isinstance(nilai, bool) or not isinstance(nilai, (int, float)):
            raise ValueError(f"{self.nama}: harus angka, bukan {nilai!r}")
        if self.minimum is not None and nilai < self.minimum:
            raise ValueError(f"{self.nama}: {nilai} di bawah {self.minimum}")
        if self.maksimum is not None and nilai > self.maksimum:
            raise ValueError(f"{self.nama}: {nilai} di atas {self.maksimum}")
        return nilai


class Pilihan(Bidang):
    def __init__(self, *pilihan, **kw):
        super().__init__(**kw)
        self.pilihan = pilihan

    def periksa(self, nilai):
        if nilai not in self.pilihan:
            raise ValueError(f"{self.nama}: {nilai!r} bukan salah satu dari {list(self.pilihan)}")
        return nilai


class Model:
    bidang = {}

    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        cls.bidang = {n: b for n, b in vars(cls).items() if isinstance(b, Bidang)}

    def __init__(self, **data):
        for nama, b in self.bidang.items():
            if nama in data:
                setattr(self, nama, data.pop(nama))
            elif b.wajib:
                raise ValueError(f"{nama}: wajib diisi")
        if data:
            raise ValueError(f"bidang tak dikenal: {sorted(data)}")

    def ke_dict(self):
        return {n: getattr(self, n) for n in self.bidang}


class Pohon(Model):
    jenis = Pilihan("Jati", "Meranti", "Mahoni", "Sengon")
    diameter_cm = Angka(minimum=1, maksimum=300)
    tinggi_m = Angka(minimum=0.5, maksimum=80, wajib=False)


print(list(Pohon.bidang))
print(Pohon(jenis="Jati", diameter_cm=32.5).ke_dict())
for rusak in [dict(jenis="Pinus", diameter_cm=20), dict(jenis="Jati", diameter_cm=900),
              dict(jenis="Jati"), dict(jenis="Jati", diameter_cm=20, warna="hijau")]:
    try:
        Pohon(**rusak)
    except ValueError as e:
        print("Ditolak:", e)
['jenis', 'diameter_cm', 'tinggi_m']
{'jenis': 'Jati', 'diameter_cm': 32.5, 'tinggi_m': None}
Ditolak: jenis: 'Pinus' bukan salah satu dari ['Jati', 'Meranti', 'Mahoni', 'Sengon']
Ditolak: diameter_cm: 900 di atas 300
Ditolak: diameter_cm: wajib diisi
Ditolak: bidang tak dikenal: ['warna']

Pohon hanya berisi tiga baris bidang, dan kita mendapat: validasi jenis dan rentang, bidang wajib vs opsional (tinggi_m bernilai None bila tidak diisi), serta penolakan bidang tak dikenal. Pesan error menyebut nama bidang karena __set_name__. Pydantic jauh lebih lengkap (konversi tipe, JSON, kecepatan), tetapi gagasan dasarnya sama.

Latihan

  1. Buat deskriptor Teks(maks_panjang) yang menolak teks lebih panjang dari batas dan memangkas spasi di ujung. Pasang di kelas Petak pada bidang nama.
  2. Tulis __init_subclass__ pada kelas Laporan yang menolak kelas turunan tanpa metode ringkas. (Petunjuk: hasattr(cls, "ringkas").)
  3. Dengan ast.NodeVisitor, hitung berapa kali print dipanggil dalam sebuah berkas .py yang Anda pilih, dan tampilkan nomor barisnya (node.lineno).
  4. Tantangan: tambahkan bidang Tanggal pada kerangka proyek yang menerima teks "2026-10-10" dan menyimpannya sebagai datetime.date, serta metode Model.dari_dict(data) yang membuat objek dari kamus.

Ringkasan Bab

  • Deskriptor (__get__/__set__/__set_name__) mengendalikan akses atribut; fungsi, property, dan classmethod adalah deskriptor.
  • Deskriptor data mengalahkan __dict__ objek; deskriptor non-data dikalahkan olehnya.
  • __init_subclass__ menjalankan kode setiap ada kelas turunan; cukup untuk sebagian besar kebutuhan yang dulu memakai metaclass.
  • Metaclass (type dan turunannya) mengatur pembuatan kelas; pakai bila alat yang lebih sederhana tidak cukup.
  • inspect membaca tanda tangan, anggota, sumber, dan tumpukan panggilan.
  • ast mengubah kode menjadi pohon yang bisa dibaca dan diubah; rumus dari pengguna dihitung dengan daftar putih simpul, bukan eval().

Selanjutnya, di Bab 65: kita menengok ruang mesin CPython: bagaimana objek disimpan, apa itu bytecode, kapan sampah memori dibersihkan, dan nasib GIL.