Kursus Python / Membuat record data dengan dataclass

dataclass menghasilkan metode seperti inisialisasi, representasi, dan perbandingan nilai dari deklarasi field. Fitur pustaka standar ini menyederhanakan class untuk record data, tetapi tidak otomatis memvalidasi anotasi tipe atau aturan bisnis.

Kurangi kode berulang pada record

Dekorator @dataclass menemukan field melalui anotasi pada badan class. Dengan pengaturan biasa, ia menghasilkan __init__, __repr__, dan __eq__. Anda tidak harus menulis sendiri semua pemberian nilai, tampilan field, dan logika perbandingan untuk record sederhana.

Ini berbeda dari mengubah semua objek menjadi record. Class dengan perilaku kompleks mungkin membutuhkan rancangan yang lain. Gunakan dataclass ketika sekumpulan field dan metode terkait benar-benar mewakili kebutuhan data. Detail parameter ada pada dokumentasi dataclasses Python.

Untuk list bawaan yang baru pada setiap record, gunakan field(default_factory=list). Fungsi list dipanggil saat nilai bawaan diperlukan, bukan satu list yang dibagi ke semua objek. Aturannya berhubungan dengan pembagian atribut class dan instance.

Jalankan tag independen dan perbandingan field

Simpan kode lengkap berikut sebagai data_records.py dengan encoding UTF-8. Gunakan Python 3.12 atau lebih baru melalui lingkungan proyek, lalu jalankan python data_records.py. Semua impor berasal dari pustaka standar.

from dataclasses import dataclass, field

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

    def __post_init__(self):
        if type(self.amount) is not int or self.amount < 0:
            raise ValueError("invalid amount")

a = Expense("lunch", 8000)
b = Expense("bus", 2500)
a.tags.append("work")
print(a)
print(b.tags)
print(a == Expense("lunch", 8000, ["work"]))

Keluaran yang diharapkan:

Expense(name='lunch', amount=8000, tags=['work'])
[]
True

Initializer yang dihasilkan menerima name, amount, dan opsi tags. Ketika tag tidak diberikan, factory membuat list baru. Karena itu, penambahan work pada a.tags tidak muncul pada b.tags.

print(a) menggunakan representasi yang dihasilkan untuk memperlihatkan nama class dan nilai field. Perbandingan terakhir bernilai benar karena kedua objek merupakan tipe yang sama dan field yang dibandingkan bernilai sama. Mereka tetap dua objek berbeda; kesamaan nilai bukan identitas objek.

Factory bekerja ketika argumen tidak diberikan. Jika pemanggil secara eksplisit memberikan list yang sama ke dua record, contoh menyimpan referensi list tersebut pada keduanya. Dataclass tidak otomatis menyalin argumen milik pemanggil. Tentukan kebijakan penyalinan jika kemandirian juga diperlukan untuk input eksplisit.

Pahami validasi dan batas frozen

__post_init__ dijalankan oleh initializer yang dihasilkan setelah field diisi. Contoh memeriksa bahwa jumlah merupakan integer biasa dan tidak negatif, sehingga -1, True, 3.5, dan string angka ditolak. Pemeriksaan itu ditulis oleh program; anotasi amount: int tidak menjalankannya secara otomatis.

Field lain juga tidak otomatis diperiksa. Anotasi name: str tidak membuat Expense(123, 8000) ditolak oleh contoh ini. Jika nama atau isi tag mempunyai syarat, tambahkan dan uji aturan tersebut secara eksplisit.

Setelah pembuatan, a.amount = -1 masih dapat dilakukan pada class biasa ini tanpa memanggil __post_init__ lagi. Jika semua perubahan jumlah harus divalidasi, pertimbangkan property dan setter atau rancangan pembaruan keadaan yang berbeda.

Menggunakan @dataclass(frozen=True) menolak pemberian nilai biasa pada field dan dapat menghasilkan FrozenInstanceError. Namun item.tags.append("work") masih dapat mengubah list di dalam field. Membekukan pengikatan field bukan membuat seluruh struktur objek immutable secara mendalam. Untuk record yang seharusnya tidak berubah, pilih tipe isi yang sesuai, misalnya tuple, dan periksa keseluruhan kontraknya.

Uji perbandingan, konversi, dan input yang tidak sah

  1. Jalankan sumber asli dan cocokkan representasi a, tag kosong milik b, serta hasil perbandingan benar.
  2. Ubah jumlah awal menjadi -1 pada percobaan terpisah. Pastikan pembuatan ditolak, lalu pulihkan 8000.
  3. Tambahkan tag hanya pada satu instance dan periksa bahwa instance lain tetap independen. Bandingkan dengan kasus sengaja memberikan satu list eksplisit kepada keduanya.
  4. Jika mencoba frozen=True, uji pemberian nilai field dan penambahan isi list sebagai dua operasi berbeda. Pulihkan sumber awal setelah pengamatan.

dataclasses.asdict membantu mengubah field record menjadi dictionary dan menangani struktur dataclass bersarang. Pada contoh list sederhana, hasil konversi mempunyai list yang terpisah. Namun konversi ke dictionary belum berarti seluruh isinya dapat ditulis ke JSON: tanggal atau Decimal tetap membutuhkan aturan representasi yang sesuai. Lihat konversi dan validasi JSON untuk membedakan tahapan tersebut.

Perbandingan field bawaan juga bukan aturan identitas transaksi. Dua pengeluaran bernama sama dengan jumlah dan tag sama belum tentu satu kejadian yang sama. Jika aplikasi perlu mendeteksi transaksi duplikat, rancang identifier serta aturan perbandingannya secara sengaja, lalu uji terpisah dari kenyamanan representasi dataclass.