Dataclass

Decorator @dataclass untuk kelas pembawa data, termasuk opsi frozen dan field.

Daftar isi

Kelas pembawa data biasanya berisi atribut beserta nilai defaultnya, dan tiga metode standarnya berulang di setiap kelas, yaitu __init__, __repr__, dan __eq__. Decorator @dataclass dari modul dataclasses menuliskan ketiganya secara otomatis berdasarkan definisi atributnya, sehingga kelas pembawa data menjadi ringkas tanpa kehilangan perilaku standar. Topik ini membahas penggunaan dasar, nilai default dengan field(), opsi frozen, serta pengisian nilai turunan melalui __post_init__.

Kelas data dasar#

Setiap atribut pada dataclass ditulis sebagai anotasi variabel, yaitu nama diikuti tipe yang diharapkan, tanpa perlu menulis __init__. Dari anotasi tersebut, decorator menghasilkan __init__ dengan parameter sesuai urutan atribut, __repr__ yang menampilkan setiap nilai, dan __eq__ yang membandingkan seluruh atributnya.

python Contoh: dataclass Titik
from dataclasses import dataclass


@dataclass
class Titik:
    x: int
    y: int

a = Titik(2, 3)
b = Titik(2, 3)
print(a)
print(a == b)
text Keluaran
Titik(x=2, y=3)
True

Cetakan Titik(x=2, y=3) berasal dari __repr__ yang dihasilkan otomatis, sedangkan True berasal dari __eq__ yang membandingkan nilai atributnya. Untuk mendapatkan ketiga perilaku itu secara manual, tiga metode harus ditulis sendiri, dan di situlah penghematan dataclass terlihat. Anotasi tipenya tidak diverifikasi oleh interpreter, tetapi dataclass memerlukannya untuk menentukan mana yang menjadi atribut, dan tipe tersebut tetap berguna bagi alat pemeriksa tipe.

Secara bawaan, dataclass dapat berubah, dan perilaku yang dihasilkan dapat disesuaikan lewat argumen decorator, misalnya eq=True atau repr=True yang memang nilai bawaannya. Opsi slots=True yang tersedia sejak Python 3.10 menghasilkan kelas dengan __slots__ untuk menghemat memori pada instans dalam jumlah besar. Kombinasi opsi ini membuat dataclass tetap ringkas tanpa mengunci desainnya.

Nilai default dan field()#

Atribut dengan nilai default cukup diberi tanda sama dengan, dan parameter pada __init__ yang dihasilkan menjadi opsional. Untuk tipe yang dapat diubah seperti list, nilai default tidak boleh dituliskan langsung, melainkan melalui field(default_factory=list), yang memanggil list untuk setiap instans baru. Pola ini menghindarkan state bersama, yaitu masalah yang muncul ketika atribut kelas berupa list dimutasi oleh satu instans.

python inventaris.py
from dataclasses import dataclass, field


@dataclass
class Barang:
    nama: str
    jumlah: int = 0
    riwayat: list = field(default_factory=list)

    def catat(self, kejadian):
        self.riwayat.append(kejadian)

b1 = Barang("buku tulis", 12)
b2 = Barang("pensil")
b1.catat("stok masuk 12")
print(b1)
print(b2)
text Keluaran
Barang(nama='buku tulis', jumlah=12, riwayat=['stok masuk 12'])
Barang(nama='pensil', jumlah=0, riwayat=[])

Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.

bash
# menjalankan program inventaris
python3 inventaris.py
text Keluaran
Barang(nama='buku tulis', jumlah=12, riwayat=['stok masuk 12'])
Barang(nama='pensil', jumlah=0, riwayat=[])

Instans b2 dibuat dengan satu argumen saja, karena jumlah dan riwayat memiliki default. Pemanggilan catat() pada b1 tidak memengaruhi b2, karena default_factory membuat list baru untuk setiap instans. Metode catat() juga dapat didefinisikan di badan dataclass, sehingga kelas pembawa data tetap dapat memiliki perilaku kecil miliknya sendiri.

Fungsi field() menampung kebutuhan lain di luar default, misalnya init=False untuk atribut yang tidak menjadi parameter konstruktor dan repr=False untuk atribut yang disembunyikan dari cetakan. Keduanya dibutuhkan pada contoh __post_init__ di bagian berikutnya. Urutan atribut tanpa default harus mendahului yang berdefault, sebagaimana aturan parameter pada fungsi biasa.

frozen=True: dataclass yang tidak dapat diubah#

Opsi frozen=True menghasilkan kelas yang atributnya tidak dapat ditugaskan setelah instans dibuat. Upaya penugasan ditolak dengan FrozenInstanceError, sehingga instans berperilaku seperti tuple, yaitu nilai yang aman dibagikan ke bagian lain program tanpa risiko berubah. Dataclass semacam ini juga dapat menjadi kunci dictionary bila atribut-atributnya dapat di-hash.

python Contoh: dataclass frozen
from dataclasses import dataclass


@dataclass(frozen=True)
class Koordinat:
    lintang: float
    bujur: float

k = Koordinat(-6.2, 106.8)
print(k.lintang, k.bujur)
try:
    k.lintang = 0.0
except Exception as e:
    print(type(e).__name__, str(e))
text Keluaran
-6.2 106.8
FrozenInstanceError cannot assign to field 'lintang'

Pembacaan atribut tetap normal, sedangkan penugasan k.lintang = 0.0 memicu FrozenInstanceError yang menyebut nama atributnya. Opsi ini tepat untuk data yang berfungsi seperti catatan tetap, misalnya koordinat, konfigurasi, atau hasil perhitungan yang tidak boleh berubah di tengah proses. Dataclass frozen juga otomatis mendapatkan __hash__, sehingga instansnya dapat dipakai sebagai kunci dictionary atau anggota set. Apabila perubahan memang diperlukan, fungsi dataclasses.replace() menghasilkan instans baru dengan sebagian nilai yang diganti.

Nilai turunan dengan post_init#

Beberapa atribut tidak diterima dari pemanggil, melainkan dihitung dari atribut lain setelah __init__ selesai. Dataclass menyediakan __post_init__ yang dijalankan otomatis tepat setelah __init__ yang dihasilkannya. Atribut semacam itu dideklarasikan dengan field(init=False) agar tidak ikut menjadi parameter konstruktor.

python Contoh: __post_init__ menghitung luas
from dataclasses import dataclass, field


@dataclass
class PersegiPanjang:
    panjang: float
    lebar: float
    luas: float = field(init=False)

    def __post_init__(self):
        self.luas = self.panjang * self.lebar

p = PersegiPanjang(4, 5)
print(p)
text Keluaran
PersegiPanjang(panjang=4, lebar=5, luas=20)

Konstruktor yang dihasilkan hanya menerima panjang dan lebar, sedangkan luas dihitung oleh __post_init__ dan ikut tampil pada __repr__. Pola ini berguna untuk nilai turunan yang dibaca sering, sebab nilai dihitung sekali saat instans dibuat, alih-alih dihitung ulang setiap kali dibaca. Metode ini juga tempat yang tepat untuk validasi antar atribut, misalnya menolak lebar bernilai negatif dengan raise ValueError. Pada dataclass frozen, penugasan di dalam __post_init__ harus melalui object.__setattr__(self, "luas", ...), karena mekanisme penolakan penugasan baru berlaku setelah inisialisasi selesai. Apabila nilai turunannya tidak ingin ditampilkan, opsi repr=False pada field() menyembunyikannya dari cetakan.

Kesalahan umum#

Kesalahan pertama adalah memberi nilai default list atau dictionary secara langsung, yang ditolak saat kelas didefinisikan.

python Contoh: default yang dapat diubah ditolak
from dataclasses import dataclass

try:
    @dataclass
    class Rusak:
        item: list = []
except ValueError as e:
    print(type(e).__name__, str(e))
text Keluaran
ValueError mutable default <class 'list'> for field item is not allowed: use default_factory

Pesan errornya sudah memuat perbaikannya, yaitu memakai default_factory. Bentuk field(default_factory=list) membuat list baru untuk setiap instans, sehingga masalah state bersama tidak pernah terjadi.

Kesalahan kedua adalah menugaskan atribut pada dataclass frozen, yang selalu berakhir dengan FrozenInstanceError sebagaimana contoh Koordinat. Perbaikannya adalah membuat instans baru, baik lewat pemanggilan konstruktor maupun fungsi dataclasses.replace(). Kesalahan ketiga adalah menulis atribut tanpa anotasi, misalnya jumlah = 0 tanpa : int, karena penugasan biasa di badan kelas dianggap variabel kelas dan tidak menjadi field, sehingga tidak muncul pada __init__ maupun __repr__. Kesalahan ini tidak menimbulkan error saat kelas didefinisikan, dan baru terlihat ketika konstruktor yang diharapkan tidak menerima parameternya. Menambahkan anotasi tipe pada setiap atribut menyelesaikannya sekaligus menjaga dokumentasi kelasnya tetap akurat.

Ringkasan#

  • Decorator @dataclass menghasilkan __init__, __repr__, dan __eq__ dari anotasi atribut pada badan kelas.
  • Nilai default ditulis langsung untuk tipe tetap, sedangkan tipe yang dapat diubah memakai field(default_factory=...).
  • Opsi frozen=True menolak penugasan atribut dengan FrozenInstanceError, sehingga instansnya aman dibagikan.
  • Metode __post_init__ mengisi atribut turunan setelah inisialisasi, dengan field(init=False) agar tidak menjadi parameter konstruktor.

Referensi#