Neden önemli?
Orta seviyede sınıf yazmayı, kalıtımı (inheritance) ve dataclass'ı öğrendin. Kurumsal kodda ise sınıfların "Python gibi" davranması beklenir: bir Para nesnesi == ile karşılaştırılabilmeli, bir kümeye konabilmeli, sorted ile sıralanabilmeli; bir Sepet nesnesi len() ve for ile kullanılabilmeli. Ilgaz Bank'ta bir işlem motoru yazarken "bu iki tutar eşit mi?" sorusunun cevabı id() karşılaştırması olursa raporlar yanlış çıkar. Bu dersin konusu, sınıflarını dil ile bütünleştiren özel metotlar (dunder methods, "double underscore") ve kodun sınırlarını tanımlayan soyutlama araçlarıdır.
Eşitlik, hash ve sıralama
Varsayılan olarak iki nesne yalnızca aynı nesneyse eşittir. __eq__ yazarak "değer eşitliği" tanımlarsın. Dikkat: __eq__ yazıp __hash__ yazmazsan Python nesneyi hashlenemez yapar; küme ve sözlük anahtarı olarak kullanamazsın. Değişmez (immutable) nesnelerde __hash__'i eşitlikte kullandığın alanlardan üret.
from functools import total_ordering
@total_ordering
class Para:
"""Ilgaz Bank: kuruş hassasiyetinde tutar."""
__slots__ = ('kurus', 'birim')
def __init__(self, tutar: float, birim: str = 'TRY'):
self.kurus = round(tutar * 100)
self.birim = birim
def __repr__(self):
return f'Para({self.kurus / 100:.2f}, {self.birim!r})'
def __eq__(self, other):
if not isinstance(other, Para):
return NotImplemented
return (self.kurus, self.birim) == (other.kurus, other.birim)
def __hash__(self):
return hash((self.kurus, self.birim))
def __lt__(self, other):
if not isinstance(other, Para) or self.birim != other.birim:
return NotImplemented
return self.kurus < other.kurus
def __add__(self, other):
if not isinstance(other, Para) or self.birim != other.birim:
return NotImplemented
return Para((self.kurus + other.kurus) / 100, self.birim)
a, b, c = Para(10.10), Para(10.10), Para(3.5)
print(a == b, a is b) # True False
print(len({a, b, c})) # 2 -> hash tutarlı
print(sorted([a, c, Para(7)])) # küçükten büyüğe
print(a + c, a > c)
functools.total_ordering, yalnızca __eq__ ve bir karşılaştırma metodu (örneğin __lt__) tanımladığında geri kalan __le__, __gt__, __ge__ metotlarını üretir. NotImplemented döndürmek, "ben bilmiyorum, karşı taraf denesin" demektir; raise etmekten farklıdır.Koleksiyon gibi davranmak
Dört metot yeter: __len__, __getitem__, __iter__, __contains__. Yalnızca __getitem__ yazarsan Python 0, 1, 2 diye indeksleyerek for döngüsünü ve in kontrolünü bile üretir; ama açıkça __iter__ yazmak daha hızlı ve nettir.
class Sepet:
def __init__(self):
self._kalemler: list[tuple[str, int, float]] = []
def ekle(self, urun, adet, fiyat):
self._kalemler.append((urun, adet, fiyat))
return self # zincirleme için
def __len__(self):
return len(self._kalemler)
def __getitem__(self, i): # int ve dilim (slice) destekler
return self._kalemler[i]
def __iter__(self):
return iter(self._kalemler)
def __contains__(self, urun):
return any(k[0] == urun for k in self._kalemler)
def __call__(self, kdv=0.20): # sepet(0.2) -> toplam
return round(sum(a * f for _, a, f in self._kalemler) * (1 + kdv), 2)
def __bool__(self):
return bool(self._kalemler)
s = Sepet().ekle('kahve', 2, 150.0).ekle('bardak', 4, 40.0)
print(len(s), 'kahve' in s, 'çay' in s)
print(s[0], s[-1:])
for urun, adet, fiyat in s:
print(f'{urun:<8}{adet:>3} x {fiyat:>7.2f}')
print('KDV dahil:', s())
print('boş sepet:', bool(Sepet()))
__bool__, o yoksa __len__ ile belirlenir. Koleksiyon sınıflarında __len__ yazdığında if sepet: otomatik olarak "sepet boş mu?" anlamına gelir.ABC: zorunlu arayüz
abc.ABC ve @abstractmethod, alt sınıfın mutlaka gerçekleştirmesi gereken metotları tanımlar. Eksik bırakılan metot varsa sınıf örneklenemez; hata, nesne yaratılırken, yani erken çıkar. Ödeme sağlayıcıları, bildirim kanalları, depolama katmanları gibi "birden fazla gerçekleştirme, tek sözleşme" durumlarında kullan.
from abc import ABC, abstractmethod
class OdemeSaglayici(ABC):
@abstractmethod
def tahsil_et(self, tutar: float) -> str: ...
def makbuz(self, tutar): # somut metot: alt sınıflara miras
return f'{self.__class__.__name__}: {tutar:.2f} TL tahsil edildi'
class KrediKarti(OdemeSaglayici):
def tahsil_et(self, tutar):
return self.makbuz(tutar)
class Havale(OdemeSaglayici):
pass # tahsil_et eksik!
print(KrediKarti().tahsil_et(250))
try:
Havale()
except TypeError as e:
print('Hata:', e)
print(issubclass(KrediKarti, OdemeSaglayici), isinstance(KrediKarti(), OdemeSaglayici))
Protocol: yapısal tipleme
ABC "kalıtımla sözleşme" kurar; typing.Protocol ise "bu metotlara sahip olan herkes uyar" der (ördek tipleme, duck typing). Alt sınıf olmak gerekmez; @runtime_checkable eklersen isinstance da çalışır. Dış kütüphanelerin sınıflarını değiştiremediğin, ama onları kendi fonksiyonlarına parametre olarak almak istediğin durumlarda Protocol tek çözümdür.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Raporlanabilir(Protocol):
def satir(self) -> str: ...
class Siparis:
def __init__(self, no, tutar): self.no, self.tutar = no, tutar
def satir(self): return f'SIP-{self.no} {self.tutar:.2f}'
class Musteri:
def __init__(self, ad): self.ad = ad
def satir(self): return f'MUS {self.ad}'
def yazdir(kayitlar: list[Raporlanabilir]):
for k in kayitlar:
print(k.satir())
yazdir([Siparis(1, 99.9), Musteri('Ayşe')])
print(isinstance(Siparis(2, 1), Raporlanabilir), isinstance(3, Raporlanabilir))
| Özellik | ABC | Protocol |
|---|---|---|
| İlişki | Nominal: alt sınıf olmalı | Yapısal: metotlar uyuşmalı |
| Eksik metot | Örnekleme anında TypeError | Statik denetleyici (mypy) uyarır |
| Somut metot paylaşımı | Evet, miras alınır | Hayır (yalnızca imza) |
| Dış kütüphane sınıfı | Uyarlamak gerekir | Olduğu gibi uyar |
| Tipik kullanım | Kendi eklenti sistemin | Fonksiyon parametresi sözleşmesi |
Descriptor ve property
property tek alana doğrulama eklemek için idealdir. Aynı doğrulamayı on sınıfta yirmi alana uygulayacaksan descriptor yaz: __set_name__, __get__, __set__ metotlarına sahip bir sınıf. Pydantic, Django ORM ve dataclass'ın field mekanizması bu altyapıya dayanır.
class Pozitif:
"""Alan değeri pozitif sayı olmalı; aksi halde ValueError."""
def __set_name__(self, owner, name):
self.ad = name
self.gizli = '_' + name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.gizli)
def __set__(self, obj, deger):
if not isinstance(deger, (int, float)) or deger <= 0:
raise ValueError(f'{self.ad} pozitif olmalı, gelen: {deger!r}')
setattr(obj, self.gizli, deger)
class KrediBasvurusu:
tutar = Pozitif()
vade_ay = Pozitif()
def __init__(self, tutar, vade_ay):
self.tutar = tutar
self.vade_ay = vade_ay
@property
def aylik_taksit(self): # salt okunur hesaplanmış alan
return round(self.tutar / self.vade_ay, 2)
k = KrediBasvurusu(120_000, 24)
print(k.tutar, k.vade_ay, k.aylik_taksit)
try:
k.vade_ay = 0
except ValueError as e:
print('Hata:', e)
__slots__, classmethod, staticmethod
__slots__ nesnenin __dict__ sözlüğünü kaldırır: bellek düşer, öznitelik erişimi hızlanır, yanlışlıkla self.tutr = 5 yazmak AttributeError verir. Milyonlarca küçük nesne ürettiğin (işlem kayıtları, log satırları) sınıflarda değerlidir; dinamik öznitelik eklemen gereken yerlerde kullanma.
@classmethod alternatif kurucu (constructor) yazmanın standart yoludur: Siparis.from_dict(...), datetime.fromisoformat(...). İlk parametresi sınıfın kendisidir; alt sınıftan çağrılınca alt sınıfı üretir. @staticmethod ise sınıfla ilgili ama ne örneğe ne sınıfa ihtiyaç duyan yardımcı fonksiyondur (ör. doğrulama).
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Islem:
hesap: str
tutar: float
@staticmethod
def hesap_gecerli(hesap: str) -> bool:
return hesap.startswith('TR') and len(hesap) == 26
@classmethod
def from_dict(cls, d: dict):
if not cls.hesap_gecerli(d['hesap']):
raise ValueError('IBAN geçersiz')
return cls(hesap=d['hesap'], tutar=float(d['tutar']))
i = Islem.from_dict({'hesap': 'TR' + '0' * 24, 'tutar': '150.75'})
print(i)
try:
i.ekstra = 1
except (AttributeError, TypeError) as e: # Python 3.12'de frozen+slots bazen TypeError verir
print('slots engelledi:', type(e).__name__)
__slots__ tanımlamazsa __dict__ geri gelir ve kazanım kaybolur. Çoklu kalıtımda iki ebeveynin ikisi de boş olmayan __slots__ taşıyorsa "layout conflict" hatası alırsın.Metaclass'a kısa bakış
Metaclass, sınıfın sınıfıdır: type. class X: ifadesi aslında type('X', (), {...}) çağrısıdır. Kendi metaclass'ını yazarak sınıf tanımlanırken müdahale edebilirsin (otomatik kayıt, alan doğrulama). Uygulama kodunda nadiren gerekir; çoğu ihtiyaç __init_subclass__ ile daha basit çözülür:
class Eklenti:
kayit: dict[str, type] = {}
def __init_subclass__(cls, anahtar=None, **kw):
super().__init_subclass__(**kw)
Eklenti.kayit[anahtar or cls.__name__.lower()] = cls
class CsvDisaAktar(Eklenti, anahtar='csv'): ...
class JsonDisaAktar(Eklenti, anahtar='json'): ...
print(sorted(Eklenti.kayit))
print(type(Eklenti), type(type))
Nesnenin yüzü: __repr__, __str__ ve __format__
Bir nesneyi ekrana üç farklı yol taşır. __repr__ geliştirici içindir: hata ayıklayıcıda, logda, liste içinde görünür; mümkünse nesneyi yeniden üretecek ifadeyi (Para(1250, 'TRY')) döndürmelidir. __str__ son kullanıcı içindir ve print/str() çağırır; tanımlı değilse __repr__ kullanılır. __format__ ise f-string'deki biçim belirtecini (f"{x:tl}") yorumlamanı sağlar.
class Para:
def __init__(self, kurus, birim="TRY"):
self.kurus, self.birim = kurus, birim
def __repr__(self):
return f"Para({self.kurus!r}, {self.birim!r})"
def __str__(self):
return f"{self.kurus / 100:.2f} {self.birim}"
def __format__(self, spec):
if spec == "tl": # Türkçe biçim: 1.234,50 TL
metin = f"{self.kurus / 100:,.2f}".replace(",", "_").replace(".", ",").replace("_", ".")
return f"{metin} TL"
return format(str(self), spec)
p = Para(123450)
print(p) # __str__
print([p]) # liste içinde __repr__
print(f"{p!r} | {p:tl} | {p:>14}|")
Aritmetik: yansıyan operatörler ve NotImplemented
a + b önce a.__add__(b)'yi dener. Bu NotImplemented döndürürse Python yansıyan (reflected) metodu, b.__radd__(a)'yı dener. sum() toplamaya 0 ile başladığı için 0 + Para(...) hesaplanır; sınıfın sum ile çalışması için __radd__ şarttır. Yerinde işlemler (+=) için __iadd__ tanımlamazsan Python __add__ ile yeni nesne üretir; değişmez değer nesnelerinde (value object) istenen de budur.
| Amaç | Dunder | Not |
|---|---|---|
a + b, b + a | __add__, __radd__ | Tanımadığın türde NotImplemented döndür, hata fırlatma |
a += b | __iadd__ | Yoksa __add__ kullanılır |
-a, abs(a) | __neg__, __abs__ | İade, ters kayıt |
if a: | __bool__ | Yoksa __len__, o da yoksa hep True |
| Sıralama | __eq__ + __lt__ + @total_ordering | Diğer dördünü dekoratör (decorator) üretir |
with a: | __enter__, __exit__ | __exit__ True dönerse hata yutulur |
a.x bulunamadı | __getattr__ | Yalnızca normal arama başarısızsa çağrılır |
| Alt sınıf tanımlandı | __init_subclass__ | Metaclass'sız eklenti kaydı |
from functools import total_ordering
@total_ordering
class Tutar:
def __init__(self, kurus):
self.kurus = kurus
def __repr__(self):
return f"Tutar({self.kurus})"
def __add__(self, other):
if isinstance(other, Tutar):
return Tutar(self.kurus + other.kurus)
return NotImplemented
def __radd__(self, other):
if other == 0: # sum() başlangıç değeri
return self
return self.__add__(other)
def __eq__(self, other):
return isinstance(other, Tutar) and self.kurus == other.kurus
def __lt__(self, other):
if not isinstance(other, Tutar):
return NotImplemented
return self.kurus < other.kurus
__hash__ = None # değişebilir: hash'lenmesin
islemler = [Tutar(500), Tutar(1200), Tutar(300)]
print(sum(islemler), max(islemler), sorted(islemler))
print(Tutar(5) >= Tutar(3), Tutar(5) != Tutar(5))
Temsil ve sarmalama: __getattr__
__getattr__ yalnızca nitelik normal yollarla (örnek sözlüğü, sınıf, üst sınıflar) bulunamadığında çağrılır. Bu yüzden bir nesneyi sarıp bilmediğin her isteği içteki nesneye devretmek (delegation, proxy) için idealdir. __getattribute__ ise her erişimde çağrılır; yanlış yazılırsa sonsuz özyinelemeye (recursion) girer, nadiren gerekir. __setattr__ yazarken atamayı super().__setattr__(ad, deger) ile yap; self.ad = deger yine __setattr__'ı çağırır.
class Loglu:
def __init__(self, hedef):
self._hedef = hedef
self.cagrilar = []
def __getattr__(self, ad): # yalnız bulunamayınca
nitelik = getattr(self._hedef, ad)
if callable(nitelik):
def sarmal(*a, **k):
self.cagrilar.append(ad)
return nitelik(*a, **k)
return sarmal
return nitelik
stok = Loglu({"elma": 10})
stok.get("elma"); stok.setdefault("armut", 0)
print(stok.cagrilar, len(stok._hedef))
Alt sınıfları otomatik kaydet: __init_subclass__
Tasarım kalıpları dersinde kayıt (registry) fabrikasını dekoratörle kurdun. Taban sınıfa __init_subclass__ yazarsan her alt sınıf tanımlandığı anda taban sınıfa haber verir; dekoratörü unutmak mümkün olmaz. Sınıf satırındaki anahtar sözcükler (class X(Taban, kod="pdf")) bu metoda iletilir.
class Disa:
turler = {}
def __init_subclass__(cls, kod, **kw):
super().__init_subclass__(**kw)
Disa.turler[kod] = cls
class CSV(Disa, kod="csv"):
def yaz(self, satirlar): return "\n".join(",".join(map(str, s)) for s in satirlar)
class JSONL(Disa, kod="jsonl"):
def yaz(self, satirlar): return "\n".join(str(list(s)) for s in satirlar)
print(sorted(Disa.turler))
print(Disa.turler["csv"]().yaz([(1, "İzmir"), (2, "Bursa")]))
Bir kez hesapla: functools.cached_property
@cached_property ilk erişimde metodu çalıştırır ve sonucu örnek sözlüğüne yazar; sonraki erişimler sözlükten gelir. Pahalı ve değişmeyen türetilmiş değerler (rapor özeti, ayrıştırılmış yapılandırma) için idealdir. Nesnenin durumu değişebiliyorsa önbellek bayatlar: ya @property kullan ya da değişiklikte del self.ozellik ile önbelleği sil. __slots__ kullanan sınıfta __dict__ olmadığı için çalışmaz.
Dunder disiplini: kısa kontrol listesi
- __eq__ yazdıysan __hash__'ı düşün. Değer nesnesi değişmezse
hash((alanlar)), değişebiliyorsa__hash__ = None(sözlük anahtarı olmasın). - Tanımadığın türe NotImplemented. Karşılaştırma ve aritmetikte hata fırlatma; Python karşı tarafa sorsun.
- Sözleşmeyi bütün kur.
__lt__yazdıysantotal_orderingekle;__len__yazdıysan__bool__davranışının uzunluktan türeyeceğini unutma. - Sürpriz yapma.
+operatörü yan etki üretmemeli, girdiyi değiştirmemeli; okuyan kişi aritmetik bekler. - Önce dataclass'a bak.
@dataclass(frozen=True, order=True)eşitlik, hash, sıralama ve repr'i tek satırda üretir; elle dunder yalnızca özel kural gerektiğinde.
len(x), x + y, with x) dunder metodu sınıfta arar, örnekte değil. nesne.__len__ = lambda: 5 yazmak len(nesne)'yi değiştirmez.__repr__ (loglarda <Siparis object at 0x7f...> yerine Siparis(no=1042, tutar=Para(...))), doğru __eq__/__hash__ ve para birimi karışınca hata veren aritmetik. Kod incelemesinde (code review) "bu nesne logda nasıl görünecek?" sorusunu sor.Kurumsal kodda dunder disiplini
İş hayatında sık görülen hatalar: __eq__ yazıp __hash__ unutmak (nesne kümeye girince TypeError), değişebilir (mutable) nesneye __hash__ vermek (sözlükte kaybolan anahtarlar), __repr__ yazmamak (log dosyasında <Siparis object at 0x...>). Pratik kural: değer nesneleri için @dataclass(frozen=True) kullan, eşitlik ve hash hazır gelir; davranış sınıflarında yalnızca gerçekten gereken dunder'ları yaz.
ABC mi Protocol mü sorusunda ekip kuralı genelde şudur: kendi yazdığın eklenti sistemi ve paylaşılan somut metotlar varsa ABC; fonksiyon parametresi için "şu metoda sahip olsun" diyorsan Protocol. İkisini birlikte de kullanabilirsin: ABC gerçekleştirmeyi zorunlu kılar, Protocol test çiftlerinin (test double) kalıtım olmadan geçmesini sağlar.
Descriptor yazarken __set_name__ kullan; alan adını elle tekrar etmek hata kaynağıdır. Performans kritik sınıflarda __slots__ ile dataclass'ın slots=True seçeneğini birleştir. Metaclass'ı yalnızca bir çerçeve (framework) yazıyorsan düşün; uygulama kodunda görürsen önce __init_subclass__ ya da sınıf dekoratörü ile çözülüp çözülemeyeceğini sor.