Neden önemli?
Önceki derste Hesap sınıfı için __init__, __repr__ yazdın; iki hesabı karşılaştırmak için __eq__ de gerekti. Anadolu Market'in sipariş, müşteri, ürün, kargo gibi onlarca veri tipi için bunu tekrarlamak hem sıkıcı hem hataya açıktır. dataclass bu metotları tip ipuçlarına bakarak otomatik üretir. Tip ipuçları (type hints) ise kodun "bu fonksiyon ne alır, ne döner" sorusunu okuyana ve araçlara söyler: editör otomatik tamamlar, mypy hataları çalıştırmadan yakalar, ekip arkadaşın fonksiyonu açmadan kullanır. Kurumsal Python kodunun bugünkü standardı budur.
dataclass temelleri
from dataclasses import dataclass
@dataclass
class Urun:
kod: str
ad: str
fiyat: float
stok: int = 0 # varsayılanlı alanlar sona
k = Urun("K-100", "Klavye", 850.0, 12)
m = Urun("M-200", "Mouse", fiyat=320.0)
print(k) # __repr__ hazır
print(m.stok)
print(k == Urun("K-100", "Klavye", 850.0, 12)) # __eq__ alan bazlı
k.stok -= 1
print(k.stok)
Dataclass sıradan bir sınıftır: metot, property, classmethod ekleyebilirsin. Kurucudan sonra çalışması gereken doğrulama için __post_init__ vardır.
from dataclasses import dataclass
@dataclass
class Urun:
kod: str
ad: str
fiyat: float
stok: int = 0
def __post_init__(self):
if self.fiyat < 0:
raise ValueError("fiyat negatif olamaz")
self.kod = self.kod.upper()
@property
def kdvli(self) -> float:
return round(self.fiyat * 1.2, 2)
def satis(self, adet: int) -> float:
if adet > self.stok:
raise ValueError("stok yetersiz")
self.stok -= adet
return round(self.kdvli * adet, 2)
u = Urun("k-100", "Klavye", 850.0, 5)
print(u.kod, u.kdvli)
print(u.satis(2), "TL;", u.stok, "kaldı")
try:
Urun("x", "Bozuk", -1)
except ValueError as e:
print("Hata:", e)
field, frozen, order, asdict
Değiştirilebilir varsayılan (liste, sözlük) doğrudan yazılamaz; sınıf niteliği tuzağını hatırla. Bunun için field(default_factory=list) kullanılır. frozen=True nesneyi değiştirilemez (ve hashlenebilir) yapar; order=True karşılaştırma metotlarını alan sırasına göre üretir; asdict nesneyi iç içe sözlüğe çevirir (JSON'a hazırlamak için).
from dataclasses import dataclass, field, asdict
@dataclass(frozen=True, order=True)
class Para:
tutar: float
birim: str = "TL"
@dataclass
class Siparis:
no: int
musteri: str
kalemler: list[str] = field(default_factory=list) # her örneğe yeni liste
toplam: Para = Para(0.0)
notlar: dict[str, str] = field(default_factory=dict, repr=False)
s1 = Siparis(1001, "Ayşe")
s2 = Siparis(1002, "Can")
s1.kalemler.append("K-100")
print(s1.kalemler, s2.kalemler) # paylaşılmıyor
p = Para(100.0)
try:
p.tutar = 5
except Exception as e:
print(type(e).__name__)
print(sorted([Para(30), Para(5), Para(12)])) # order=True
print({Para(1), Para(1)}) # frozen -> hashlenebilir
print(asdict(s1))
ValueError ile seni durdurur. Doğru yol her zaman default_factory: çağrılabilir bir şey ver, her örnek için yeniden çağrılsın.| Seçenek | Nerede | Ne yapar |
|---|---|---|
default= | field | Sabit varsayılan (değişmez değerler için) |
default_factory= | field | Her örnek için yeni nesne üretir (list, dict, set) |
repr=False | field | Alanı __repr__ dışında bırakır (uzun ya da gizli veri) |
compare=False | field | Alanı __eq__ ve sıralamadan çıkarır |
frozen=True | dataclass | Değiştirilemez, hashlenebilir (küme/sözlük anahtarı) |
order=True | dataclass | <, > vb. alan sırasına göre |
slots=True | dataclass (3.10+) | __slots__ üretir, bellek azalır |
kw_only=True | dataclass (3.10+) | Tüm alanlar anahtar kelimeyle verilir |
Tip ipuçları: sözdizimi
Parametreye ad: tip, dönüşe -> tip yazılır. Python 3.9+ ile yerleşik koleksiyonlar doğrudan genel tip olur (list[int], dict[str, float]); 3.10+ ile X | None, eski Optional[X]'in yerini alır. Tip ipuçları çalışma zamanında zorlanmaz: yanlış tip verirsen Python ses çıkarmaz; denetim editörün ve mypy'nin işidir.
def ortalama(tutarlar: list[float]) -> float:
return sum(tutarlar) / len(tutarlar) if tutarlar else 0.0
def musteri_bul(musteriler: dict[str, str], no: str) -> str | None:
return musteriler.get(no) # yoksa None: imza bunu açıkça söylüyor
def etiketle(no: int, kalemler: list[str], oncelik: bool = False) -> tuple[int, str]:
etiket = ("ACİL " if oncelik else "") + ", ".join(kalemler)
return no, etiket
print(ortalama([450.0, 120.0, 980.0]))
print(musteri_bul({"M1": "Ayşe"}, "M1"), musteri_bul({"M1": "Ayşe"}, "M9"))
print(etiketle(1001, ["K-100", "M-200"], oncelik=True))
# Çalışma zamanında zorlanmaz:
print(ortalama.__annotations__)
print(etiketle("hatalı", [])) # mypy bunu yakalar, Python çalıştırır
-> str | None yazdığında çağıran taraf None kontrolü yapmak zorunda kalır (mypy uyarır). Bu, "boş sonuç" hatalarının yarısını kodu çalıştırmadan yakalar.Takma ad, Literal ve Callable
from typing import Literal, Callable
HesapNo = str # tip takma adı: okunurluk
Durum = Literal["bekliyor", "onaylandi", "reddedildi"]
def durum_guncelle(no: HesapNo, durum: Durum) -> str:
return f"{no}: {durum}"
def uygula(tutarlar: list[float], f: Callable[[float], float]) -> list[float]:
return [f(t) for t in tutarlar]
print(durum_guncelle("TR01", "onaylandi"))
print(uygula([100.0, 250.0], lambda t: round(t * 1.2, 2)))
TypedDict ve Protocol (kısa)
TypedDict, elindeki veri zaten sözlükse (JSON'dan geldi, dataclass'a çevirmek istemiyorsun) anahtarlarını ve tiplerini belgelemeni sağlar; çalışma zamanında düz sözlüktür. Protocol, "şu metotlara sahip olan her nesne" demektir; ördek tiplemenin tip sistemindeki karşılığıdır ve kalıtım (inheritance) gerektirmez.
from typing import TypedDict, Protocol
class SiparisKaydi(TypedDict):
no: int
sehir: str
tutar: float
def sehir_toplam(kayitlar: list[SiparisKaydi], sehir: str) -> float:
return sum(k["tutar"] for k in kayitlar if k["sehir"] == sehir)
veri: list[SiparisKaydi] = [
{"no": 1, "sehir": "Ankara", "tutar": 450.0},
{"no": 2, "sehir": "İzmir", "tutar": 120.0},
{"no": 3, "sehir": "Ankara", "tutar": 980.0},
]
print(sehir_toplam(veri, "Ankara"))
print(type(veri[0]).__name__) # düz dict
class Odenebilir(Protocol):
def ode(self, tutar: float) -> str: ...
class KrediKarti: # Odenebilir'den kalıtım almıyor
def ode(self, tutar: float) -> str:
return f"KK ile {tutar} ödendi"
def tahsil_et(yontem: Odenebilir, tutar: float) -> str:
return yontem.ode(tutar)
print(tahsil_et(KrediKarti(), 149.9))
mypy mantığı
mypy dosya.py komutu kodu çalıştırmadan tip ipuçlarını denetler: ortalama("abc") gibi çağrıları, None kontrolü yapılmadan kullanılan değerleri, eksik dönüşleri raporlar. Tarayıcıdaki bu ortamda mypy yok, ama mantığı şudur: tip ipucu belgeleme + statik denetimdir, performans ya da çalışma zamanı güvenliği değildir. Gerçek projede CI'da mypy (ya da pyright) çalıştırılır; "Kalite ve CI" dersinde kuracaksın.
--strict ile hepsini ister.Derinleş: dataclass'ın az bilinen araçları ve tiplerle yaşamak
Temel @dataclass kullanımını, field, frozen ve asdict'i gördün. Gerçek projede hemen şu sorularla karşılaşırsın: Kurucuda verilmeyen ama hesaplanan bir alanı nasıl tutarım? Değişmez bir nesnenin "bir alanı farklı" kopyasını nasıl üretirim? Sınıf sabiti neden alan sanılıyor? Kalıtımda neden TypeError alıyorum? JSON'dan iç içe dataclass nasıl kurulur? Bu bölüm bunları yanıtlıyor. Generic, overload ve NewType "İleri tip sistemi" dersinde, çalışma zamanı doğrulaması "Pydantic" dersinde.
1. Hesaplanan alanlar: init=False, InitVar ve ClassVar
field(init=False) alanı kurucudan çıkarır; değerini __post_init__ içinde sen verirsin ama repr, == ve asdict'te görünmeye devam eder. InitVar ise tersidir: kurucuya verilir, __post_init__'e parametre olarak gelir ama alan olarak saklanmaz (ör. ham şifre, ham metin). ClassVar ile işaretlenen ad ise alan değil, sınıf niteliğidir; dataclass ona hiç dokunmaz.
from dataclasses import dataclass, field, InitVar, fields, asdict
from typing import ClassVar
@dataclass
class Fatura:
KDV: ClassVar[float] = 0.20 # sınıf sabiti: alan DEĞİL
no: str
kalemler: list[float]
indirim_kodu: InitVar[str | None] = None # kurucuya gelir, saklanmaz
indirim: float = field(init=False, default=0.0)
toplam: float = field(init=False)
def __post_init__(self, indirim_kodu):
ara = sum(self.kalemler)
if indirim_kodu == "YAZ10":
self.indirim = round(ara * 0.10, 2)
self.toplam = round((ara - self.indirim) * (1 + self.KDV), 2)
f = Fatura("F-1", [400.0, 600.0], indirim_kodu="YAZ10")
print(f)
print([x.name for x in fields(f)]) # KDV ve indirim_kodu yok
print(asdict(f))
2. Değişmez nesneyi "değiştirmek": replace
frozen=True nesneye atama yapamazsın; ama çoğu zaman "aynısı, yalnız şu alanı farklı" bir nesne istersin. dataclasses.replace(nesne, alan=yeni) tam bunu yapar: yeni bir nesne üretir, __post_init__'i yeniden çalıştırır (doğrulama atlanmaz). Frozen sınıfın __post_init__'inde bir alanı düzeltmen gerekirse self.x = ... hata verir; çıkış yolu object.__setattr__(self, "x", deger)'dir.
from dataclasses import dataclass, replace, FrozenInstanceError
@dataclass(frozen=True)
class Adres:
sehir: str
ilce: str
posta_kodu: str
def __post_init__(self):
if len(self.posta_kodu) != 5 or not self.posta_kodu.isdigit():
raise ValueError(f"geçersiz posta kodu: {self.posta_kodu}")
object.__setattr__(self, "sehir", self.sehir.strip().title())
a = Adres(" ankara ", "Çankaya", "06690")
b = replace(a, ilce="Yenimahalle", posta_kodu="06170")
print(a)
print(b, a is b)
try:
a.ilce = "Keçiören"
except FrozenInstanceError:
print("frozen: atama yok, replace kullan")
try:
replace(a, posta_kodu="6")
except ValueError as e:
print(e)
| Araç | Ne işe yarar | Örnek |
|---|---|---|
fields(cls) | Alanların listesi (ad, tip, varsayılan) | CSV başlığı üretmek |
asdict / astuple | İç içe sözlüğe / demete çevir | JSON'a, veritabanı satırına yazmak |
replace(n, **d) | Değişikliklerle yeni nesne | frozen nesneyi güncellemek |
field(init=False) | Kurucuda yok, hesaplanır | toplam, oluşturulma no |
InitVar[T] | Kurucuda var, saklanmaz | ham şifre, ham metin |
ClassVar[T] | Alan değil, sınıf niteliği | KDV oranı, sayaç |
kw_only=True | Alanlar yalnız adla verilir | kalıtımda sıra sorunu |
3. Kalıtım ve "non-default argument follows default argument"
Dataclass alt sınıfının alanları üst sınıfınkilerin arkasına eklenir. Üst sınıfta varsayılanlı bir alan varsa alt sınıfa varsayılansız alan ekleyemezsin, çünkü üretilen __init__ geçersiz olur ve TypeError alırsın. Python 3.10+ çözümü kw_only=True'dur: anahtar kelimeyle verilen parametrelerde sıra sorunu yoktur.
from dataclasses import dataclass, field
@dataclass(kw_only=True)
class Kayit:
olusturan: str = "sistem"
etiketler: list[str] = field(default_factory=list)
@dataclass(kw_only=True)
class Talep(Kayit):
no: int # varsayılansız: kw_only sayesinde sorun yok
konu: str
t = Talep(no=42, konu="Sokak lambası arızası", olusturan="çağrı merkezi")
print(t)
try:
Talep(42, "Çukur") # konumsal verilemez
except TypeError as e:
print("TypeError:", e)
4. JSON'dan iç içe dataclass kurmak
asdict gidiş yolunu çözer ama dönüş yolu (sözlükten nesneye) senin işindir: Urun(**d) düz sınıflar için yeter, iç içe listeler için her alt sözlüğü kendi sınıfına çevirmen gerekir. Bilinmeyen anahtarlar (**d ile TypeError verir) için fields() ile süzersin. Bu "dönüştürücü" kodu tek bir @classmethod içinde topla.
import json
from dataclasses import dataclass, field, fields, asdict
@dataclass
class Kalem:
sku: str
adet: int
fiyat: float
@dataclass
class Siparis:
no: int
kalemler: list[Kalem] = field(default_factory=list)
@classmethod
def sozlukten(cls, d: dict) -> "Siparis":
izinli = {f.name for f in fields(Kalem)}
kalemler = [Kalem(**{k: v for k, v in x.items() if k in izinli})
for x in d.get("kalemler", [])]
return cls(no=int(d["no"]), kalemler=kalemler)
ham = '{"no": "7", "kalemler": [{"sku": "K-1", "adet": 2, "fiyat": 90.0, "renk": "siyah"}]}'
s = Siparis.sozlukten(json.loads(ham))
print(s)
metin = json.dumps(asdict(s))
print(metin)
print(Siparis.sozlukten(json.loads(metin)) == s)
5. Tiplerle yaşamak: daraltma, Final ve Self
str | None dönen bir değeri kullanmadan önce if x is None: return ... yazdığında, tip denetleyici o satırdan sonra x'in str olduğunu bilir; buna tip daraltma (narrowing) denir. isinstance da daraltır. typing.Final bir adın yeniden atanmaması gerektiğini söyler (MAX_TUTAR: Final = 50_000); typing.Self (3.11+) ise return self ya da return cls(...) yapan metotların dönüş tipidir, alt sınıflarda da doğru tipi verir. Hiçbiri çalışma zamanında zorlanmaz; mypy/pyright ve editörün denetler.
-> "Siparis" yerine -> Siparis) tırnaksız yazılabilir. Ama __annotations__'a bakan kodlar artık tip nesnesi değil dize görür; gerçek tipleri almak için typing.get_type_hints(fonksiyon) kullan.Any denetimi kapatır; "her şey olabilir ve umurumda değil" demektir. "Her şey olabilir ama kullanmadan önce türünü kontrol edeceğim" diyorsan object yaz: mypy seni isinstance ile daraltmaya zorlar.models.py ya da domain/ klasöründe durur; dış dünyayla temas eden her yerde (API yanıtı, CSV satırı, kuyruk mesajı) sozlukten/sozluge gibi tek bir dönüştürücü bulunur. Değer nesneleri (para, adres, tarih aralığı) frozen=True yapılır ve replace ile güncellenir; böylece bir fonksiyonun adresini "yanlışlıkla" değiştirmesi imkânsızlaşır. CI'da mypy --strict ya da pyright çalıştıran ekiplerde imzasız fonksiyon kod incelemesinden (code review) geçmez.dataclass mı NamedTuple mı sözlük mü?
- dataclass: varsayılan seçim; değiştirilebilir ya da frozen, metotlu, tipli.
- typing.NamedTuple: küçük, değişmez, demet gibi açılabilir kayıtlar (koordinat, tarih aralığı).
- TypedDict: veri zaten JSON/sözlük biçimindeyse ve öyle kalacaksa.
- düz sözlük: anahtarlar çalışma zamanında belirleniyorsa (ör. şehir → toplam).
Generic ve TypeVar
def ilk(x: list[T]) -> T gibi "içeri ne girerse o çıkar" imzalar TypeVar ile yazılır; Python 3.12 bunu def ilk[T](x: list[T]) -> T kısa sözdizimine indirdi. "İleri tip sistemi" dersinde Generic sınıflar, ParamSpec ve overload gelecek.
Çalışma zamanı doğrulama
Dış dünyadan (API, dosya) gelen veriyi tip ipucu korumaz. Üretim ortamında pydantic gibi kütüphaneler dataclass benzeri sınıfları çalışma zamanında doğrular ve JSON'dan güvenle kurar. Standart kütüphanede kalacaksan __post_init__ içinde elle kontrol et.