Dapatkan pemahaman penuh tentang dasar-dasar kelas data dengan Python

Jika Anda ingin merasakan sendiri Medium, pertimbangkan untuk mendukung saya dan ribuan penulis lainnya dengan mendaftar menjadi anggota. Biayanya hanya $5 per bulan, ini sangat mendukung kami, para penulis, dan Anda dapat mengakses semua cerita menakjubkan di Medium.

Apa itu Kelas Data?

Kelas data adalah fitur baru yang tersedia di bawaan Python sejak versi 3.7. Mereka menyediakan dekorator dan fungsi untuk membuat kelas yang lebih sederhana, nyaman, dan aman yang terutama digunakan untuk memproses data, itulah namanya.

Salah satu manfaat utama Kelas Data adalah kelas ini secara otomatis menghasilkan beberapa metode khusus untuk Anda, seperti __init__, __repr__, dan __eq__. Hal ini dapat menghemat banyak waktu dan kode yang berlebihan saat menentukan kelas yang terutama digunakan untuk memproses data.

Manfaat utama lainnya dari Kelas Data adalah penggunaan pengetikan yang kuat, yang memastikan bahwa atribut suatu instance ditentukan. Hal ini dicapai melalui penggunaan anotasi tipe, yang memungkinkan Anda menentukan tipe setiap atribut saat mendefinisikan kelas. Hal ini dapat mencegah bug karena ambiguitas tipe dan juga membuat kode lebih mudah ditangkap oleh pengembang lain.

Cara menggunakan Kelas Data

Untuk menggunakan Kelas Data, Anda harus terlebih dahulu mengimpor dekorator kelas data dari modul dataclasses. Dekorator ini secara bawaan disertakan dalam Python 3.7 dan lebih tinggi.

Menggunakan Kelas Data sangat sederhana. Hiasi saja definisi kelas Anda dengan dekorator @dataclass untuk mendefinisikan kelas data.

Berikut adalah contoh kelas data sederhana dengan parameter default:

from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)  # Output: Point(x=1.0, y=2.0)

Kita telah mendefinisikan kelas data Point dengan dua bidang, x dan y, keduanya merupakan float. Saat Anda membuat instance kelas Point, Anda dapat menentukan nilai x dan y sebagai argumen ke konstruktor.

Secara default, Kelas Data akan menghasilkan metode __init__ untuk Anda yang menggunakan bidang kelas sebagai argumen. Mereka juga akan menghasilkan metode __repr__ yang mengembalikan representasi string dari objek, yang dicetak saat Anda memanggil print(p) pada contoh di atas.

Anda dapat menyesuaikan metode mana yang dihasilkan oleh dekorator dengan meneruskan argumen tambahan, seperti repr=False untuk menonaktifkan metode __repr__.

Fungsi lapangan

Anda juga dapat menentukan nilai default untuk bidang Kelas Data dengan menggunakan argumen default dan default_factory pada fungsi field().

Misalnya:

@dataclass
class Point:
    x: float = 0.0
    y: float = field(default=0.0)

p1 = Point(x=1.0, y=2.0)
print(p1)  # Output: Point(x=1.0, y=2.0)

p2 = Point(1.0)
print(p2)  # Output: Point(x=1.0, y=0.0)

Dalam contoh ini, kita telah mendefinisikan kelas Point dengan dua bidang, x dan y. Bidang x memiliki nilai default 0,0 yang ditentukan langsung dalam definisi bidang, sedangkan bidang y memiliki nilai default 0,0 yang ditentukan menggunakan fungsi field().

Saat kita membuat instance kelas Point, kita bisa menentukan nilai untuk x dan y sebagai argumen ke konstruktor. Jika kita tidak menentukan nilai untuk y, maka akan menggunakan nilai default 0,0.

Di dalam Kelas Data, dua penggunaan untuk menentukan nilai default setara untuk atribut yang tidak dapat diubah.

Namun, fungsi field() memungkinkan lebih banyak fleksibilitas dan opsi tambahan untuk menentukan atribut.

Argumen repr dan init bidang

Memang benar, fungsi field() memiliki argumen default __repr__ dan __init__ yang disetel ke True. Kita dapat menyetelnya ke False untuk mengubah perilakunya.

@dataclass
class Point:
    x: float = field(default=0.0, repr=False)
    y: float = field(default=0.0, init=False)

p1 = Point(3.0) 
print(p1) # Output: Point(y=0.0)
p2 = Point(3.0, 2.0) # TypeError: Point.__init__() takes from 1 to 2 positional arguments but 3 were given

Pada contoh pertama, instance p1 tidak menampilkan nilai x dalam representasi stringnya, yang berguna untuk menyembunyikan variabel sementara atau sensitif (setidaknya dari representasi string).

Pada contoh kedua, instancep2 , tidak dapat dibuat karena argumen init disetel ke False dan kami mencoba menginisialisasi variabel y. Ini berguna untuk variabel yang hanya boleh dikembalikan oleh suatu metode. Mari tambahkan metode ke Kelas Data kita yang hanya menggunakan y sebagai output komputasi dalam fungsi compute_y_with_x :

@dataclass
class Point:
    x: float = field(default=0.0, repr=False)
    y: float = field(default=0.0, init=False)
    def compute_y_with_x(self):
        self.y = self.x ** 2

p2 = Point(x=2.0)
p2.compute_y_with_x()
print(p2) # Output: Point(y=4.0)

Di sini, kita melihat bahwa p2 menggunakan y sebagai variabel yang tidak diinisialisasi (init=False) yang merupakan hasil transformasi variabel masukan kita x. Kami tidak peduli dengan x setelah inisialisasi, jadi kami menghapus representasi stringnya (repr=False).

bidang default_pabrik

Apakah Anda ingat ketika kita membahas bahwa menggunakan argumen default suatu kelas atau menyetel bidang dengan argumen default memiliki efek yang sama pada objek yang tidak dapat diubah, namun tidak pada objek yang dapat diubah?

Mari kita lihat contoh saat kita mencoba menginisialisasi atribut ke objek yang mutable, seperti daftar. Kita juga akan membuat metode yang memungkinkan kita menambahkan elemen ke daftar bernama add_a_dimension:

@dataclass
class Points:
    coord: list = field(default=[])
    def add_a_dimension(self, element):
        self.coord.append(element)
# Output: ValueError

Kelas ini tidak dapat dibangun karena, seperti yang dinyatakan, “menggunakan nilai default yang dapat diubah (seperti daftar) untuk bidang ‹coord› tidak diperbolehkan.” Ini adalah langkah keamanan yang ditambahkan oleh Kelas Data, yang sangat berguna dalam mencegah bug yang mungkin kita temui pada kelas reguler.

Memang benar, jika kita ingin mendefinisikan Kelas Data Points menggunakan kelas biasa, maka akan sama persis dengan:

class Points:
    coord = []
    def __init__(self, coord=coord):
        self.coord = coord
    def add_a_dimension(self, element):
        self.coord.append(element)

Dan kami tidak akan mengalami kesalahan apa pun saat mendefinisikan kelas ini! Namun, jika kita mendefinisikan kelas dengan cara ini menggunakan kelas reguler, kita akan melihat perilaku yang tidak diinginkan:

p1 = Points()
p2 = Points()
p1.coord, p2.coord # Output: ([],[])
p1.add_a_dimension(3)

Oke, kita membuat instance daftar kosong dan menambahkan 3 ke instance p1. Menurut Anda berapa nilai p1.coord dan p2.coord sekarang?

p1.coord, p2.coord # Output: ([3], [3])

Hebatnya, instance p2 juga terpengaruh oleh penambahan 3 ke dalam daftar!

Hal ini karena dengan Python ketika Anda membuat sebuah instance dari suatu kelas, instance tersebut akan berbagi copy atribut kelas yang sama. Karena daftar dapat diubah, p1 dan p2 akan berbagi salinan daftar coord yang sama.

Untuk mengimplementasikan kelas ini dengan benar guna menghindari hasil yang tidak diinginkan ini, Kelas Data memberikan argumen untuk field() yang disebut default_factory.

Argumen ini memungkinkan Anda menentukan fungsi yang akan dipanggil untuk membuat nilai default baru untuk bidang setiap kali instance baru dari kelas tersebut dibuat. Hal ini memastikan bahwa setiap instance kelas memiliki salinan unik bidangnya sendiri, daripada berbagi objek mutable yang sama.

@dataclass
class Points:
    coord: list = field(default_factory=lambda: [])
    def add_a_dimension(self, element):
        self.coord.append(element)
p1 = Points()
p2 = Points()
p1.coord, p2.coord # Output ([], [])

Perlu diperhatikan bahwa tidak ada ValueError meskipun kita mendefinisikan daftar yang dapat diubah sebagai nilai default untuk coord.

Mari kita periksa output setelah memanggil metode add_a_dimension hanya pada satu instance:

p1.add_a_dimension(3)
p1.coord, p2.coord # Output ([3], [])

Ah, akhirnya kita mendapatkan hasil yang diinginkan! Instance p2 tidak diubah oleh metode yang dipanggil oleh instance p1. Tepatnya karena setiap instance memiliki salinan uniknya sendiri dari bidang coord .

Seperti yang bisa kita lihat, Kelas Data memberikan keamanan dengan menangani objek yang bisa berubah dan tidak bisa diubah dengan benar.

Warisan

Satu hal terakhir yang perlu dipertimbangkan adalah bagaimana Kelas Data bekerja dengan warisan. Secara default, Kelas Data tidak memiliki metode __init__, jadi perlu ada metode yang memungkinkan Anda menimpa atribut yang diwariskan. Metode ini disebut __post_init__ dan dipanggil setelah instance diinisialisasi.

Berikut ini contoh untuk membantu mengilustrasikan konsep ini:

@dataclass
class Point:
    x: float = field(default=0.0)
    y: float = field(default=0.0)
    
    def __post_init__(self):
        self.x = self.x ** 2
        self.y = self.y ** 2

@dataclass
class ColoredPoint(Point):
    color: str = field(default='black')
    
    def __post_init__(self):
        self.color = self.color.upper()

Dalam contoh ini, Kelas Point memiliki metode __post_init__ yang mengkuadratkan nilai x dan y. Kelas ColoredPoint mewarisi dari Point dan juga memiliki metode __post_init__ sendiri yang menjadikan nilai atribut color menjadi huruf besar.

Mari buat instance Point:

p0 = Point(2.0,2.0)
print(p0) # Output: Point(x=4.0, 4.0)

Kita melihat dari output bahwa metode __post_init__ dipanggil, dan nilai x dan y dikuadratkan.

Sekarang mari kita buat sebuah instance dari ColoredPoint:

p1 = ColoredPoint(2.0, 2.0, 'red')
print(p1) # Output: ColoredPoint(x=2.0, y=2.0, color='RED')

Saat membuat instance ColoredPoint, metode __post_init__ dipanggil dan nilai color ditulis dalam huruf besar, tetapi nilai x dan y tidak dikuadratkan, tahukah Anda alasannya?

Ini karena metode __post_init__ dari Point tidak dipanggil dalam metode __post_init__ dari ColoredPoint kami!

Untuk memanggil metode __post_init__ dari kelas dasar, Anda dapat menggunakan fungsi super(), yang mengembalikan referensi ke kelas dasar. Ini adalah versi ColoredPoint yang telah diperbaiki:

@dataclass
class ColoredPoint(Point):
    color: str = field(default='red')

    def __post_init__(self):
        super().__post_init__()
        self.color = self.color.upper()
p2 = ColoredPoint(2.0, 2.0, 'red')
print(p2) # Output: ColoredPoint(x=4.0, y=4.0, color='RED')

Dengan perubahan ini, metode __post_init__ dari kelas Point akan dipanggil ketika instance ColoredPoint dibuat, dan nilai x dan y akan dikuadratkan seperti yang diharapkan.

Saya senang bisa membantu Anda mempelajari tentang Kelas Data. Seperti yang Anda lihat, Kelas Data menyediakan versi kelas reguler yang disempurnakan yang lebih aman dan menekankan pengetikan yang kuat. Saya harap Anda merasa lebih percaya diri dengan kemampuan Anda menggunakan Kelas Data dalam proyek mendatang.

Tanpa biaya tambahan, Anda dapat berlangganan Medium melalui tautan rujukan saya.



Atau Anda bisa mendapatkan semua postingan saya di kotak masuk Anda.Lakukan di sini!