Program
Data class adalah salah satu fitur Python yang, setelah Anda menemukannya, Anda tidak akan kembali ke cara lama. Pertimbangkan kelas biasa berikut:
class Exercise:
def __init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
Bagi saya, definisi kelas itu sangat tidak efisien — di metode __init__, Anda mengulang setiap parameter setidaknya tiga kali. Ini mungkin terdengar sepele, tetapi pikirkan seberapa sering Anda menulis kelas seumur hidup dengan parameter yang jauh lebih banyak.
Sebagai perbandingan, lihat alternatif data class dari kode di atas:
from dataclasses import dataclass
@dataclass
class Exercise:
name: str
reps: int
sets: int
weight: float # Weight in lbs
Potongan kode yang tampak sederhana ini jauh lebih baik daripada kelas biasa. Dekorator kecil @dataclass mengimplementasikan kelas __init__, __repr__, __eq__ di balik layar, yang jika ditulis manual akan memakan setidaknya 20 baris kode.
Selain itu, banyak fitur lain seperti operator perbandingan, pengurutan objek, dan imutabilitas, semuanya hanya berjarak satu baris untuk tercipta secara “ajaib” bagi kelas kita.
Jadi, tujuan tutorial ini adalah menunjukkan mengapa data class adalah salah satu hal terbaik yang terjadi pada Python jika Anda menyukai pemrograman berorientasi objek.
Mari kita mulai!
Dasar-dasar Data Class Python
Mari bahas beberapa konsep mendasar dari data class Python yang membuatnya begitu berguna.
Beberapa metode dihasilkan secara otomatis dalam data class
Meski penuh fitur, data class adalah kelas biasa yang membutuhkan jauh lebih sedikit kode untuk mengimplementasikan fungsionalitas yang sama. Ini kelas Exercise lagi:
from dataclasses import dataclass
@dataclass
class Exercise:
name: str
reps: int
sets: int
weight: float
ex1 = Exercise("Bench press", 10, 3, 52.5)
# Verifying Exercise is a regular class
ex1.name
'Bench press'
Saat ini, Exercise sudah memiliki metode __repr__ dan __eq__ yang terimplementasi. Mari kita verifikasi:
repr(ex1)
"Exercise(name='Bench press', reps=10, sets=3, weight=52.5)"
Representasi objek oleh repr harus mengembalikan kode yang dapat merekreasinya, dan kita bisa lihat itulah yang terjadi pada ex1.
Sebagai perbandingan, Exercise yang didefinisikan dengan cara lama akan terlihat seperti ini:
class Exercise:
def __init__(self, name, reps, sets, weight):
self.name = name
self.reps = reps
self.sets = sets
self.weight = weight
ex3 = Exercise("Bench press", 10, 3, 52.5)
ex3
<__main__.Exercise at 0x7f6834100130>
Terlihat buruk dan tidak berguna!
Sekarang, mari verifikasi keberadaan __eq__, yaitu operator kesetaraan:
# Redefine the class
@dataclass
class Exercise:
name: str
reps: int
sets: int
weight: float
ex1 = Exercise("Bench press", 10, 3, 52.5)
ex2 = Exercise("Bench press", 10, 3, 52.5)
Membandingkan kelas dengan dirinya sendiri dan dengan kelas lain yang parameternya identik harus menghasilkan True:
ex1 == ex2
True
ex1 == ex1
True
Dan memang begitu! Dalam kelas biasa, logika ini akan merepotkan untuk ditulis.
Data class memerlukan type hint
Seperti yang mungkin Anda perhatikan, data class memerlukan type hint saat mendefinisikan field. Faktanya, data class mengizinkan tipe apa pun dari modul typing. Misalnya, berikut cara membuat field yang dapat menerima tipe data Any:
from typing import Any
@dataclass
class Dummy:
attr: Any
Namun, keunikan Python adalah meskipun data class memerlukan type hint, tipe tersebut sebenarnya tidak dipaksakan.
Sebagai contoh, membuat instance kelas Exercise dengan tipe data yang sama sekali tidak benar tetap dapat dijalankan tanpa error:
silly_exercise = Exercise("Bench press", "ten", "three sets", 52.5)
silly_exercise.sets
“Three sets”
Jika Anda ingin memaksa tipe data, Anda harus menggunakan type checker seperti Mypy.
Data class memungkinkan nilai default pada field
Sejauh ini, kita belum menambahkan nilai default pada kelas kita. Mari kita perbaiki:
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float = 0
# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-ups', reps=10, sets=3, weight=0)
Ingat bahwa field non-default tidak boleh berada setelah field default. Misalnya, kode di bawah ini akan melempar error:
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float # NOT ALLOWED
ex5 = Exercise()
ex5
TypeError: non-default argument 'weight' follows default argument
Dalam praktiknya, Anda jarang mendefinisikan default dengan sintaks name: type = value.
Sebagai gantinya, Anda akan menggunakan fungsi field yang memberi kontrol lebih pada setiap definisi field:
from dataclasses import field
@dataclass
class Exercise:
name: str = field(default="Push-up")
reps: int = field(default=10)
sets: int = field(default=3)
weight: float = field(default=0)
# Now, all fields have defaults
ex5 = Exercise()
ex5
Exercise(name='Push-up', reps=10, sets=3, weight=0)
Fungsi field memiliki lebih banyak parameter, seperti:
reprinitcomparedefault_factory
dan seterusnya. Kita akan membahas ini di bagian berikutnya.
Data class dapat dibuat dengan fungsi
Catatan terakhir tentang dasar data class adalah bahwa definisinya bisa lebih singkat lagi dengan menggunakan fungsi make_dataclass:
from dataclasses import make_dataclass
Exercise = make_dataclass(
"Exercise",
[
("name", str),
("reps", int),
("sets", int),
("weight", float),
],
)
ex3 = Exercise("Deadlifts", 8, 3, 69.0)
ex3
Exercise(name='Deadlifts', reps=8, sets=3, weight=69.0)
Namun Anda akan mengorbankan keterbacaan, jadi saya tidak menyarankan menggunakan fungsi ini.
Data Class Python Lanjutan
Di bagian ini, kita akan membahas fitur-fitur lanjutan data class yang memberikan lebih banyak manfaat. Salah satunya adalah default factory.
Default factory
Untuk menjelaskan default factory, mari buat kelas lain bernama WorkoutSession yang menerima dua field:
from dataclasses import dataclass
from typing import List
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float = 0
@dataclass
class WorkoutSession:
exercises: List[Exercise]
duration_minutes: int
Dengan menggunakan tipe List, kita menyatakan bahwa WorkoutSession menerima daftar instance Exercise.
# Define the Exercise instances for HIIT training
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex2 = Exercise(name="Mountain Climbers", reps=20, sets=3)
ex3 = Exercise(name="Jump Squats", reps=12, sets=3)
exercises_monday = [ex1, ex2, ex3]
hiit_monday = WorkoutSession(exercises=exercises_monday, duration_minutes=30)
Saat ini, setiap instance sesi latihan memerlukan exercises untuk diinisialisasi. Namun ini tidak mencerminkan cara orang berolahraga — pertama, mereka memulai sesi (mungkin di aplikasi), lalu menambahkan latihan seiring berjalannya waktu.
Jadi, kita harus bisa membuat sesi tanpa latihan dan tanpa durasi. Mari wujudkan ini dengan menambahkan daftar kosong sebagai nilai default untuk exercises:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = []
duration_minutes: int = None
hiit_monday = WorkoutSession("25-02-2024")
ValueError: mutable default <class 'list'> for field exercises is not allowed: use default_factory
Namun, kita mendapat error — ternyata data class tidak mengizinkan nilai default yang mutable.
Untungnya, kita bisa memperbaikinya dengan menggunakan default factory:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=list) # PAY ATTENTION
duration_minutes: int = 0
hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[], duration_minutes=0)
Parameter default_factory menerima fungsi yang mengembalikan nilai awal untuk field data class. Artinya, ia dapat menerima fungsi apa pun:
tupledictset- Fungsi kustom buatan pengguna apa pun
Ini berlaku terlepas dari apakah hasil fungsi tersebut mutable atau tidak.
Sekarang, jika dipikir-pikir, kebanyakan orang memulai latihan dengan pemanasan yang biasanya serupa untuk jenis latihan apa pun. Jadi, menginisialisasi sesi tanpa latihan mungkin bukan yang diinginkan sebagian orang.
Sebagai gantinya, mari buat fungsi yang mengembalikan tiga Exercise pemanasan:
def create_warmup():
return [
Exercise("Jumping jacks", 30, 1),
Exercise("Squat lunges", 10, 2),
Exercise("High jumps", 20, 1),
]
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5 # Increase the default duration as well
hiit_monday = WorkoutSession()
hiit_monday
WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)
Sekarang, setiap kali kita membuat sesi, sesi tersebut akan memuat beberapa latihan pemanasan yang sudah tercatat. Versi baru WorkoutSession memiliki durasi default lima menit untuk mengakomodasi hal itu.
Menambahkan metode ke data class
Karena data class adalah kelas biasa, menambahkan metode ke dalamnya tetap sama. Mari tambahkan dua metode ke data class WorkoutSession kita:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
def add_exercise(self, exercise: Exercise):
self.exercises.append(exercise)
def increase_duration(self, minutes: int):
self.duration_minutes += minutes
Dengan menggunakan metode-metode ini, sekarang kita dapat mencatat aktivitas baru ke sebuah sesi:
hiit_monday = WorkoutSession()
# Log a new exercise
new_exercise = Exercise("Deadlifts", 6, 4, 60)
hiit_monday.add_exercise(new_exercise)
hiit_monday.increase_duration(15)
Tetapi ada masalah:
hiit_monday
WorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), Exercise(name='Squat lunges', reps=10, sets=2, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0), Exercise(name='Deadlifts', reps=6, sets=4, weight=60)], duration_minutes=20)
Saat kita mencetak sesi, representasi default-nya terlalu verbose dan sulit dibaca karena berisi kode untuk merekreasikan objek. Mari kita perbaiki.
__repr__ dan __str__ dalam data class
Data class mengimplementasikan __repr__ secara otomatis tetapi tidak __str__. Ini membuat kelas menggunakan __repr__ ketika kita memanggil print pada objek tersebut.
Jadi, mari timpa perilaku ini dengan mendefinisikan __str__ sendiri:
@dataclass
class Exercise:
name: str = "Push-ups"
reps: int = 10
sets: int = 3
weight: float = 0
def __str__(self):
base = f"{self.name}: {self.reps}/{self.sets}"
if self.weight == 0:
return base
return base + f", {self.weight} lbs"
ex1 = Exercise(name="Burpees", reps=15, sets=3)
ex1
Exercise(name='Burpees', reps=15, sets=3, weight=0)
__repr__ masih sama, tetapi ketika kita memanggil print:
print(ex1)
Burpees: 15/3
Representasi string kelas jauh lebih enak dibaca. Sekarang, mari perbaiki WorkoutSession juga:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5 # Increase the default duration as well
def add_exercise(self, exercise: Exercise):
self.exercises.append(exercise)
def increase_duration(self, minutes: int):
self.duration_minutes += minutes
def __str__(self):
base = ""
for ex in self.exercises:
base += str(ex) + "\n"
base += f"\nSession duration: {self.duration_minutes} minutes."
return base
hiit_monday = WorkoutSession()
print(hiit_monday)
Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1
Session duration: 5 minutes.
Catatan: Gunakan tombol “Explain code” di bagian bawah cuplikan untuk mendapatkan penjelasan kode baris demi baris.
Sekarang, kita mendapatkan keluaran yang ringkas dan mudah dibaca.
Perbandingan dalam data class
Untuk banyak kelas, masuk akal untuk membandingkan objeknya dengan suatu logika. Untuk latihan, ini bisa berupa durasi latihan, intensitas, atau beban.
Pertama, mari lihat apa yang terjadi jika kita mencoba membandingkan dua sesi latihan dalam keadaan saat ini:
hiit_wednesday = WorkoutSession()
hiit_wednesday.add_exercise(Exercise("Pull-ups", 7, 3))
print(hiit_wednesday)
Jumping jacks: 30/1
Squat lunges: 10/2
High jumps: 20/1
Pull-ups: 7/3
Session duration: 5 minutes.
hiit_monday > hiit_wednesday
TypeError: '>' not supported between instances of 'WorkoutSession' and 'WorkoutSession'
Kita menerima TypeError karena data class tidak mengimplementasikan operator perbandingan. Namun ini mudah diperbaiki dengan menyetel parameter order ke True:
@dataclass(order=True)
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
...
hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)
hiit_monday.increase_duration(10)
hiit_wednesday = WorkoutSession()
hiit_monday > hiit_wednesday
True
Kali ini, perbandingan berhasil, tetapi apa yang sebenarnya kita bandingkan?
Dalam data class, perbandingan dilakukan sesuai urutan field didefinisikan. Saat ini, kelas dibandingkan berdasarkan durasi latihan karena field pertama, exercises, berisi objek non-standar.
Kita bisa memverifikasi ini dengan menambah durasi sesi hari Rabu:
hiit_monday = WorkoutSession()
# hiit_monday.add_exercise(...)
hiit_wednesday = WorkoutSession()
hiit_wednesday.increase_duration(10)
hiit_monday > hiit_wednesday
False
Sesuai dugaan, kita menerima False.
Namun apa yang terjadi jika field pertama Workout adalah tipe lain, misalnya string? Mari kita coba:
@dataclass(order=True)
class WorkoutSession:
date: str = None # DD-MM-YYYY
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
...
hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)
hiit_wednesday = WorkoutSession("27-02-2024")
hiit_monday > hiit_wednesday
False
Meskipun sesi Senin berlangsung lebih lama, perbandingan menunjukkan bahwa sesi tersebut lebih kecil daripada Rabu. Alasannya adalah “25” datang sebelum “27” dalam perbandingan string Python.
Jadi, bagaimana kita mempertahankan urutan field dan tetap mengurutkan sesi berdasarkan durasi latihan? Ini mudah melalui fungsi field:
@dataclass(order=True)
class WorkoutSession:
date: str = field(default=None, compare=False)
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
...
hiit_monday = WorkoutSession("25-02-2024")
hiit_monday.increase_duration(10)
hiit_wednesday = WorkoutSession("27-02-2024")
hiit_monday > hiit_wednesday
True
Dengan menyetel compare ke False untuk field apa pun, kita mengecualikannya dari pengurutan, seperti yang dibuktikan oleh hasil di atas.
Manipulasi field pasca-inisialisasi
Saat ini, kita memiliki durasi sesi default lima menit untuk mengakomodasi latihan pemanasan. Namun, ini hanya masuk akal jika pengguna memulai sesi dengan pemanasan. Bagaimana jika mereka memulai sesi dengan latihan lain:
new_session = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])
new_session.duration_minutes
5
Untuk satu latihan saja, total durasi lima menit terasa tidak masuk akal. Setiap sesi harus secara dinamis memperkirakan durasinya berdasarkan jumlah set dari tiap latihan. Artinya kita harus membuat duration_minutes bergantung pada field exercises.
Mari kita implementasikan:
@dataclass
class WorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = field(default=0, init=False)
def __post_init__(self):
set_duration = 3
for ex in self.exercises:
self.duration_minutes += ex.sets * set_duration
...
Kali ini, kita mendefinisikan duration_minutes dengan init disetel ke False untuk menunda inisialisasi field.
Lalu, di dalam metode khusus __post_init__, kita memperbarui nilainya berdasarkan total jumlah set di setiap Exercise.
Sekarang, ketika kita menginisialisasi WorkoutSession, duration_minutes secara dinamis bertambah tiga menit untuk setiap set di tiap latihan.
# Adding an exercise with three sets
hiit_friday = WorkoutSession([Exercise("Diamond push-ups", 10, 3)])
hiit_friday.duration_minutes
9
Secara umum, jika Anda ingin mendefinisikan field yang bergantung pada field lain dari data class Anda, Anda dapat menggunakan logika __post_init__.
Imutabilitas dalam Data Class
Data class WorkoutSession kita hampir siap; tinggal perlu dilindungi. Saat ini, kelas ini mudah sekali diutak-atik:
hiit_friday.duration_minutes = 1000
hiit_friday.duration_minutes
1000
del hiit_friday.exercises
Kita ingin melindungi semua field kelas kita agar hanya bisa dimodifikasi dengan cara yang kita inginkan. Untuk mencapai ini, dekorator @dataclass menawarkan argumen frozen yang praktis:
@dataclass(frozen=True)
class FrozenExercise:
name: str
reps: int
sets: int
weight: int | float = 0
ex1 = FrozenExercise("Muscle-ups", 5, 3)
Sekarang, jika kita ingin mengubah field apa pun, kita mendapat error:
ex1.sets = 5
FrozenInstanceError: cannot assign to field 'sets'
Menyetel frozen ke True secara otomatis menambahkan metode __deleteattr__ dan __setattr__ untuk setiap field sehingga terlindung dari penghapusan atau pembaruan setelah inisialisasi. Juga, orang lain tidak akan bisa menambahkan field baru:
ex1.new_field = 10
FrozenInstanceError: cannot assign to field 'new_field'
Fungsionalitas ini akan membutuhkan puluhan baris kode jika kita menggunakan kelas tradisional.
Namun, perlu dicatat bahwa kita tidak bisa membuat kelas benar-benar immutable. Misalnya, mari tulis ulang WorkoutSession dengan frozen disetel ke True:
@dataclass(frozen=True)
class ImmutableWorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
session1 = ImmutableWorkoutSession()
Seperti yang diharapkan, kita tidak bisa langsung memodifikasi daftar latihan:
session1.exercises = [Exercise()]
Namun, exercises adalah daftar, yang sepenuhnya mutable, sehingga operasi berikut memungkinkan:
# Mengubah salah satu elemen dalam daftar
# Changing one of the elements in a list
session1.exercises[1] = FrozenExercise("Totally new exercise", 5, 5)
print(session1)
ImmutableWorkoutSession(exercises=[Exercise(name='Jumping jacks', reps=30, sets=1, weight=0), FrozenExercise(name='Totally new exercise', reps=5, sets=5, weight=0), Exercise(name='High jumps', reps=20, sets=1, weight=0)], duration_minutes=5)
Jadi, untuk melindungi dari perubahan yang tidak disengaja, disarankan menggunakan objek immutable seperti tuple untuk nilai field.
Pewarisan dalam data class
Satu poin terakhir yang akan kita bahas adalah urutan field di kelas induk dan turunan.
Karena data class adalah kelas biasa, pewarisan berjalan seperti biasa:
@dataclass(frozen=True)
class ImmutableWorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
pass
Namun, karena field terakhir di kelas induk (ImmutableWorkoutSession) memiliki nilai default, semua field di kelas turunan harus memiliki nilai default.
Sebagai contoh, ini tidak diizinkan:
@dataclass(frozen=True)
class ImmutableWorkoutSession:
exercises: List[Exercise] = field(default_factory=create_warmup)
duration_minutes: int = 5
@dataclass(frozen=True)
class CardioWorkoutSession(ImmutableWorkoutSession):
intensity_level: str # Not allowed, must have a default
TypeError: non-default argument 'intensity_level' follows default argument
Kekurangan Data Class dan Sumber Lanjutan
Data class terus meningkat sejak Python 3.7 (sejak awal pun sudah bagus) dan mencakup banyak kasus penggunaan saat Anda perlu menulis kelas. Namun, data class bisa kurang menguntungkan dalam skenario berikut:
- Metode
__init__kustom - Metode
__new__kustom - Beragam pola pewarisan
Dan banyak lagi, seperti dibahas dalam thread Reddit yang bagus ini. Jika Anda menginginkan alasan lebih rinci mengapa data class diperkenalkan dan mengapa mereka bukan pengganti langsung definisi kelas biasa, bacalah PEP 557.
Jika Anda tertarik pada pemrograman berorientasi objek secara umum, berikut kursus untuk melanjutkan perjalanan Anda:
Pada dasarnya, data class adalah struktur yang lebih canggih untuk menyimpan dan mengambil data dengan lebih efisien. Namun, Python memiliki banyak struktur data lain yang melakukan tugas ini dengan cara yang kurang lebih serupa. Misalnya, Anda dapat mempelajari counter, defaultdict, dan namedtuple di bab terakhir dari kursus Data Types for Data Science.

Saya adalah pembuat konten ilmu data dengan pengalaman lebih dari 2 tahun dan salah satu dengan jumlah pengikut terbesar di Medium. Saya suka menulis artikel mendetail tentang AI dan ML dengan sedikit gaya sarkastik karena harus ada sesuatu untuk membuatnya sedikit kurang membosankan. Saya telah menghasilkan lebih dari 130 artikel dan satu kursus DataCamp, dengan satu lagi sedang dalam proses. Konten saya telah dilihat oleh lebih dari 5 juta pasang mata, dengan 20 ribu di antaranya menjadi pengikut di Medium dan LinkedIn.
