Python Dataclasses
Nedir?
Python 3.7 ile gelen güzel bir yapı olan dataclass'lar, Python geliştiricileri için mükemmel bir yenilik. Bu yazıda elimden geldiğince bu yapıyı anlatmaya çalısacağım.
Efendim dataclass'lar normal class'lardaki kendini sürekli tekrarlayan kodları yazmayı engelleyen, içinde (genelde) veri depolayan classlardır. Aşağıdaki örnekte bir dataclass'ın nasıl tanımladığını görebilirsiniz.
Unutmayın
dataclass'lar Python'un 3.7 sürümünde bulunur.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: intGörüldüğü gibi dataclass'lar @dataclass decoratoru ile tanımlanır.
Sınıf değişkenlerinin yanında tipinin yazıldığını fark etmişsinizdir. Bu Python'da Type Annotations (opens in a new tab) olarak geçer.
dataclass'lar bunu kullandığından dolayı bilmek önemlidir. Şurada (opens in a new tab) type annotations ile ilgili cok güzel bir makale var.
Bir dataclass, bazı special method (opens in a new tab)'ları sizin için değiştirir. Mesela __str__() methodu otomatik olarak sınıf içindeki değerleri bastıracaktır.
>>> p = Person("Ömer", 19)
>>> p
Person(name='Ömer', age=19)
>>> p.name = "Ahmet"
>>> p.age = 30
>>> p
Person(name='Ahmet', age=30)Eğer biz böyle bir çıktı isteseydik şöyle bir şey yazmamız gerekirdi;
class NPerson:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name!r}, age={self.age})"
def __str__(self):
return self.__repr__()Görüldüğü gibi bir değişkeni __init__'den alıp nesne değişkeni yapmak için 3 kere yazmamız gerekiyor.
Ayrıca dikkatli bakarsanız objelerin tam olarak tanımlanmadığını göreceksiniz.
>>> p = Person("Ömer", 19)
>>> p == Person("Ömer", 19)
True
>>> np = NPerson("Ahmet", 30)
>>> np == NPerson("Ahmet", 30)
FalseBunun nedeni dataclass'ların __eq__ methodunu override etmiş olmaları. Normalde Python, obje karşılaştırma yaparken objelerin adreslerini karşılaştırır ama dataclass'lar sınıf içindeki değerleri karşılaştırır. Eğer bunu kendiniz yazmak isteseydiniz;
class NPerson:
[...]
def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.name, self.age) == (other.name, other.age)dataclass'lar bunu bizim için yapar.
dataclass'lara default değerler de verebiliriz;
@dataclass
class Person:
name: str
age: int = 18
>>> p = Person("Ahmet")
>>> p
Person(name='Ahmet', age=18)Not:
dataclass'lar (ve Python) aslında değişkenlerin tipine dikkat etmez. Type annotations sadece okunabilirliği artırır.
from dataclasses import dataclass
from typing import Any
@dataclass
class Person:
name: Any
age: str
>>> p = Person(14, "Foo")
>>> p
Person(name=14, age='Foo')Şu ana kadar hiç fonksiyon yazmadık ama dataclass'da fonksiyon yazmak ile normalde yazmak arasında hiç fark yoktur.
from dataclasses import dataclass
from typing import List
@dataclass
class Student:
name: str
age: int
results: List[int]
def average(self):
return sum(self.results) / len(self.results)>>> st = Student("Ömer", 19, [85,97,67])
>>> st.average()
83.0Biraz @dataclass decoratoru hakkında konuşalım. @dataclassdecoratoru birçok parametre alabilir.
@dataclass
class Foo:
[...]
# Aslında buna eşittir.
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Foo:
[...]
-
init: eğerTrueise__init__fonksiyonunu override eder. -
repr: eğerTrueise__repr__fonksiyonunu override eder. -
eq: eğerTrueise__eq__fonksiyonunu override eder. Bu konuya yukarda değinmiştik. -
order: eğerTrueise (varsayılanFalse)__lt__,__le__,__gt__ve__ge__fonksiyları override eder. Bu fonksiyonlar karşılaştırma fonksiyonlarıdır.dataclass,__eq__'de olduğu gibi class değerlerini karşılaştırır. -
frozen: eğerTrueise nesne oluşturduktan sonra gelen değer atamalarıFrozenInstanceErrorhatasını raise edecektir.
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Student:
name: str
age: int
results: List[int]
def average(self):
return sum(self.results) / len(self.results)>>> st = Student("Ömer", 19, [85,97,67])
>>> st.name = "Ahmet"
---------------------------------------------------------------------------
FrozenInstanceError Traceback (most recent call last)
<file> in <module>
----> 1 st.name = "Ahmet"
<string> in __setattr__(self, name, value)
FrozenInstanceError: cannot assign to field 'name'dataclass ile kalıtım da yapabiliriz.
from dataclasses import dataclass
from typing import List
@dataclass
class Person:
name: str
age: int
def say(self, s):
print(f"{self.name}: {s}")
@dataclass
class Student(Person):
results: List[int]
def average(self):
return sum(self.results) / len(self.results)>>> st = Student("Ömer", 19, [85,97,67])
>>> st.say("Hello world")
Ömer: Hello worldAlternatifler
dataclass'ların (genellikle) veri depoladığını söylemiştik. Bunu Python'da sadece dataclass'ların yapmadığını görmüşsünüzdür. Basit veri yapıları olan tuple ve dict de veri depolar.
person_tuple = (19, "Ömer") # Tuple
person_dict = {'age': 19, 'name': 'Ömer'} # DictAma dikkat ederseniz dataclass'lar kadar kullanışlı olmadığını görürsünüz. Mesela tuple'de argumanların yerlerini karıştırabilirsiniz debug ederken bu işinizi çok zorlaştır. dict de ise dataya erişmek için mutlaka bir key'e ihtiyaç vardır.
person_dict['name'] ## person_dict.name desek daha hoş olmaz mı?Aslında
dictveri tipindeki objelerin verilerine nokta(.) notasyonu ile erişebiliriz. Şöyle:
Konu ile ilgili içerik Sözlük Veri Tipini Python Nesnesine Dönüştürme (opens in a new tab)
class NDict(dict):
def __init__(self, *arg, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
super().__init__(*arg, **kwargs)
person = NDict(name="Ömer", age=19)
person.age # 19
person.name # ÖmerTabi bunun ne kadar zahmetli olduğunu görüyorsunuz. Ama durun yukarıdaki kodun daha iyisini yapan bir veri yapısı var zaten. namedtuple
from collections import namedtuple
Person = namedtuple("Person", ['age', 'name'])
person = Person(16, "Ömer")
person
# Person(age=16, name='Ömer')
person.name
# ÖmerE dataclass'lardan farkı ne bunun?
Öncelikle dataclass'ların çok daha fazla özelliği buluyor. Yukarıda anlattığım kalıtım ve fonksiyon ekleme işlemleri namedtuple'de çok daha zor. Öte yandan karşılaştırma yaparken namedtuple istediğinizi vermeyecektir. Yukarıdaki örnekten devam edelim
>>> person == (16, "Ömer")
Trueİyi bir şey gibi gözükse de sonuçta kendi türünde olmadığını zannettiğimiz objelerle tam anlamıyla doğru karşılaştırmalar vermiyor.
Ayırca namedtuple obje oluştuktan sonra verilerin değişmesine izin vermeyecektir.
Person = namedtuple("Person", ['age', 'name'])
person = Person(16, "Ömer")
person.age = 22
----------------------------------------
AttributeError Traceback (most recent call last)
<file> in <module>
3 Person = namedtuple("Person", ['age', 'name'])
4 person = Person(16, "Ömer")
----> 5 person.age = 22
AttributeError: can't set attributefield()
Bir seneryo üzerinden devam edelim.
from dataclasses import dataclass
from typing import List
@dataclass
class Student:
id: int
name: str
@dataclass
class Lesson:
students: List[Student]Buradan yeni nesneler üretelim
omer = Student(1, "Ömer")
bersu = Student(2, "Bersu")
math = Lesson([omer, bersu])
print(math)
# Lesson(students=[Student(id=1, name='Ömer'), Student(id=2, name='Bersu')])Şimdi Lesson sınıfına default deger vermeyi deneyelim. Bunu yaparken bir factory fonksiyon yazalım.
NAMES = ["Ömer", "Ahmet", "Cem", "Zehra", "Büşra", "Bersu"]
def collect_students():
return [Student(i + 1, v) for i, v in enumerate(NAMES)]
collect_students()
# [Student(id=1, name='Ömer'),
# Student(id=2, name='Ahmet'),
# Student(id=3, name='Cem'),
# Student(id=4, name='Zehra'),
# Student(id=5, name='Büşra'),
# Student(id=6, name='Bersu')]Teoride Lessona varsayılan değer vermek için şöyle yaparsınız.
@dataclass
class Lesson:
students: List[Student] = collect_students()Böyle bir tanım Python'ın en büyük anti-pattern'lerinden birisidir: Varsayılan olarak değişken değer kullanmak. Buradaki problem şu ki Lesson'nun tüm versiyonları aynı .students'in varsayılan liste objesini kullanacak. Kısacası bir Lesson'dan herhangi bir Student silindiği vakit Lesson'nun tüm versiyonlarından da silinecek. Aslına bakarsanız dataclass'lar bunun olmasının önüne geçip size ValueError döndürüyor.
---------------------------------------------------
ValueError Traceback (most recent call last)
<file> in <module>
12 name: str
13
---> 14 @dataclass
15 class Lesson:
16 students: List[Student] = collect_students()
\python\python37-32\lib\dataclasses.py in _get_field(cls, a_name, a_type)
725 # For real fields, disallow mutable defaults for known types.
726 if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)):
--> 727 raise ValueError(f'mutable default {type(f.default)} for field '
728 f'{f.name} is not allowed: use default_factory')
729
ValueError: mutable default <class 'list'> for field students is not allowed: use default_factoryBunun önüne geçmek için field methodunun default_factory diye bir parametresi var.
from datacasses import dataclass, field
@dataclass
class Lesson:
students: List[Student] = field(default_factory=collect_students)
Lesson()
# Lesson(students=[Student(id=1, name='Ömer'), Student(id=2, name='Ahmet'), Student(id=3, name='Cem'), Student(id=4, name='Zehra'), Student(id=5, name='Büşra'), Student(id=6, name='Bersu')])field, sadece default_factory ile sınırlı değil. Bu bağlantıdan (opens in a new tab) diğer parametrelere ve ne işe yaradıklarına ulaşabilirsiniz.
Optimizasyon
Bahsedeceğim şey __slots__, __slots__ kısaca, sınıflara dinamik olmayan sabit attributelar belirleyerek RAM'dan ve hızdan tasarruf sağlıyor. __slots__, kendi başına ele alınması gereken bir konu oluğu için şuradan (opens in a new tab) daha fazla bilgiye ulaşabilirsiniz.
dataclass'larda __slots__ kullanımı normal classlardaki gibidir.
from dataclasses import dataclass, field
@dataclass
class NormalPerson:
name: str
age: int
salary: int
@dataclass
class SlotPerson:
__slots__ = ['name', 'age', 'salary']
name: str
age: int
salary: intHafızada sahip olduğu büyüklüğe bakalım.
from sys import getsizeof
getsizeof(NormalPerson("Ahmet", 33, 3000)), getsizeof(SlotPerson("Ahmet", 33, 3000))
# (32, 36)Ayırca Python'un veriye erişmesi de normal class'lara göre daha hızlıdır.
from timeit import timeit
timeit(setup="slot_p = SlotPerson('Ahmet', 33, 3000)", globals=globals())
# 0.012656699999752163
timeit(setup="normal_p = NormalPerson('Ahmet', 33, 3000)", globals=globals())
# 0.012095599999156548tabi yazmış olduğumuz sınıfın basitliğinden dolayı aradaki fark oldukça az. Daha büyük sınıflarda bu fark dikkate değer biçimde artıyor.
2024 © Faruk