Salah satu fitur baru yang diperkenalkan di Python 3.7 adalah kelas Data. Mereka dirancang untuk mengotomatisasi pembuatan kode untuk kelas yang digunakan untuk menyimpan data. Terlepas dari kenyataan bahwa mereka menggunakan mekanisme kerja lain, mereka dapat dibandingkan dengan "tuple bernama yang bisa berubah dengan nilai default."
Pendahuluan
Semua contoh di atas memerlukan Python 3.7 atau lebih tinggi untuk operasi mereka.
Sebagian besar pengembang python harus menulis kelas-kelas ini secara teratur:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
Sudah dalam contoh ini, redundansi terlihat. Judul dan pengidentifikasi penulis digunakan beberapa kali. Kelas nyata juga akan berisi metode yang ditimpa __eq__
dan __repr__
.
Modul dataclasses
berisi dekorator @dataclass
. Dengan menggunakannya, kode yang mirip akan terlihat seperti ini:
from dataclasses import dataclass @dataclass class Book: title: str author: str
Penting untuk dicatat bahwa anotasi jenis diperlukan . Semua bidang yang tidak memiliki tanda jenis akan diabaikan. Tentu saja, jika Anda tidak ingin menggunakan jenis tertentu, Anda dapat menentukan Any
dari modul typing
.
Apa yang Anda dapatkan sebagai hasilnya? Anda secara otomatis mendapatkan kelas, dengan metode yang diterapkan __init__
, __repr__
, __eq__
dan __eq__
. Selain itu, ini akan menjadi kelas reguler dan Anda dapat mewarisi darinya atau menambahkan metode sewenang-wenang.
>>> book = Book(title="Fahrenheit 451", author="Bradbury") >>> book Book(title='Fahrenheit 451', author='Bradbury') >>> book.author 'Bradbury' >>> other = Book("Fahrenheit 451", "Bradbury") >>> book == other True
Alternatif
Tuple atau kamus
Tentu saja, jika strukturnya cukup sederhana, Anda dapat menyimpan data dalam kamus atau tuple:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
Namun, pendekatan ini memiliki kelemahan:
- Harus diingat bahwa variabel berisi data yang terkait dengan struktur ini.
- Dalam hal kamus, Anda harus melacak nama-nama tombol. Inisialisasi kamus
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
juga akan benar secara formal. - Dalam kasus tuple, Anda harus melacak urutan nilai, karena mereka tidak memiliki nama.
Ada opsi yang lebih baik:
Namedtuple
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
Jika kita menggunakan kelas yang dibuat dengan cara ini, kita mendapatkan hal yang sama seperti menggunakan kelas data.
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
Namun terlepas dari kesamaan umum, tuple bernama memiliki keterbatasan. Mereka datang dari fakta bahwa tuple bernama masih tuple.
Pertama, Anda masih dapat membandingkan contoh kelas yang berbeda.
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
Kedua, tuple bernama tidak dapat diubah. Dalam beberapa situasi, ini berguna, tetapi saya ingin lebih banyak fleksibilitas.
Akhirnya, Anda dapat beroperasi pada tuple bernama juga yang biasa. Misalnya, beralih lagi.
Proyek lainnya
Jika tidak terbatas pada perpustakaan standar, Anda dapat menemukan solusi lain untuk masalah ini. Secara khusus, proyek ini menarik. Ia dapat melakukan lebih dari sekadar dataclass dan bekerja pada versi python yang lebih lama seperti 2.7 dan 3.4. Namun demikian, fakta bahwa itu bukan bagian dari perpustakaan standar mungkin merepotkan
Ciptaan
Anda dapat menggunakan dekorator @dataclass
untuk membuat kelas data. Dalam hal ini, semua bidang kelas yang ditentukan dengan anotasi tipe akan digunakan dalam metode yang sesuai dari kelas yang dihasilkan.
Sebagai alternatif, ada fungsi make_dataclass
, yang berfungsi serupa untuk membuat tupel bernama.
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
Nilai default
Salah satu fitur yang bermanfaat adalah kemudahan menambahkan nilai default ke bidang. Masih tidak perlu mendefinisikan ulang metode __init__
, cukup tentukan nilai langsung di kelas.
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
Mereka akan diperhitungkan dalam metode __init__
dihasilkan
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
Tetapi seperti halnya kelas dan metode reguler, Anda harus berhati-hati menggunakan standar yang bisa berubah. Jika, misalnya, Anda perlu menggunakan daftar sebagai nilai default, ada cara lain, tetapi lebih pada itu di bawah ini.
Selain itu, penting untuk memantau urutan bidang mana dengan nilai default yang ditentukan, karena sama persis dengan urutannya dalam metode __init__
Kelas Data Tidak Berubah
Contoh tuple bernama tidak berubah. Dalam banyak situasi, ini adalah ide yang bagus. Untuk kelas data, Anda dapat melakukannya juga. Cukup tentukan parameter frozen=True
saat membuat kelas, dan jika Anda mencoba mengubah bidangnya, pengecualian FrozenInstanceError
akan FrozenInstanceError
@dataclass(frozen=True) class Book: title: str author: str
>>> book = Book("Fahrenheit 451", "Bradbury") >>> book.title = "1984" dataclasses.FrozenInstanceError: cannot assign to field 'title'
Pengaturan Kelas Data
Selain parameter frozen
, dekorator @dataclass
memiliki parameter lain:
init
: jika True
(default), metode __init__
dihasilkan. Jika kelas sudah memiliki metode __init__
yang ditentukan, parameter diabaikan.repr
: memungkinkan (secara default) pembuatan metode __repr__
. String yang dihasilkan berisi nama kelas dan nama serta representasi dari semua bidang yang didefinisikan dalam kelas. Dalam hal ini, masing-masing bidang dapat dikecualikan (lihat di bawah)eq
: memungkinkan (secara default) pembuatan metode __eq__
. Objek dibandingkan dengan cara yang sama seolah-olah mereka adalah tupel yang berisi nilai bidang yang sesuai. Selain itu, pencocokan jenis diperiksa.order
memungkinkan (default tidak aktif) pembuatan metode __lt__
, __le__
, __gt__
dan __ge__
. Objek dibandingkan dengan cara yang sama dengan tupel nilai bidang yang sesuai. Pada saat yang sama, jenis objek juga diperiksa. Jika order
ditentukan, tetapi eq
tidak, pengecualian ValueError
akan dibuang. Juga, kelas tidak boleh berisi metode perbandingan yang sudah didefinisikan.unsafe_hash
memengaruhi pembuatan metode __hash__
. Perilaku juga tergantung pada nilai-nilai parameter eq
dan frozen
Kustomisasi masing-masing bidang
Dalam sebagian besar situasi standar, ini tidak diperlukan, tetapi dimungkinkan untuk menyesuaikan perilaku kelas data hingga masing-masing bidang menggunakan fungsi bidang.
Default yang Dapat Diubah
Situasi khas yang disebutkan di atas adalah penggunaan daftar atau nilai default yang bisa berubah-ubah. Anda mungkin menginginkan kelas "rak buku" yang berisi daftar buku. Jika Anda menjalankan kode berikut:
@dataclass class Bookshelf: books: List[Book] = []
interpreter akan melaporkan kesalahan:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
Namun, untuk nilai yang dapat diubah lainnya, peringatan ini tidak akan berfungsi dan akan menyebabkan perilaku program yang salah.
Untuk menghindari masalah, disarankan untuk menggunakan parameter default_factory
dari fungsi field
. Nilainya dapat berupa objek atau fungsi apa pun yang disebut tanpa parameter.
Versi kelas yang benar terlihat seperti ini:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
Pilihan lain
Selain default_factory
ditentukan, fungsi bidang memiliki parameter berikut:
default
: nilai default
. Parameter ini diperlukan karena bidang ajakan menggantikan nilai bidang default.init
: memungkinkan (default) penggunaan suatu bidang dalam metode __init__
repr
: memungkinkan (default) penggunaan suatu bidang dalam metode __repr__
compare
termasuk (default) penggunaan bidang dalam metode perbandingan ( __eq__
, __le__
dan lainnya)hash
: mungkin nilai boolean atau None
. Jika True
, bidang digunakan untuk menghitung hash. Jika None
ditentukan (secara default), nilai parameter compare
digunakan.
Salah satu alasan untuk menentukan hash=False
untuk compare=True
diberikan compare=True
mungkin kesulitan menghitung hash bidang sementara itu diperlukan untuk perbandingan.metadata
: kamus khusus atau None
. Nilai tersebut dibungkus dalam MappingProxyType
sehingga menjadi tidak berubah. Parameter ini tidak digunakan oleh kelas data itu sendiri dan dimaksudkan untuk ekstensi pihak ketiga.
Memproses setelah inisialisasi
Metode __init__
dibuat secara otomatis memanggil metode __post_init__
, jika didefinisikan dalam kelas. Sebagai aturan, ini disebut dalam bentuk self.__post_init__()
, namun, jika variabel tipe InitVar
didefinisikan di kelas, mereka akan dilewatkan sebagai parameter metode.
Jika metode __init__
belum dihasilkan, maka __post_init__
tidak akan dipanggil.
Misalnya, tambahkan deskripsi buku yang dihasilkan
@dataclass class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
Parameter untuk inisialisasi saja
Salah satu kemungkinan yang terkait dengan metode __post_init__
adalah parameter yang hanya digunakan untuk inisialisasi. Jika, ketika mendeklarasikan sebuah field, tentukan InitVar
sebagai tipenya, nilainya akan diteruskan sebagai parameter dari metode __post_init__
. Tidak ada cara lain yang bidang yang digunakan dalam kelas data.
@dataclass class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury') >>> Book("Fareneheit 481", "Bradbury", gen_desc=False) Book(title='Fareneheit 481', author='Bradbury', desc=None)
Warisan
Ketika Anda menggunakan dekorator @dataclass
, ia melewati semua kelas induk dimulai dengan objek dan untuk setiap kelas data menemukan itu menyimpan bidang dalam kamus yang diurutkan, kemudian menambahkan properti dari kelas yang diproses. Semua metode yang dihasilkan menggunakan bidang dari kamus yang dipesan.
Akibatnya, jika kelas induk mendefinisikan nilai default, Anda harus mendefinisikan bidang dengan nilai default.
Karena kamus yang diurutkan menyimpan nilai-nilai dalam urutan penyisipan, untuk kelas-kelas berikut
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
metode __init__
dengan tanda tangan ini akan dihasilkan:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)