Ana içeriğe geç
Kesit Python
Orta · Nesne yönelimli programlama

dataclass ve tip ipuçları (type hints)

dataclass ile boilerplate'siz veri sınıfı tanımlar; tip ipuçları, Optional, TypedDict ve Protocol ile kodu kendini belgeleyen ve mypy ile denetlenebilir hale getirirsin.

Uygulamada aç, 21 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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))
field(default=[]) de olmazdataclass liste/sözlük/küme varsayılanı görünce 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çenekNeredeNe yapar
default=fieldSabit varsayılan (değişmez değerler için)
default_factory=fieldHer örnek için yeni nesne üretir (list, dict, set)
repr=FalsefieldAlanı __repr__ dışında bırakır (uzun ya da gizli veri)
compare=FalsefieldAlanı __eq__ ve sıralamadan çıkarır
frozen=TruedataclassDeğiştirilemez, hashlenebilir (küme/sözlük anahtarı)
order=Truedataclass<, > vb. alan sırasına göre
slots=Truedataclass (3.10+)__slots__ üretir, bellek azalır
kw_only=Truedataclass (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
None dönebilen fonksiyonlar-> 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.

Aşamalı tiplemeHer şeyi tiplemek zorunda değilsin. Önce fonksiyon imzalarını, özellikle modül sınırlarını ve veri sınıflarını tiple; iç değişkenleri Python zaten çıkarsar. mypy varsayılan olarak tiplenmemiş fonksiyonları denetlemez; --strict ile hepsini ister.
Derinleş

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 çevirJSON'a, veritabanı satırına yazmak
replace(n, **d)Değişikliklerle yeni nesnefrozen nesneyi güncellemek
field(init=False)Kurucuda yok, hesaplanırtoplam, oluşturulma no
InitVar[T]Kurucuda var, saklanmazham şifre, ham metin
ClassVar[T]Alan değil, sınıf niteliğiKDV oranı, sayaç
kw_only=TrueAlanlar yalnız adla verilirkalı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.

from __future__ import annotationsBu satır tüm ipuçlarını metin olarak saklar; ileri başvurular (-> "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.
Ne zaman Any?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.
Gerçek işteKurumsal projelerde veri sınıfları genellikle bir 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.
Uzmanlaş

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.

Alıştırmalara geç (21)