dataclass ve NamedTuple
Python'da sınıf yazarken en sıkıcı kısım ne biliyor musun? __init__ yazıp her attribute'u tek tek self.x = x diye atamak. Sonra __repr__ yaz, __eq__ yaz, __hash__ yaz… Aynı tür boilerplate kodları tekrar tekrar.
Python 3.7 ile gelen `@dataclass` decorator'ı bu sorunu çözüyor. Sınıfın attribute'larını tanımla, gerisini Python halleder.
Dataclass Nedir?
Dataclass, ağırlıklı olarak veri taşımak için kullanılan sınıfları hızlıca oluşturmayı sağlayan bir decorator'dır. __init__, __repr__ ve __eq__ metodlarını otomatik oluşturur.
Analoji: Form Şablonu 📋
Bir form düşün — ad, soyad, email, telefon alanları var. Her müşteri için yeni bir boş form basmak yerine, alanları bir kere tanımlarsın ve form otomatik basılır. Dataclass da böyle: attribute'ları bir kere tanımla, Python boilerplate'i otomatik üretsin.
# Klasik yol — tekrar tekrar aynı şeyler
class ProductOld:
def __init__(self, name, price, stock=0):
self.name = name
self.price = price
self.stock = stock
def __repr__(self):
return f"ProductOld(name='{self.name}', price={self.price}, stock={self.stock})"
def __eq__(self, other):
if not isinstance(other, ProductOld):
return NotImplemented
return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)# Dataclass yolu — aynı işlevsellik, çok daha az kod
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: int = 0
# __init__, __repr__, __eq__ otomatik oluşturuldu!
p1 = Product("Laptop", 15000, 5)
p2 = Product("Laptop", 15000, 5)
p3 = Product("Mouse", 500)
print(p1) # Product(name='Laptop', price=15000, stock=5)
print(p1 == p2) # True — otomatik __eq__
print(p1 == p3) # False
print(p3.stock) # 0 — default değer3 satır vs 15 satır. Ve üretilen kod aynı işi yapıyor. İşte bu yüzden dataclass'a "boilerplate killer" denir.
@dataclass Decorator Parametreleri
@dataclass decorator'ı birçok parametre alır ve davranışını özelleştirmene olanak tanır:
@dataclass(
init=True, # __init__ oluştur (default: True)
repr=True, # __repr__ oluştur (default: True)
eq=True, # __eq__ ve __ne__ oluştur (default: True)
order=False, # __lt__, __le__, __gt__, __ge__ oluştur (default: False)
frozen=False, # Immutable yap (default: False)
unsafe_hash=False, # __hash__ zorla oluştur
slots=False, # __slots__ kullan — Python 3.10+ (default: False)
)
class MyClass:
...order=True: Otomatik Karşılaştırma
@dataclass(order=True)
class Student:
grade: float # Önce grade'e göre sıralar
name: str # Sonra name'e göre
s1 = Student(85, "Ali")
s2 = Student(92, "Veli")
s3 = Student(85, "Ayşe")
print(s1 < s2) # True (85 < 92)
print(s1 < s3) # False (85 == 85, "Ali" < "Ayşe"? False)
print(s3 < s1) # True (85 == 85, "Ayşe" < "Ali"? True)
# Sıralama!
students = [s2, s1, s3]
print(sorted(students))
# [Student(grade=85, name='Ali'), Student(grade=85, name='Ayşe'), Student(grade=92, name='Veli')]Karşılaştırma, field'ların tanım sırasına göre tuple karşılaştırması gibi çalışır. Bu yüzden sıralama için önemli olan field'ı önce yaz.
Field Options: default, default_factory, field()
Basit Default Değerler
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
workers: int = 4
config1 = Config()
print(config1) # Config(host='localhost', port=8080, debug=False, workers=4)
config2 = Config(host="0.0.0.0", debug=True)
print(config2) # Config(host='0.0.0.0', port=8080, debug=True, workers=4)Mutable Default: default_factory
Mutable değerler (list, dict, set) için default_factory kullanmalısın:
from dataclasses import dataclass, field
@dataclass
class ShoppingCart:
owner: str
items: list = field(default_factory=list)
discounts: dict = field(default_factory=dict)
tags: set = field(default_factory=set)
cart1 = ShoppingCart("Ali")
cart2 = ShoppingCart("Veli")
cart1.items.append("Laptop")
print(cart1.items) # ['Laptop']
print(cart2.items) # [] — bağımsız! Tuzağa düşmedik.⚠️ Dikkat: Mutable default değerleri doğrudan yazmak hata verir:
items: list = []dersenValueError: mutable default <class 'list'> is not allowedalırsın. Bu bilinçli bir tasarım — seni klasik class attribute tuzağından korur. Her zamanfield(default_factory=list)kullan.
field() ile İleri Seçenekler
from dataclasses import dataclass, field
@dataclass
class User:
username: str
email: str
password: str = field(repr=False) # repr'da gösterme!
is_active: bool = field(default=True)
login_count: int = field(default=0, repr=False)
_internal_id: str = field(default="", init=False, repr=False)
def __post_init__(self):
self._internal_id = f"USR-{hash(self.username) % 10000:04d}"
u = User("ali", "ali@mail.com", "secret123")
print(u)
# User(username='ali', email='ali@mail.com', is_active=True)
# password ve login_count repr'da YOK!
print(u.password) # secret123 — erişilebilir ama repr'da gizli
print(u._internal_id) # USR-XXXX — init'e dahil değilfield() parametreleri:
| Parametre | Default | Açıklama |
|---|---|---|
default | MISSING | Varsayılan değer |
default_factory | MISSING | Mutable varsayılan için callable |
repr | True | __repr__'a dahil et |
init | True | __init__'e parametre olarak ekle |
compare | True | __eq__ ve order karşılaştırmasına dahil et |
hash | None | Hash hesaplamasına dahil et |
metadata | None | Ek bilgi (dict) |
frozen=True: Immutable Dataclass
frozen=True ile dataclass'ı değiştirilemez (immutable) yapabilirsin:
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(3, 4)
print(p) # Point(x=3, y=4)
# p.x = 10 # FrozenInstanceError: cannot assign to field 'x'
# Frozen dataclass otomatik olarak hashable'dır!
points = {Point(0, 0), Point(1, 1), Point(0, 0)}
print(points) # {Point(x=0, y=0), Point(x=1, y=1)} — duplicate temizlendi
# Dict key olarak
distances = {
Point(0, 0): "Başlangıç",
Point(3, 4): "Hedef",
}
print(distances[Point(3, 4)]) # HedefNe Zaman frozen=True Kullan?
Değerin değişmemesi gereken konfigürasyonlarda
Set/dict key olarak kullanılacak nesnelerde
Fonksiyonel programlama tarzında
Thread-safe olması gereken veri yapılarında
@dataclass(frozen=True)
class Color:
r: int
g: int
b: int
@property
def hex(self):
return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
RED = Color(255, 0, 0)
GREEN = Color(0, 255, 0)
BLUE = Color(0, 0, 255)
print(RED.hex) # #ff0000
# RED.r = 128 # FrozenInstanceError!Post-Init: __post_init__
__post_init__ metodu, __init__ tamamlandıktan hemen sonra çağrılır. Hesaplanan field'lar, doğrulama veya dönüşüm için idealdir:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
area: float = field(init=False) # init'e dahil değil
circumference: float = field(init=False)
def __post_init__(self):
if self.radius <= 0:
raise ValueError("Yarıçap pozitif olmalı!")
self.area = math.pi * self.radius ** 2
self.circumference = 2 * math.pi * self.radius
c = Circle(5)
print(c)
# Circle(radius=5, area=78.53981633974483, circumference=31.41592653589793)
try:
bad = Circle(-1)
except ValueError as e:
print(e) # Yarıçap pozitif olmalı!Validation ve Normalizasyon
@dataclass
class Email:
address: str
username: str = field(init=False)
domain: str = field(init=False)
def __post_init__(self):
# Normalizasyon
self.address = self.address.lower().strip()
# Doğrulama
if "@" not in self.address:
raise ValueError(f"Geçersiz email: {self.address}")
# Hesaplama
self.username, self.domain = self.address.split("@")
e = Email(" Ali@Gmail.COM ")
print(e.address) # ali@gmail.com
print(e.username) # ali
print(e.domain) # gmail.comInitVar: Sadece Init'te Kullanılan Parametre
Bazen bir parametreyi sadece __post_init__'te kullanmak istersin ama field olarak saklamak istemezsin:
from dataclasses import dataclass, field, InitVar
@dataclass
class DatabaseConnection:
host: str
port: int
password: InitVar[str] # Sadece init'te — field olarak saklanmaz!
connection_string: str = field(init=False)
def __post_init__(self, password):
self.connection_string = f"postgresql://{self.host}:{self.port}?password={password}"
conn = DatabaseConnection("localhost", 5432, "secret123")
print(conn)
# DatabaseConnection(host='localhost', port=5432, connection_string='...')
# password SAKLANMADI — güvenli!
print(conn.connection_string)
# postgresql://localhost:5432?password=secret123
# hasattr(conn, 'password') # False — nesne üzerinde yokDataclass vs Regular Class vs NamedTuple
| Özellik | Regular Class | @dataclass | NamedTuple |
|---|---|---|---|
__init__ otomatik | ❌ | ✅ | ✅ |
__repr__ otomatik | ❌ | ✅ | ✅ |
__eq__ otomatik | ❌ | ✅ | ✅ |
| Mutable | ✅ | ✅ (default) | ❌ |
| Immutable | ❌ (convention) | ✅ (frozen=True) | ✅ (her zaman) |
| Method ekleyebilir | ✅ | ✅ | ✅ (ama alışılmadık) |
| Inheritance | ✅ | ✅ | ⚠️ (sınırlı) |
| Index erişimi | ❌ | ❌ | ✅ (p[0], p[1]) |
| Unpack | ❌ | ❌ | ✅ (x, y = point) |
| Dict key | __hash__ gerekir | frozen=True | ✅ (her zaman) |
| Bellek | Normal | Normal (slots=True ile az) | Az |
Ne Zaman Hangisi?
Regular class: Karmaşık davranışlar, çok metod, özel
__init__gerekiyorsa.Dataclass: Ağırlıklı veri taşıyan, orta karmaşıklıkta sınıflar. En yaygın seçim.
NamedTuple: Basit, immutable, tuple-like veri yapıları. Unpack gerekiyorsa.
typing.NamedTuple: Typed Version
collections.namedtuple'ın modern, type-annotated versiyonu:
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
z: float = 0.0 # Default değer
p = Point(3, 4)
print(p) # Point(x=3, y=4, z=0.0)
print(p.x) # 3
print(p[0]) # 3 — index erişimi!
x, y, z = p # Unpack!
print(x, y, z) # 3 4 0.0
# Immutable
# p.x = 10 # AttributeError
# Hashable — set/dict key olabilir
print(hash(p)) # -3550238948...NamedTuple vs Frozen Dataclass
from typing import NamedTuple
from dataclasses import dataclass
class PointNT(NamedTuple):
x: float
y: float
@dataclass(frozen=True)
class PointDC:
x: float
y: float
# NamedTuple: tuple gibi davranır
pnt = PointNT(3, 4)
print(pnt[0]) # 3 — index erişimi
print(list(pnt)) # [3, 4] — iterable
x, y = pnt # unpack
print(isinstance(pnt, tuple)) # True
# Frozen Dataclass: sınıf gibi davranır
pdc = PointDC(3, 4)
# print(pdc[0]) # TypeError — index yok
# x, y = pdc # TypeError — unpack yok
print(isinstance(pdc, tuple)) # False💡 İpucu: NamedTuple seç: basit veri, tuple uyumluluğu, unpack gerekli. Frozen dataclass seç: daha zengin davranış, method ekleme, kalıtım.
Inheritance ile Dataclass
Dataclass'lar normal sınıflar gibi kalıtımı destekler:
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
company: str
salary: float
@dataclass
class Manager(Employee):
department: str
team_size: int = 0
# Tüm field'lar zincirlenir
m = Manager("Ali", 35, "TechCorp", 50000, "Engineering", 12)
print(m)
# Manager(name='Ali', age=35, company='TechCorp', salary=50000, department='Engineering', team_size=12)
# isinstance çalışır
print(isinstance(m, Person)) # True
print(isinstance(m, Employee)) # TrueKalıtımda Default Değer Kuralı
Önemli bir kural: Default'suz field'lar, default'lu field'lardan sonra gelemez:
@dataclass
class Base:
x: int
y: int = 0 # Default var
# ❌ Bu çalışmaz!
# @dataclass
# class Child(Base):
# z: int # Default yok ama y'nin default'u var → TypeError!
# ✅ Bu çalışır
@dataclass
class Child(Base):
z: int = 0 # Default ekle
# Veya field sırasını düşünerek tasarlaFrozen Dataclass Kalıtımı
@dataclass(frozen=True)
class Coordinate:
x: float
y: float
@dataclass(frozen=True)
class GeoPoint(Coordinate):
name: str = ""
gp = GeoPoint(41.0082, 28.9784, "İstanbul")
print(gp) # GeoPoint(x=41.0082, y=28.9784, name='İstanbul')
# Hâlâ immutable
# gp.x = 0 # FrozenInstanceErrorslots=True: Bellek Optimizasyonu (Python 3.10+)
Normal Python sınıfları attribute'ları __dict__ sözlüğünde saklar. slots=True ile attribute'lar sabit slotlarda saklanır — daha az bellek, daha hızlı erişim:
@dataclass
class RegularPoint:
x: float
y: float
@dataclass(slots=True)
class SlottedPoint:
x: float
y: float
import sys
rp = RegularPoint(3, 4)
sp = SlottedPoint(3, 4)
print(sys.getsizeof(rp.__dict__)) # ~104 bytes (dict overhead)
# sp.__dict__ # AttributeError — __dict__ yok!
# Slotted nesne daha küçük
print(hasattr(rp, '__dict__')) # True
print(hasattr(sp, '__dict__')) # False
print(hasattr(sp, '__slots__')) # TrueSlots'ın Kısıtlamaları
@dataclass(slots=True)
class Strict:
x: int
y: int
s = Strict(1, 2)
# s.z = 3 # AttributeError! Slots dinamik attribute eklemeye izin vermez
# __dict__ olmadığı için vars() çalışmaz
# print(vars(s)) # TypeErrorNe Zaman slots=True Kullan?
Binlerce/milyonlarca nesne oluşturacaksan (bellek tasarrufu).
Performans kritikse (attribute erişimi ~%10-20 daha hızlı).
Dinamik attribute ekleme gerekmiyorsa.
Pratik: Product, Order, User Dataclass'ları
Gerçek bir e-ticaret senaryosu:
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class Product:
"""Ürün bilgisi."""
name: str
price: float
category: str
stock: int = 0
sku: str = ""
description: str = ""
def __post_init__(self):
if self.price < 0:
raise ValueError("Fiyat negatif olamaz!")
if not self.sku:
# Otomatik SKU oluştur
clean = self.name.upper().replace(" ", "-")[:10]
self.sku = f"PRD-{clean}"
@property
def is_in_stock(self):
return self.stock > 0
@property
def price_with_tax(self):
return round(self.price * 1.20, 2) # %20 KDV
def sell(self, quantity=1):
if quantity > self.stock:
raise ValueError(f"Yetersiz stok! Mevcut: {self.stock}")
self.stock -= quantity
return quantity * self.price
@dataclass
class OrderItem:
"""Sipariş kalemi."""
product: Product
quantity: int
unit_price: float = field(init=False)
def __post_init__(self):
self.unit_price = self.product.price
@property
def total(self):
return self.unit_price * self.quantity
@dataclass
class Order:
"""Sipariş."""
customer_name: str
items: list[OrderItem] = field(default_factory=list)
order_id: str = field(default="", init=False)
created_at: datetime = field(default_factory=datetime.now, repr=False)
status: str = "pending"
_order_counter: int = field(default=0, init=False, repr=False)
_counter = 0 # Class variable (dataclass field değil)
def __post_init__(self):
Order._counter += 1
self.order_id = f"ORD-{Order._counter:04d}"
def add_item(self, product, quantity=1):
item = OrderItem(product, quantity)
self.items.append(item)
return self
@property
def total(self):
return sum(item.total for item in self.items)
@property
def total_with_tax(self):
return round(self.total * 1.20, 2)
@property
def item_count(self):
return sum(item.quantity for item in self.items)
def confirm(self):
self.status = "confirmed"
for item in self.items:
item.product.sell(item.quantity)
return f"Sipariş {self.order_id} onaylandı. Toplam: {self.total_with_tax} TL"
def summary(self):
lines = [f"📦 Sipariş: {self.order_id} ({self.status})"]
lines.append(f"👤 Müşteri: {self.customer_name}")
lines.append(f"📅 Tarih: {self.created_at.strftime('%d/%m/%Y %H:%M')}")
lines.append("─" * 40)
for item in self.items:
lines.append(f" {item.product.name} x{item.quantity} = {item.total:.2f} TL")
lines.append("─" * 40)
lines.append(f" Ara Toplam: {self.total:.2f} TL")
lines.append(f" KDV (%20): {self.total * 0.20:.2f} TL")
lines.append(f" TOPLAM: {self.total_with_tax:.2f} TL")
return "\n".join(lines)
@dataclass
class User:
"""Kullanıcı profili."""
username: str
email: str
full_name: str = ""
is_active: bool = True
orders: list[Order] = field(default_factory=list, repr=False)
created_at: datetime = field(default_factory=datetime.now, repr=False)
def __post_init__(self):
self.email = self.email.lower().strip()
if not self.full_name:
self.full_name = self.username.title()
def place_order(self, order: Order):
self.orders.append(order)
@property
def total_spent(self):
return sum(
o.total_with_tax for o in self.orders
if o.status == "confirmed"
)
@property
def order_count(self):
return len(self.orders)# Kullanım
laptop = Product("MacBook Pro", 45000, "Elektronik", stock=10)
mouse = Product("Magic Mouse", 2500, "Elektronik", stock=50)
keyboard = Product("Mechanical KB", 3500, "Elektronik", stock=30)
print(laptop)
# Product(name='MacBook Pro', price=45000, category='Elektronik', stock=10, sku='PRD-MACBOOK-PR', ...)
user = User("ali.yilmaz", "Ali@Gmail.COM", "Ali Yılmaz")
print(user)
# User(username='ali.yilmaz', email='ali@gmail.com', full_name='Ali Yılmaz', is_active=True)
order = Order("Ali Yılmaz")
order.add_item(laptop, 1).add_item(mouse, 2).add_item(keyboard, 1)
print(order.summary())
result = order.confirm()
print(result)
user.place_order(order)
print(f"\nToplam harcama: {user.total_spent} TL")
print(f"Laptop stok: {laptop.stock}") # 9 — 1 satıldıİleri Teknikler
asdict() ve astuple()
from dataclasses import dataclass, asdict, astuple
@dataclass
class Address:
street: str
city: str
zip_code: str
@dataclass
class Contact:
name: str
email: str
address: Address
addr = Address("Atatürk Cad. 123", "İstanbul", "34000")
contact = Contact("Ali", "ali@mail.com", addr)
# Sözlüğe çevir (nested dahil!)
d = asdict(contact)
print(d)
# {'name': 'Ali', 'email': 'ali@mail.com',
# 'address': {'street': 'Atatürk Cad. 123', 'city': 'İstanbul', 'zip_code': '34000'}}
# JSON'a çevirme
import json
print(json.dumps(d, ensure_ascii=False, indent=2))
# Tuple'a çevir
t = astuple(contact)
print(t) # ('Ali', 'ali@mail.com', ('Atatürk Cad. 123', 'İstanbul', '34000'))fields(): Field Bilgilerini Alma
from dataclasses import dataclass, fields, field
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = field(default=False, metadata={"env": "APP_DEBUG"})
for f in fields(Config):
print(f" {f.name}: {f.type.__name__} = {f.default}")
if f.metadata:
print(f" metadata: {f.metadata}")
# host: str = localhost
# port: int = 8080
# debug: bool = False
# metadata: {'env': 'APP_DEBUG'}replace(): Kopyala ve Değiştir
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool = False
config = Config("localhost", 8080)
# Frozen olduğu için değiştiremiyoruz ama kopyalayıp değiştirebiliriz
dev_config = replace(config, debug=True)
prod_config = replace(config, host="0.0.0.0", port=80)
print(config) # Config(host='localhost', port=8080, debug=False)
print(dev_config) # Config(host='localhost', port=8080, debug=True)
print(prod_config) # Config(host='0.0.0.0', port=80, debug=False)Yaygın Hatalar
1. Mutable Default Tuzağı
# ❌ HATA!
# @dataclass
# class Bad:
# items: list = [] # ValueError!
# ✅ DOĞRU
@dataclass
class Good:
items: list = field(default_factory=list)2. Default Sıra Hatası
# ❌ HATA! Default'suz field, default'lu field'dan sonra
# @dataclass
# class Bad:
# x: int = 0
# y: int # TypeError!
# ✅ DOĞRU
@dataclass
class Good:
y: int # Default'suz önce
x: int = 0 # Default'lu sonra3. __post_init__'te Self Kullanmayı Unutmak
@dataclass
class Example:
name: str
def __post_init__(self):
# self.name zaten set edilmiş, kullanabilirsin
self.name = self.name.strip().title()💡 İpucu: Dataclass'lar çok güçlü ama her sınıf dataclass olmak zorunda değil. Eğer sınıfın ağırlıklı olarak veri taşıyorsa ve çok metodu yoksa → dataclass. Eğer karmaşık davranışlar, özel init mantığı veya performans optimizasyonu gerekiyorsa → normal class. İkisini de bilmek ve doğru yerde doğru olanı kullanmak önemli.
Özet
`@dataclass` decorator'ı
__init__,__repr__,__eq__metodlarını otomatik oluşturur — boilerplate killer.`field()` ile default değer, repr/init/compare dahil etme, metadata gibi seçenekler ayarlanır. Mutable default'lar için
default_factorykullan.`frozen=True` ile immutable dataclass oluşturulur. Otomatik hashable — set/dict key olarak kullanılabilir.
`__post_init__` metodu init sonrası hesaplama, doğrulama ve normalizasyon için kullanılır.
Dataclass vs regular class vs NamedTuple: Veri ağırlıklıysa dataclass, karmaşık davranış varsa regular class, basit/immutable/tuple-like ise NamedTuple.
`slots=True` (Python 3.10+) bellek optimizasyonu sağlar — binlerce nesne oluştururken faydalı.
asdict(),astuple(),fields(),replace()yardımcı fonksiyonlar güçlü araçlar sunar.
AI Asistan
Sorularını yanıtlamaya hazır