Python

Python Dataclasses: Data Classes

Pelajari @dataclass decorator, field options, inheritance, validation, frozen dataclasses, slots, dan praktik terbaik untuk membuat class yang bersih dan efisien

1. Pengenalan Dataclasses

dataclasses adalah modul Python yang diperkenalkan di Python 3.7 (PEP 557) yang menyediakan decorator dan fungsi untuk membuat class yang terutama digunakan untuk menyimpan data. Dengan @dataclass, Python secara otomatis menghasilkan method seperti __init__, __repr__, __eq__, dan lainnya berdasarkan field yang dideklarasikan.

Tanpa dataclass, Anda harus menulis banyak boilerplate code untuk method __init__, __repr__, dan __eq__ secara manual. Dataclass menghilangkan kerumitan ini dan menghasilkan kode yang lebih bersih, lebih singkat, dan lebih mudah dipelihara.

Mengapa Dataclasses?

Fitur Penjelasan
Kurang BoilerplateTidak perlu tulis __init__, __repr__, __eq__ secara manual
Type HintsDidesain untuk digunakan dengan type annotations
ImmutabilityOpsional: buat dataclass frozen (tidak bisa diubah)
Field OptionsKontrol default value, factory, comparison, hashing
InheritanceBisa diwarisi dan dikombinasikan
SlotsOptimasi memori dengan __slots__

Dataclass vs Regular Class

Python β€” Perbandingan
# === Regular Class (manual, banyak boilerplate) ===
class PenggunaManual:
    def __init__(self, nama: str, umur: int, email: str):
        self.nama = nama
        self.umur = umur
        self.email = email

    def __repr__(self):
        return f"PenggunaManual(nama='{self.nama}', umur={self.umur}, email='{self.email}')"

    def __eq__(self, other):
        if not isinstance(other, PenggunaManual):
            return NotImplemented
        return (self.nama == other.nama and
                self.umur == other.umur and
                self.email == other.email)

    def __hash__(self):
        return hash((self.nama, self.umur, self.email))

# === Dataclass (singkat, otomatis!) ===
from dataclasses import dataclass

@dataclass
class Pengguna:
    nama: str
    umur: int
    email: str

# Keduanya menghasilkan perilaku yang sama!
p1 = Pengguna("Budi", 25, "budi@mail.com")
p2 = Pengguna("Budi", 25, "budi@mail.com")

print(p1)            # Pengguna(nama='Budi', umur=25, email='budi@mail.com')
print(p1 == p2)      # True
print(repr(p1))      # Pengguna(nama='Budi', umur=25, email='budi@mail.com')

# Dataclass menghemat ~15 baris kode per class!
Diagram: Method yang Di-generate Otomatis
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        @dataclass AUTO-GENERATED METHODS             β”‚
β”‚                                                      β”‚
β”‚  Deklarasi:          Method yang dihasilkan:         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚ @dataclassβ”‚       β”‚ βœ… __init__()        β”‚         β”‚
β”‚  β”‚ class Foo:│──────►│ βœ… __repr__()        β”‚         β”‚
β”‚  β”‚   x: int β”‚       β”‚ βœ… __eq__()          β”‚         β”‚
β”‚  β”‚   y: str β”‚       β”‚ ❌ __hash__()        β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚ ❌ __lt__(), dll     β”‚         β”‚
β”‚                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                                                      β”‚
β”‚  order=True  β†’ βœ… __lt__, __le__, __gt__, __ge__     β”‚
β”‚  frozen=True β†’ ❌ __setattr__, __delattr__           β”‚
β”‚  eq=False    β†’ ❌ __eq__ (pakai default id)          β”‚
β”‚  unsafe_hash=True β†’ βœ… __hash__ (meskipun mutable)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Sintaks Dasar @dataclass

Python β€” @dataclass Dasar
from dataclasses import dataclass, field

# === Dataclass paling sederhana ===
@dataclass
class Titik:
    x: float
    y: float

t = Titik(3.0, 4.0)
print(t)            # Titik(x=3.0, y=4.0)
print(t.x, t.y)    # 3.0 4.0

# === Dataclass dengan default values ===
@dataclass
class Produk:
    nama: str
    harga: float
    stok: int = 0               # Default 0
    aktif: bool = True           # Default True
    kategori: str = "umum"      # Default "umum"

p1 = Produk("Laptop", 15000000)
p2 = Produk("Mouse", 150000, stok=50, kategori="aksesoris")

print(p1)  # Produk(nama='Laptop', harga=15000000, stok=0, aktif=True, kategori='umum')
print(p2)  # Produk(nama='Mouse', harga=150000, stok=50, aktif=True, kategori='aksesoris')

# === Dataclass dengan berbagai tipe ===
from typing import Optional

@dataclass
class Mahasiswa:
    nama: str
    nim: str
    jurusan: str
    semester: int = 1
    ipk: Optional[float] = None
    aktif: bool = True

mhs = Mahasiswa("Budi Santoso", "2024001", "Teknik Informatika")
print(mhs)
# Mahasiswa(nama='Budi Santoso', nim='2024001', jurusan='Teknik Informatika',
#           semester=1, ipk=None, aktif=True)

# === Dataclass dengan method tambahan ===
@dataclass
class Lingkaran:
    jari_jari: float

    @property
    def luas(self) -> float:
        import math
        return math.pi * self.jari_jari ** 2

    @property
    def keliling(self) -> float:
        import math
        return 2 * math.pi * self.jari_jari

    def scale(self, faktor: float) -> 'Lingkaran':
        return Lingkaran(self.jari_jari * faktor)

l = Lingkaran(5)
print(f"Luas: {l.luas:.2f}")       # Luas: 78.54
print(f"Keliling: {l.keliling:.2f}")  # Keliling: 31.42
print(l.scale(2))                    # Lingkaran(jari_jari=10)

Decorator Parameters

Python β€” Decorator Parameters
from dataclasses import dataclass

# @dataclass bisa menerima parameter:
# init=True     β†’ generate __init__ (default)
# repr=True     β†’ generate __repr__ (default)
# eq=True       β†’ generate __eq__ (default)
# order=False   β†’ generate __lt__, __le__, __gt__, __ge__
# frozen=False  β†’ buat immutable
# unsafe_hash=False β†’ generate __hash__

# Tanpa __init__ (manual)
@dataclass(init=False)
class CustomInit:
    x: int
    y: int

    def __init__(self, x: int, y: int = 0):
        self.x = x
        self.y = y + 10  # Custom logic

c = CustomInit(5)
print(c)  # CustomInit(x=5, y=10)

# Dengan ordering
@dataclass(order=True)
class Siswa:
    nama: str
    nilai: float

siswa_list = [
    Siswa("Budi", 85),
    Siswa("Ani", 92),
    Siswa("Dimas", 78),
]

# Bisa di-sort langsung!
siswa_list.sort(reverse=True)
for s in siswa_list:
    print(f"  {s.nama}: {s.nilai}")
# Ani: 92
# Budi: 85
# Dimas: 78

# Tanpa __eq__ (gunakan default identity comparison)
@dataclass(eq=False)
class NoEquality:
    id: int
    nama: str

a = NoEquality(1, "test")
b = NoEquality(1, "test")
print(a == b)  # False β€” berbeda objek, eq=False

3. Field Options

Fungsi field() memberikan kontrol lebih detail pada setiap field, seperti default value yang mutable, factory, visibility, dan opsi comparison.

Python β€” Field Options
from dataclasses import dataclass, field

# === Default factory untuk mutable defaults ===
# ❌ SALAH β€” mutable default akan di-share antar instance!
# @dataclass
# class Config:
#     tags: list[str] = []  ← ERROR: ValueError!

# βœ… BENAR β€” gunakan field(default_factory=...)
@dataclass
class Config:
    tags: list[str] = field(default_factory=list)
    metadata: dict = field(default_factory=dict)
    warnings: list[str] = field(default_factory=list)

c1 = Config()
c2 = Config()

c1.tags.append("python")
print(c1.tags)  # ['python']
print(c2.tags)  # [] β€” tidak terpengaruh!

# === field() parameters ===
# default          β†’ nilai default
# default_factory  β†’ callable untuk default mutable
# repr             β†’ tampilkan di __repr__?
# compare          β†’ gunakan di __eq__?
# hash             β†’ gunakan di __hash__?
# init             β†’ sertakan di __init__?
# metadata         β†’ dict metadata tambahan

@dataclass
class User:
    nama: str
    email: str
    password_hash: str = field(repr=False)       # Sembunyikan dari repr
    api_key: str = field(compare=False)           # Abaikan saat perbandingan
    login_count: int = field(default=0, init=False)  # Tidak di __init__
    _internal: str = field(default="secret", repr=False, compare=False, hash=False)

u = User("Budi", "budi@mail.com", "hashed_pw_123")
print(u)
# User(nama='Budi', email='budi@mail.com') β€” password tidak tampil!

# === field dengan metadata ===
@dataclass
class ValidatedField:
    nama: str = field(metadata={"max_length": 50, "pattern": r"^[a-zA-Z ]+$"})
    umur: int = field(metadata={"min": 0, "max": 150})
    email: str = field(metadata={"format": "email"})

# Metadata bisa diakses via fields()
from dataclasses import fields

for f in fields(ValidatedField):
    print(f"{f.name}: {f.metadata}")
# nama: {'max_length': 50, 'pattern': '^[a-zA-Z ]+$'}
# umur: {'min': 0, 'max': 150}
# email: {'format': 'email'}

Default Values dan Required Fields

Python β€” Required vs Optional
from dataclasses import dataclass, field

# Aturan: field tanpa default HARUS ada SEBELUM field dengan default
# ❌ SALAH
# @dataclass
# class Bad:
#     nama: str = "default"
#     umur: int        ← ERROR! Non-default after default

# βœ… BENAR
@dataclass
class Good:
    umur: int              # Required (tanpa default)
    nama: str              # Required (tanpa default)
    kota: str = "Jakarta"  # Optional (dengan default)
    aktif: bool = True     # Optional (dengan default)

# Membuat field "required" meskipun ada default_factory
# dengan menggunakan sentinel pattern
_MISSING = object()

@dataclass
class FlexibleConfig:
    host: str
    port: int = field(default=8080)
    debug: bool = field(default=False)
    custom_header: str = field(default_factory=lambda: _MISSING)

    def __post_init__(self):
        if self.custom_header is _MISSING:
            self.custom_header = f"Server-{self.host}"

c = FlexibleConfig("localhost")
print(c.custom_header)  # Server-localhost

4. Post-Init Processing (__post_init__)

Method __post_init__ dipanggil setelah __init__ selesai. Ini sangat berguna untuk validasi data, menghitung derived fields, atau operasi setup tambahan.

Python β€” __post_init__
from dataclasses import dataclass, field

# === Validasi data ===
@dataclass
class UserProfile:
    nama: str
    umur: int
    email: str

    def __post_init__(self):
        # Validasi nama
        if not self.nama or len(self.nama.strip()) == 0:
            raise ValueError("Nama tidak boleh kosong!")
        self.nama = self.nama.strip().title()

        # Validasi umur
        if self.umur < 0 or self.umur > 150:
            raise ValueError(f"Umur tidak valid: {self.umur}")

        # Validasi email
        if "@" not in self.email:
            raise ValueError(f"Email tidak valid: {self.email}")
        self.email = self.email.lower()

# Test validasi
user = UserProfile("  budi santoso ", 25, "BUDI@Mail.com")
print(user)
# UserProfile(nama='Budi Santoso', umur=25, email='budi@mail.com')

# UserProfile("", 25, "test@mail.com")  ← ValueError!
# UserProfile("Budi", -5, "test@mail.com")  ← ValueError!

# === Derived/Computed fields ===
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)        # Tidak di __init__, dihitung otomatis
    perimeter: float = field(init=False)   # Sama

    def __post_init__(self):
        self.area = self.width * self.height
        self.perimeter = 2 * (self.width + self.height)

r = Rectangle(10, 5)
print(f"Luas: {r.area}")       # Luas: 50
print(f"Keliling: {r.perimeter}")  # Keliling: 30

# === InitVar β€” variable khusus untuk __post_init__ ===
from dataclasses import dataclass, field, InitVar

@dataclass
class User:
    nama: str
    email: str
    password: InitVar[str] = None       # Hanya di __post_init__, bukan field
    confirm_password: InitVar[str] = None
    password_hash: str = field(init=False, repr=False)

    def __post_init__(self, password: str, confirm_password: str):
        if password != confirm_password:
            raise ValueError("Password tidak cocok!")
        self.password_hash = f"hashed_{password}"

u = User("Budi", "budi@mail.com", "rahasia123", "rahasia123")
print(u)           # User(nama='Budi', email='budi@mail.com')
print(u.password_hash)  # hashed_rahasia123
# 'password' bukan attribute β€” hanya digunakan saat init!

# === Validasi dengan kombinasi field ===
@dataclass
class DateRange:
    start: str
    end: str

    def __post_init__(self):
        from datetime import datetime
        fmt = "%Y-%m-%d"
        d_start = datetime.strptime(self.start, fmt)
        d_end = datetime.strptime(self.end, fmt)
        if d_end < d_start:
            raise ValueError(f"End date ({self.end}) sebelum start date ({self.start})")

dr = DateRange("2026-01-01", "2026-12-31")  # βœ… OK
# DateRange("2026-12-31", "2026-01-01")  ← ValueError!

5. Frozen Dataclasses

Frozen dataclass adalah dataclass yang immutable β€” setelah dibuat, field-nya tidak bisa diubah. Ini sangat berguna untuk data konfigurasi, constants, dan data yang perlu menjadi hashable.

Python β€” Frozen Dataclass
from dataclasses import dataclass, field

# === Frozen dataclass β€” immutable ===
@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int

merah = Color(255, 0, 0)
hijau = Color(0, 255, 0)

print(merah)  # Color(r=255, g=0, b=0)

# ❌ Tidak bisa diubah!
# merah.r = 128  ← FrozenInstanceError!

# βœ… Bisa jadi key dictionary (karena hashable)
color_names = {
    merah: "Merah",
    hijau: "Hijau",
}
print(color_names[merah])  # Merah

# βœ… Bisa jadi elemen set
warna_set = {merah, hijau, Color(0, 0, 255)}

# === Frozen dengan field yang perlu mutable ===
@dataclass(frozen=True)
class Config:
    host: str
    port: int = 8080
    # Gunakan tuple, bukan list (karena frozen)
    allowed_origins: tuple[str, ...] = ("https://example.com",)

config = Config("localhost", 3000, ("http://localhost:3000",))
print(config)
# Config(host='localhost', port=3000, allowed_origins=('http://localhost:3000',))

# === Membuat "copy" dengan perubahan (replace) ===
from dataclasses import replace

config_baru = replace(config, port=8080, host="0.0.0.0")
print(config_baru)
# Config(host='0.0.0.0', port=8080, allowed_origins=('http://localhost:3000',))
print(config)  # Tetap tidak berubah!

# === Konstanta dengan frozen dataclass ===
@dataclass(frozen=True)
class APIConfig:
    BASE_URL: str = "https://api.example.com"
    VERSION: str = "v2"
    TIMEOUT: int = 30
    MAX_RETRIES: int = 3

    @property
    def full_url(self) -> str:
        return f"{self.BASE_URL}/{self.VERSION}"

api = APIConfig()
print(api.full_url)  # https://api.example.com/v2
πŸ’‘ Tips

Gunakan frozen=True untuk data yang sehariknya tidak berubah β€” konfigurasi, constants, data dari API response, atau sebagai key dictionary. Ini membantu mencegah bugs akibat perubahan data yang tidak disengaja.

6. Inheritance

Dataclass mendukung inheritance dengan baik, termasuk kombinasi default values dan field dari parent class.

Python β€” Inheritance
from dataclasses import dataclass, field

# === Basic inheritance ===
@dataclass
class Hewan:
    nama: str
    umur: int
    spesies: str

@dataclass
class HewanPeliharaan(Hewan):
    nama_pemilik: str
    sudah_vaksin: bool = False

kucing = HewanPeliharaan("Kitty", 3, "Kucing", "Budi", True)
print(kucing)
# HewanPeliharaan(nama='Kitty', umur=3, spesies='Kucing',
#                  nama_pemilik='Budi', sudah_vaksin=True)

# Akses field dari parent
print(f"{kucing.nama} ({kucing.spesies})")  # Kitty (Kucing)

# === Multi-level inheritance ===
@dataclass
class Base:
    id: int
    created_at: str = "2026-01-01"

@dataclass
class Middle(Base):
    name: str
    status: str = "active"

@dataclass
class Leaf(Middle):
    extra: float = 0.0

obj = Leaf(id=1, name="test", status="pending", extra=3.14)
print(obj)
# Leaf(id=1, created_at='2026-01-01', name='test', status='pending', extra=3.14)

# === Inheritance dengan override ===
@dataclass
class Shape:
    name: str
    color: str = "hitam"

    def describe(self) -> str:
        return f"{self.color} {self.name}"

@dataclass
class Circle(Shape):
    radius: float = 1.0
    name: str = "Lingkaran"  # Override default dari parent

    @property
    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

c = Circle(radius=5)
print(c.describe())  # hitam Lingkaran
print(f"Luas: {c.area:.2f}")  # Luas: 78.54

# === Abstract base dengan dataclass ===
from abc import ABC, abstractmethod

@dataclass
class BaseEntity(ABC):
    id: int
    nama: str

    @abstractmethod
    def validate(self) -> bool:
        pass

@dataclass
class Product(BaseEntity):
    harga: float
    stok: int = 0

    def validate(self) -> bool:
        return self.harga > 0 and self.stok >= 0

@dataclass
class Service(BaseEntity):
    durasi_menit: int
    harga_per_menit: float

    def validate(self) -> bool:
        return self.durasi_menit > 0

    @property
    def total_harga(self) -> float:
        return self.durasi_menit * self.harga_per_menit

p = Product(1, "Laptop", 15000000, 10)
s = Service(2, "Konsultasi", 60, 50000)

print(f"{p.nama}: Rp {p.harga:,.0f} (valid={p.validate()})")
print(f"{s.nama}: Rp {s.total_harga:,.0f} (valid={s.validate()})")
⚠️ Aturan Inheritance

Dalam dataclass inheritance, field tanpa default di child class harus ditempatkan setelah field dengan default dari parent. Jika tidak, Python akan raise TypeError. Ini berbeda dari regular class karena dataclass generate __init__ secara otomatis.

7. Slots & Performa

Python β€” Slots
from dataclasses import dataclass
import sys

# === Tanpa slots (default) ===
@dataclass
class TanpaSlots:
    x: int
    y: int
    z: int

# === Dengan slots (Python 3.10+) ===
@dataclass(slots=True)
class DenganSlots:
    x: int
    y: int
    z: int

a = TanpaSlots(1, 2, 3)
b = DenganSlots(1, 2, 3)

# Slots menghemat memori
print(f"Tanpa slots: {sys.getsizeof(a)} bytes")   # ~48 bytes
print(f"Dengan slots: {sys.getsizeof(b)} bytes")  # ~40 bytes

# Slots juga mempercepat akses attribute
import timeit

t1 = timeit.timeit(lambda: a.x, number=1000000)
t2 = timeit.timeit(lambda: b.x, number=1000000)
print(f"Tanpa slots: {t1:.4f}s")
print(f"Dengan slots: {t2:.4f}s")
# Slots biasanya ~20-30% lebih cepat!

# === Perbedaan behavior ===
# Tanpa slots β€” bisa tambah attribute baru
a.w = 99  # βœ… OK

# Dengan slots β€” tidak bisa tambah attribute baru
# b.w = 99  ← AttributeError!

# === Slots + Frozen ===
@dataclass(frozen=True, slots=True)
class ImmutablePoint:
    x: float
    y: float

    def distance_to(self, other: 'ImmutablePoint') -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

p1 = ImmutablePoint(0, 0)
p2 = ImmutablePoint(3, 4)
print(f"Jarak: {p1.distance_to(p2)}")  # Jarak: 5.0

8. Perbandingan dengan Alternatif

Fitur @dataclass NamedTuple Pydantic
Mutableβœ… Ya (opsional frozen)❌ Immutableβœ… Ya
Auto __init__βœ…βœ…βœ…
Auto __repr__βœ…βœ…βœ…
Auto __eq__βœ…βœ…βœ…
Validation❌ Manual❌ Manualβœ… Built-in
Serialization❌ Manual❌ Manualβœ… JSON, dict
Inheritanceβœ…βš οΈ Terbatasβœ…
Slotsβœ… (3.10+)βœ… Defaultβœ… (2.0+)
Librarystdlibstdlibpip install
Performa🟒 Cepat🟒 Cepat🟑 Sedang
Python β€” Perbandingan Kode
# === NamedTuple ===
from typing import NamedTuple

class PointNT(NamedTuple):
    x: float
    y: float

p = PointNT(3.0, 4.0)
# p.x = 5.0  ← Error! Immutable

# === Dict-like dataclass (TypedDict) ===
from typing import TypedDict

class PointDict(TypedDict):
    x: float
    y: float

d: PointDict = {"x": 3.0, "y": 4.0}

# === Kapan pakai yang mana? ===
#
# @dataclass:
#   βœ… Data yang bisa berubah (mutable)
#   βœ… Perlu method tambahan
#   βœ… Inheritance kompleks
#   βœ… Domain model, service objects
#
# NamedTuple:
#   βœ… Data ringkas dan immutable
#   βœ… Bisa jadi tuple (unpacking, indexing)
#   βœ… Return value dari fungsi
#
# Pydantic:
#   βœ… Perlu validasi otomatis
#   βœ… API request/response models
#   βœ… Serialization (JSON) built-in
#   βœ… Data dari sumber eksternal

9. Studi Kasus Praktis

E-Commerce Models

Python β€” E-Commerce Models
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

@dataclass
class Product:
    id: int
    nama: str
    harga: float
    stok: int = 0
    kategori: str = "umum"

    def __post_init__(self):
        if self.harga < 0:
            raise ValueError(f"Harga tidak boleh negatif: {self.harga}")
        if self.stok < 0:
            raise ValueError(f"Stok tidak boleh negatif: {self.stok}")

    def is_available(self, jumlah: int = 1) -> bool:
        return self.stok >= jumlah

    def reduce_stock(self, jumlah: int) -> None:
        if not self.is_available(jumlah):
            raise ValueError(f"Stok tidak cukup: {self.stok} < {jumlah}")
        self.stok -= jumlah

@dataclass
class OrderItem:
    produk: Product
    jumlah: int
    harga_satuan: float = field(init=False)

    def __post_init__(self):
        self.harga_satuan = self.produk.harga

    @property
    def subtotal(self) -> float:
        return self.harga_satuan * self.jumlah

@dataclass
class Order:
    id: str
    items: list[OrderItem] = field(default_factory=list)
    status: OrderStatus = OrderStatus.PENDING
    created_at: datetime = field(default_factory=datetime.now)
    catatan: Optional[str] = None

    @property
    def total(self) -> float:
        return sum(item.subtotal for item in self.items)

    @property
    def jumlah_item(self) -> int:
        return sum(item.jumlah for item in self.items)

    def add_item(self, produk: Product, jumlah: int) -> None:
        if not produk.is_available(jumlah):
            raise ValueError(f"Stok {produk.nama} tidak cukup!")
        self.items.append(OrderItem(produk, jumlah))

    def checkout(self) -> float:
        if not self.items:
            raise ValueError("Order kosong!")
        for item in self.items:
            item.produk.reduce_stock(item.jumlah)
        self.status = OrderStatus.PAID
        return self.total

# === Penggunaan ===
laptop = Product(1, "Laptop Gaming", 15000000, stok=10)
mouse = Product(2, "Mouse Wireless", 150000, stok=50)
keyboard = Product(3, "Mechanical Keyboard", 350000, stok=30)

order = Order("ORD-001")
order.add_item(laptop, 1)
order.add_item(mouse, 2)
order.add_item(keyboard, 1)

print(f"Order: {order.id}")
print(f"Jumlah item: {order.jumlah_item}")
for item in order.items:
    print(f"  - {item.produk.nama} x{item.jumlah} = Rp {item.subtotal:,.0f}")
print(f"Total: Rp {order.total:,.0f}")
print(f"Status: {order.status.value}")

total = order.checkout()
print(f"\nβœ… Checkout berhasil! Total: Rp {total:,.0f}")
print(f"Laptop stok tersisa: {laptop.stok}")

10. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Dataclasses:

Pertanyaan 1: Method apa yang TIDAK di-generate otomatis oleh @dataclass secara default?

a) __init__
b) __repr__
c) __hash__
d) __eq__

Pertanyaan 2: Bagaimana cara menangani mutable default values di dataclass?

a) tags: list = []
b) tags: list = field(default_factory=list)
c) tags: list = None
d) Tidak bisa menggunakan list sebagai field

Pertanyaan 3: Apa yang dilakukan parameter frozen=True pada @dataclass?

a) Membuat class menjadi tidak bisa diinstansiasi
b) Membuat field tidak bisa diubah setelah inisialisasi
c) Menghapus semua method
d) Menyimpan data ke file

Pertanyaan 4: Apa fungsi dari __post_init__?

a) Mengganti __init__ yang di-generate
b) Dipanggil setelah __init__ untuk validasi atau perhitungan tambahan
c) Menghapus instance dari memori
d) Membuat salinan instance

Pertanyaan 5: Apa keuntungan menggunakan @dataclass(slots=True)?

a) Membuat dataclass thread-safe
b) Menghemat memori dan mempercepat akses attribute
c) Mengaktifkan automatic validation
d) Membuat dataclass menjadi serializable
πŸ” Zoom
100%
🎨 Tema