Python & AI Tutorials Logo
Pemrograman Python

33. Data Classes untuk Data Terstruktur Sederhana

Di Bab 30, kita belajar cara membuat class untuk mendefinisikan tipe kita sendiri. Kita menulis method __init__ untuk menginisialisasi instance, method __repr__ untuk menampilkannya, dan method __eq__ untuk membandingkannya. Walaupun pendekatan ini bekerja dengan sempurna, ini melibatkan penulisan banyak kode berulang, terutama ketika sebuah class terutama ada untuk menyimpan data.

data class Python menyediakan cara yang lebih bersih dan ringkas untuk membuat class yang utamanya adalah wadah untuk data. Dengan menggunakan decorator @dataclass, Python otomatis menghasilkan method umum seperti __init__, __repr__, dan __eq__ berdasarkan atribut class yang kamu definisikan. Ini mengurangi boilerplate code dan membuat tujuanmu lebih jelas.

33.1) Apa Itu Data Class dan Kapan Menggunakannya

Sebuah data class adalah class yang dirancang terutama untuk menyimpan nilai data. Alih-alih menulis method inisialisasi dan perbandingan secara manual, kamu mendefinisikan atribut yang seharusnya dimiliki class-mu, dan Python menghasilkan method yang diperlukan secara otomatis.

Mengapa Data Class Penting

Pertimbangkan sebuah class biasa untuk merepresentasikan buku:

python
class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year
    
    def __repr__(self):
        return f"Book(title={self.title!r}, author={self.author!r}, year={self.year})"
    
    def __eq__(self, other):
        if not isinstance(other, Book):
            return False
        return (self.title == other.title and 
                self.author == other.author and 
                self.year == other.year)
 
book1 = Book("1984", "George Orwell", 1949)
print(book1)  # Output: Book(title='1984', author='George Orwell', year=1949)
 
book2 = Book("1984", "George Orwell", 1949)
print(book1 == book2)  # Output: True

Ini bekerja, tapi perhatikan berapa banyak kode yang kita tulis hanya untuk menyimpan tiga informasi. Method __init__, __repr__, dan __eq__ mengikuti pola yang bisa ditebak—mereka hanya menangani atribut yang kita definisikan.

Data class menghilangkan pengulangan ini. Data class sangat berguna terutama ketika:

  • Class-mu terutama menyimpan data ketimbang mengimplementasikan perilaku yang kompleks
  • Kamu membutuhkan method standar seperti inisialisasi, representasi string, dan perbandingan kesetaraan
  • Kamu ingin kode yang lebih jelas dan mudah dirawat dengan lebih sedikit boilerplate
  • Kamu membuat objek konfigurasi, data transfer object, atau record sederhana

Data class tidak menggantikan class biasa—mereka melengkapinya. Gunakan class biasa ketika kamu butuh logika inisialisasi kustom, method yang kompleks, atau hierarki inheritance. Gunakan data class ketika kamu terutama butuh wadah terstruktur untuk data terkait.

Hubungan Antara Data Class dan Class Biasa

Data class tetaplah class Python biasa. Mereka mendukung semua fitur yang kita pelajari di Bab 30-32: method, property, inheritance, dan special method. Decorator @dataclass hanya mengotomatisasi pembuatan method umum, sehingga kamu tidak perlu menulis kode berulang.

Class Biasa

init Manual

repr Manual

eq Manual

Method Kustom

Data Class

Decorator @dataclass

init Dibuat Otomatis

repr Dibuat Otomatis

eq Dibuat Otomatis

Method Kustom

33.2) Membuat Data Class dengan @dataclass

Untuk membuat data class, kamu mengimpor decorator dataclass dari modul dataclasses dan menerapkannya pada definisi class-mu. Di dalam class, kamu mendefinisikan atribut class dengan type annotation yang menentukan data apa yang harus disimpan oleh class tersebut.

Sintaks Data Class Dasar

python
from dataclasses import dataclass
 
@dataclass
class Student:
    name: str
    student_id: int
    gpa: float
 
# Membuat instance
alice = Student("Alice Johnson", 12345, 3.8)
bob = Student("Bob Smith", 12346, 3.5)
 
print(alice)  # Output: Student(name='Alice Johnson', student_id=12345, gpa=3.8)
print(bob)    # Output: Student(name='Bob Smith', student_id=12346, gpa=3.5)

Mari kita uraikan apa yang dilakukan oleh decorator @dataclass:

  1. @dataclass: Menerapkan decorator ini membuat Python otomatis menuliskan method __init__, __repr__, dan __eq__ untukmu

  2. __init__ otomatis: Python membuat method inisialisasi yang menerima tiga parameter ini dalam urutan seperti didefinisikan, lalu menetapkannya ke atribut instance

  3. __repr__ otomatis: Python membuat representasi string yang menampilkan nama class dan semua nilai atribut

  4. __eq__ otomatis: Python membuat method perbandingan kesetaraan yang membandingkan semua atribut

  5. Mengubah type annotation menjadi atribut instance: Pada class biasa, menulis name: str di body class membuat atribut class. Namun decorator @dataclass mengubah perilaku ini—ia memakai type annotation tersebut untuk mendefinisikan atribut instance. Setiap instance mendapatkan atribut name, student_id, dan gpa miliknya sendiri.

Perbedaan kunci dari class biasa:

python
# Class biasa - ini adalah atribut class (dibagikan oleh semua instance)
class RegularStudent:
    name: str
    student_id: int
 
# Data class - ini menjadi atribut instance (setiap instance punya miliknya sendiri)
@dataclass
class DataStudent:
    name: str
    student_id: int

Memahami Type Annotation dalam Data Class

Dalam data class, type annotation mendefinisikan atribut dan mendokumentasikan tipe yang diharapkan:

python
from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
    in_stock: bool
 
# Menggunakan tipe yang benar sesuai dokumentasi
laptop = Product("Laptop", 999.99, True)
print(laptop)  # Output: Product(name='Laptop', price=999.99, in_stock=True)
 
# Python tidak memaksa tipe - ini berjalan tanpa error
macbook = Product("Macbook", "expensive", True)
print(macbook)  # Output: Product(name='Macbook', price='expensive', in_stock=True)
 
# Tapi memakai tipe yang salah akan menimbulkan masalah nanti:
discounted = laptop.price * 0.9     # Works: 899.991
discounted = macbook.price * 0.9    # TypeError: can't multiply sequence by non-int of type 'float'
 
tax = laptop.price + 50             # Works: 1049.99
tax = macbook.price + 50            # TypeError: can only concatenate str (not "int") to str

Python tidak akan menghentikanmu untuk memberikan tipe yang salah saat membuat instance data class. Type annotation utamanya adalah dokumentasi—mereka memberi tahu programmer lain (dan alat pengecekan tipe seperti mypy) tipe apa yang kamu harapkan, tetapi Python tidak menegakkannya saat runtime. Ini konsisten dengan filosofi dynamic typing Python.

Namun, mengikuti type annotation membuat kode kamu lebih bisa diprediksi dan lebih mudah di-debug. Ketika kamu memakai tipe yang salah, error akan muncul belakangan saat kamu mencoba memakai data tersebut, membuat bug lebih sulit dilacak. Alat pengecekan tipe dapat menangkap ketidaksesuaian ini sebelum kamu menjalankan kode, membantu kamu menemukan masalah lebih awal.

Mengakses dan Mengubah Atribut

Instance data class bekerja persis seperti instance class biasa. Kamu mengakses dan mengubah atribut menggunakan dot notation:

python
from dataclasses import dataclass
 
@dataclass
class Employee:
    name: str
    position: str
    salary: float
 
emp = Employee("Sarah Chen", "Software Engineer", 95000.0)
 
# Mengakses atribut
print(emp.name)      # Output: Sarah Chen
print(emp.position)  # Output: Software Engineer
 
# Mengubah atribut
emp.salary = 100000.0
emp.position = "Senior Software Engineer"
 
print(emp)  # Output: Employee(name='Sarah Chen', position='Senior Software Engineer', salary=100000.0)

Data class secara default bersifat mutable—kamu bisa mengubah atributnya setelah dibuat. Ini berbeda dari tuple atau named tuple, yang immutable. Jika kamu membutuhkan immutability, kamu bisa mengonfigurasi data class dengan frozen=True (kita akan membahas ini di Bagian 33.4).

33.3) Method yang Dihasilkan: __init__, __repr__, dan __eq__

Decorator @dataclass otomatis menghasilkan tiga method penting. Memahami apa yang dilakukan method-method ini membantumu memakai data class secara efektif dan tahu kapan harus mengustomisasinya.

Method __init__ yang Dihasilkan

Method __init__ menginisialisasi instance baru dengan nilai yang diberikan. Python menghasilkannya berdasarkan urutan definisi atributmu:

python
from dataclasses import dataclass
 
@dataclass
class Rectangle:
    width: float
    height: float
 
# __init__ yang dihasilkan menerima width dan height dalam urutan itu
rect = Rectangle(10.5, 5.0)
print(rect.width)   # Output: 10.5
print(rect.height)  # Output: 5.0
 
# Kamu juga bisa memakai keyword argument
rect2 = Rectangle(height=8.0, width=12.0)
print(rect2.width)   # Output: 12.0
print(rect2.height)  # Output: 8.0

__init__ yang dihasilkan setara dengan menulis:

python
def __init__(self, width: float, height: float):
    self.width = width
    self.height = height

Pembuatan otomatis ini menghematmu dari menulis kode inisialisasi yang berulang, terutama untuk class dengan banyak atribut.

Method __repr__ yang Dihasilkan

Method __repr__ menyediakan representasi string dari instance yang menampilkan semua nilai atribut. Ini sangat berharga untuk debugging dan logging:

python
from dataclasses import dataclass
 
@dataclass
class Point:
    x: float
    y: float
    label: str
 
point = Point(3.5, 7.2, "A")
print(point)  # Output: Point(x=3.5, y=7.2, label='A')
print(repr(point))  # Output: Point(x=3.5, y=7.2, label='A')

__repr__ yang dihasilkan mengikuti konvensi untuk menampilkan nama class dan semua atribut dalam format yang bisa dipakai untuk membuat ulang objek tersebut. Ini jauh lebih membantu daripada representasi default yang akan kamu dapatkan tanpa __repr__: <__main__.Point object at 0x...>.

Method __eq__ yang Dihasilkan

Method __eq__ memungkinkan perbandingan kesetaraan antar-instance. Dua instance data class dianggap sama jika semua atribut yang bersesuaian sama:

python
from dataclasses import dataclass
 
@dataclass
class Color:
    red: int
    green: int
    blue: int
 
color1 = Color(255, 0, 0)
color2 = Color(255, 0, 0)
color3 = Color(0, 255, 0)
 
print(color1 == color2)  # Output: True (same RGB values)
print(color1 == color3)  # Output: False (different RGB values)
print(color1 is color2)  # Output: False (different objects in memory)

Perbandingan kesetaraan otomatis ini didasarkan pada kesetaraan nilai, bukan identitas. Walaupun color1 dan color2 adalah objek yang berbeda di memori (seperti ditunjukkan oleh is), mereka dianggap sama karena atributnya cocok.

Method __eq__ yang dihasilkan membandingkan atribut dalam urutan seperti didefinisikan:

python
from dataclasses import dataclass
 
@dataclass
class Book:
    title: str
    author: str
    year: int
 
book1 = Book("1984", "George Orwell", 1949)
book2 = Book("1984", "George Orwell", 1949)
book3 = Book("Animal Farm", "George Orwell", 1945)
 
print(book1 == book2)  # Output: True (all attributes match)
print(book1 == book3)  # Output: False (title and year differ)
 
# Perbandingan dengan objek non-Book mengembalikan False
print(book1 == "1984")  # Output: False
print(book1 == None)    # Output: False

Membandingkan Method yang Dihasilkan dengan Implementasi Manual

Untuk mengapresiasi apa yang disediakan data class, mari bandingkan versi data class dengan implementasi manual:

python
from dataclasses import dataclass
 
# Versi data class (ringkas)
@dataclass
class PersonData:
    first_name: str
    last_name: str
    age: int
 
# Versi manual yang setara (panjang)
class PersonManual:
    def __init__(self, first_name: str, last_name: str, age: int):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
    
    def __repr__(self):
        return f"PersonManual(first_name={self.first_name!r}, last_name={self.last_name!r}, age={self.age})"
    
    def __eq__(self, other):
        if not isinstance(other, PersonManual):
            return False
        return (self.first_name == other.first_name and
                self.last_name == other.last_name and
                self.age == other.age)
 
# Keduanya bekerja secara identik
p1 = PersonData("Alice", "Johnson", 30)
p2 = PersonManual("Alice", "Johnson", 30)
 
print(p1)  # Output: PersonData(first_name='Alice', last_name='Johnson', age=30)
print(p2)  # Output: PersonManual(first_name='Alice', last_name='Johnson', age=30)

Versi data class mencapai fungsionalitas yang sama dengan kode yang jauh lebih sedikit. Pengurangan boilerplate ini membuat kode kamu lebih mudah dibaca, dirawat, dan dimodifikasi.

Menambahkan Method Kustom ke Data Class

Data class bisa punya method kustom seperti class biasa. Decorator @dataclass hanya menghasilkan method inisialisasi, representasi, dan kesetaraan—kamu bebas menambahkan fungsionalitas lain apa pun:

python
from dataclasses import dataclass
 
@dataclass
class Temperature:
    celsius: float
    
    def to_fahrenheit(self):
        """Convert temperature to Fahrenheit."""
        return (self.celsius * 9/5) + 32
    
    def to_kelvin(self):
        """Convert temperature to Kelvin."""
        return self.celsius + 273.15
    
    def is_freezing(self):
        """Check if temperature is at or below freezing point."""
        return self.celsius <= 0
 
temp = Temperature(25.0)
print(temp)  # Output: Temperature(celsius=25.0)
print(f"{temp.celsius}°C = {temp.to_fahrenheit()}°F")  # Output: 25.0°C = 77.0°F
print(f"Kelvin: {temp.to_kelvin()}")  # Output: Kelvin: 298.15
print(f"Freezing: {temp.is_freezing()}")  # Output: Freezing: False
 
cold_temp = Temperature(-5.0)
print(f"Freezing: {cold_temp.is_freezing()}")  # Output: Freezing: True

Data class menangani bagian yang repetitif (inisialisasi, representasi, dan perbandingan) sambil membiarkanmu menambahkan method kustom sesuai kebutuhan spesifikmu, seperti ditunjukkan pada method konversi suhu di atas.

33.4) Nilai Default dan Opsi Field

Data class mendukung nilai default untuk atribut, memungkinkanmu membuat instance tanpa harus menentukan setiap parameter. Kamu juga bisa memakai fungsi field() untuk mengonfigurasi perilaku lanjutan seperti mengecualikan atribut dari perbandingan atau mengontrol bagaimana atribut tersebut muncul dalam representasi string.

Memberikan Nilai Default

Kamu bisa menetapkan nilai default ke atribut secara langsung di definisi class. Atribut dengan default harus diletakkan setelah atribut tanpa default:

python
from dataclasses import dataclass
 
@dataclass
class User:
    username: str
    email: str
    is_active: bool = True  # Nilai default
    role: str = "user"      # Nilai default
 
# Create instances with and without defaults
user1 = User("alice", "alice@example.com")
print(user1)  # Output: User(username='alice', email='alice@example.com', is_active=True, role='user')
 
user2 = User("bob", "bob@example.com", False, "admin")
print(user2)  # Output: User(username='bob', email='bob@example.com', is_active=False, role='admin')
 
# Use keyword arguments to override specific defaults
user3 = User("charlie", "charlie@example.com", role="moderator")
print(user3)  # Output: User(username='charlie', email='charlie@example.com', is_active=True, role='moderator')

Aturan urutan (atribut tanpa default sebelum atribut dengan default) mencegah ambiguitas pada method __init__ yang dihasilkan. Ini adalah persyaratan yang sama seperti pada parameter fungsi dengan nilai default, yang kita pelajari di Bab 20.

Nilai Default Mutable dan Mengapa Itu Tidak Diizinkan

Data class melindungimu dari kesalahan umum terkait default yang mutable. Jika kamu mencoba memakai objek mutable seperti list atau dictionary secara langsung sebagai default, kamu akan mendapatkan error:

python
from dataclasses import dataclass
 
# Ini akan memunculkan error
@dataclass
class ShoppingCart:
    customer: str
    items: list = []  # ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory

Error ini mencegah masalah yang sama seperti yang kita lihat pada default argument fungsi di Bab 20, di mana semua instance akan berbagi objek mutable yang sama.

Menggunakan field() dengan default_factory untuk Default Mutable

Solusinya adalah memakai fungsi field() dengan default_factory, yang membuat nilai default baru untuk setiap instance:

python
from dataclasses import dataclass, field
 
@dataclass
class ShoppingCart:
    customer: str
    items: list = field(default_factory=list)  # Benar: list baru per instance
 
# Sekarang setiap instance mendapatkan list-nya sendiri
cart1 = ShoppingCart("Alice")
cart1.items.append("Book")
print(cart1.items)  # Output: ['Book']
 
cart2 = ShoppingCart("Bob")
print(cart2.items)  # Output: [] - Bob has an empty list
 
cart2.items.append("Laptop")
print(cart1.items)  # Output: ['Book'] - Alice's cart unchanged
print(cart2.items)  # Output: ['Laptop'] - Bob's cart independent

Parameter default_factory menerima sebuah fungsi (seperti list, dict, atau set) yang akan dipanggil untuk membuat nilai default baru setiap kali kamu membuat instance tanpa memberikan atribut tersebut. Misalnya, default_factory=list berarti Python akan memanggil list() untuk membuat list kosong baru untuk setiap instance.

Mengecualikan Field dari Perbandingan

Kadang kamu ingin atribut tertentu dikecualikan dari perbandingan kesetaraan. Gunakan field(compare=False) untuk ini:

python
from dataclasses import dataclass, field
from datetime import datetime
 
@dataclass
class LogEntry:
    message: str
    level: str
    timestamp: datetime = field(compare=False)  # Jangan bandingkan timestamp
 
# Membuat dua log entry dengan message yang sama tapi waktu berbeda
entry1 = LogEntry("User logged in", "INFO", datetime(2024, 1, 15, 10, 30))
entry2 = LogEntry("User logged in", "INFO", datetime(2024, 1, 15, 10, 35))
 
# Mereka sama karena timestamp dikecualikan dari perbandingan
print(entry1 == entry2)  # Output: True
 
# Tapi mereka punya timestamp yang berbeda
print(entry1.timestamp)  # Output: 2024-01-15 10:30:00
print(entry2.timestamp)  # Output: 2024-01-15 10:35:00

Ini berguna ketika kamu punya field metadata (seperti timestamp, ID, atau counter internal) yang seharusnya tidak memengaruhi apakah dua instance dianggap sama.

Mengecualikan Field dari Representasi

Kamu juga bisa mengecualikan field dari representasi string menggunakan field(repr=False):

python
from dataclasses import dataclass, field
 
@dataclass
class Account:
    username: str
    email: str
    password: str = field(repr=False)  # Jangan tampilkan password di repr
 
account = Account("alice", "alice@example.com", "secret123")
print(account)  # Output: Account(username='alice', email='alice@example.com')
# Password tidak ditampilkan, tapi tetap disimpan
print(account.password)  # Output: secret123

Ini sangat berguna untuk data sensitif seperti password, API key, atau struktur data besar yang akan mengotori representasi.

Membuat Data Class Immutable dengan frozen=True

Secara default, instance data class bersifat mutable—kamu bisa mengubah atributnya setelah dibuat. Jika kamu ingin instance yang immutable (seperti tuple), gunakan frozen=True:

python
from dataclasses import dataclass
 
@dataclass(frozen=True)
class Point:
    x: float
    y: float
 
point = Point(3.0, 4.0)
print(point)  # Output: Point(x=3.0, y=4.0)
 
# Mencoba mengubah akan memunculkan error
try:
    point.x = 5.0
except AttributeError as e:
    print(f"Error: {e}")  # Output: Error: cannot assign to field 'x'

Data class frozen berguna ketika kamu ingin memastikan integritas data atau memakai instance sebagai key dictionary (karena key dictionary harus immutable). Ketika sebuah data class dibuat frozen, Python juga menghasilkan method __hash__, membuat instance menjadi hashable:

python
from dataclasses import dataclass
 
@dataclass(frozen=True)
class Coordinate:
    latitude: float
    longitude: float
 
# Instance frozen bisa menjadi key dictionary
locations = {
    Coordinate(40.7128, -74.0060): "New York",
    Coordinate(51.5074, -0.1278): "London",
    Coordinate(35.6762, 139.6503): "Tokyo"
}
 
nyc = Coordinate(40.7128, -74.0060)
print(locations[nyc])  # Output: New York

33.5) Inisialisasi Kustom dengan __post_init__

Terkadang kamu perlu melakukan setup tambahan setelah method __init__ yang dihasilkan dijalankan. Method __post_init__ dipanggil otomatis setelah inisialisasi, memungkinkanmu memvalidasi data, menghitung atribut turunan, atau melakukan tugas setup lainnya.

Penggunaan Dasar __post_init__

Method __post_init__ dipanggil setelah semua atribut sudah diset oleh __init__ yang dihasilkan:

python
from dataclasses import dataclass
 
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = 0.0  # Akan dihitung di __post_init__
    
    def __post_init__(self):
        """Calculate area after initialization."""
        self.area = self.width * self.height
 
rect = Rectangle(5.0, 3.0)
print(rect)  # Output: Rectangle(width=5.0, height=3.0, area=15.0)
print(f"Area: {rect.area}")  # Output: Area: 15.0

Method __post_init__ memiliki akses ke semua atribut instance yang diset saat inisialisasi. Ini berguna untuk menghitung nilai turunan yang bergantung pada beberapa atribut.

Memvalidasi Data di post_init

Penggunaan umum __post_init__ adalah memvalidasi bahwa data yang diberikan memenuhi persyaratan tertentu:

python
from dataclasses import dataclass
 
@dataclass
class BankAccount:
    account_number: str
    balance: float
    
    def __post_init__(self):
        """Validate account data."""
        if self.balance < 0:
            raise ValueError("Balance cannot be negative")
 
# Akun yang valid
account1 = BankAccount("ACC001", 1000.0)
print(account1)  # Output: BankAccount(account_number='ACC001', balance=1000.0)
 
# Akun tidak valid - saldo negatif
try:
    account2 = BankAccount("ACC002", -500.0)
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Balance cannot be negative

Validasi ini memastikan bahwa instance selalu berada dalam keadaan yang valid. Jika data tidak memenuhi persyaratan, instance tidak pernah dibuat, mencegah objek yang tidak valid ada di programmu.

Menggunakan post_init dengan field(init=False)

Terkadang kamu menginginkan atribut yang dihitung di __post_init__ tetapi seharusnya tidak menjadi parameter di __init__. Gunakan field(init=False) untuk ini:

python
from dataclasses import dataclass, field
import math
 
@dataclass
class Circle:
    radius: float
    area: float = field(init=False)  # Bukan parameter di __init__
    circumference: float = field(init=False)
    
    def __post_init__(self):
        """Compute area and circumference from radius."""
        self.area = math.pi * self.radius ** 2
        self.circumference = 2 * math.pi * self.radius
 
# Hanya radius yang diperlukan saat inisialisasi
circle = Circle(5.0)
print(circle)  # Output: Circle(radius=5.0, area=78.53981633974483, circumference=31.41592653589793)
print(f"Area: {circle.area:.2f}")  # Output: Area: 78.54
print(f"Circumference: {circle.circumference:.2f}")  # Output: Circumference: 31.42

Pola ini berguna ketika kamu punya atribut yang selalu dihitung dari atribut lain dan seharusnya tidak pernah diset langsung saat inisialisasi.


Data class merepresentasikan fitur Python modern yang mengurangi boilerplate sambil tetap mempertahankan kekuatan penuh dari class. Mereka sangat berharga untuk membuat kode yang bersih dan mudah dibaca saat bekerja dengan data terstruktur. Saat kamu terus belajar Python, kamu akan menemukan data class menjadi pilihan yang natural untuk banyak tugas pemrograman yang berfokus pada data, melengkapi class biasa yang kamu pelajari di Bab 30-32.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai