Neden önemli?
Ilgaz Bank'ta üç hesap türü var: vadesiz, vadeli ve kredili. Üçü de para yatırma, bakiye gösterme ve işlem kaydı tutma işini aynı yapar; ama para çekme kuralı farklıdır (vadelide vade dolmadan çekilmez, kredilide eksiye inilebilir). Üç ayrı sınıf yazıp ortak kodu kopyalarsan bir hata düzeltmesini üç yerde yapmak zorunda kalırsın. Kalıtım (inheritance) ortak kısmı bir üst sınıfa koyar, alt sınıflar yalnızca farkı yazar. Polimorfizm (polymorphism) ise bu üç türü tek bir döngüde "hesap" olarak işlemeni sağlar: her nesne kendi cek metodunu bilir.
Üst sınıf ve alt sınıf
class Alt(Ust): yazımı Alt sınıfın Ust'ün tüm nitelik ve metotlarını miras almasını sağlar. Alt sınıf hiçbir şey eklemese bile çalışır.
class Hesap:
def __init__(self, no, sahip, bakiye=0.0):
self.no, self.sahip, self.bakiye = no, sahip, bakiye
def yatir(self, tutar):
self.bakiye += tutar
def cek(self, tutar):
if tutar > self.bakiye:
raise ValueError("yetersiz bakiye")
self.bakiye -= tutar
def __repr__(self):
return f"{type(self).__name__}({self.no!r}, bakiye={self.bakiye})"
class VadesizHesap(Hesap):
pass # her şeyi miras alır
v = VadesizHesap("TR01", "Ayşe", 500)
v.yatir(100)
print(v)
print(isinstance(v, VadesizHesap), isinstance(v, Hesap))
print(VadesizHesap.__mro__)
type(self).__name__ kullanırsan alt sınıflar kendi adlarıyla görünür. MRO (method resolution order) Python'ın bir metodu hangi sırayla aradığını gösterir.super() ve kurucuyu genişletme
Alt sınıf __init__ yazarsa üst sınıfınki otomatik çağrılmaz. super().__init__(...) ile üst sınıfın kurucusunu çağırıp ardından kendi niteliklerini eklersin.
class Hesap:
def __init__(self, no, sahip, bakiye=0.0):
self.no, self.sahip, self.bakiye = no, sahip, bakiye
def cek(self, tutar):
if tutar > self.bakiye:
raise ValueError("yetersiz bakiye")
self.bakiye -= tutar
def __repr__(self):
return f"{type(self).__name__}({self.no!r}, bakiye={self.bakiye})"
class Kredili(Hesap):
def __init__(self, no, sahip, bakiye=0.0, limit=1000.0):
super().__init__(no, sahip, bakiye) # ortak kısım üst sınıfta
self.limit = limit # ek nitelik
def cek(self, tutar): # metot ezme (override)
if tutar > self.bakiye + self.limit:
raise ValueError("limit aşıldı")
self.bakiye -= tutar
k = Kredili("TR02", "Mehmet", 200, limit=500)
k.cek(600)
print(k)
try:
k.cek(200)
except ValueError as e:
print("Hata:", e)
Ezerken üst sınıfı da çağırmak
Bazen davranışı tamamen değiştirmek değil, önüne bir kural eklemek istersin. Kuralı kontrol edip gerisini super() ile üst sınıfa bırakmak kod tekrarını önler.
from datetime import date
class Hesap:
def __init__(self, no, bakiye=0.0):
self.no, self.bakiye = no, bakiye
self.islemler = []
def cek(self, tutar):
if tutar > self.bakiye:
raise ValueError("yetersiz bakiye")
self.bakiye -= tutar
self.islemler.append(("çekme", tutar))
class Vadeli(Hesap):
def __init__(self, no, bakiye, vade_bitis):
super().__init__(no, bakiye)
self.vade_bitis = vade_bitis
def cek(self, tutar):
if date.today() < self.vade_bitis:
raise ValueError("vade dolmadan çekilemez")
super().cek(tutar) # kural geçtiyse normal çekme
v = Vadeli("TR03", 10000, date(2099, 1, 1))
try:
v.cek(100)
except ValueError as e:
print("Hata:", e)
v2 = Vadeli("TR04", 10000, date(2000, 1, 1))
v2.cek(100)
print(v2.bakiye, v2.islemler)
Polimorfizm: aynı çağrı, farklı davranış
Üç farklı hesap türünü tek listede tutup hepsine cek dersin; her nesne kendi sınıfının metodunu çalıştırır. Çağıran kod türleri bilmek zorunda değildir. Yeni bir hesap türü eklediğinde bu döngü değişmez; buna açık-kapalı ilkesi denir: genişlemeye açık, değişikliğe kapalı.
class Hesap:
def __init__(self, no, bakiye):
self.no, self.bakiye = no, bakiye
def aylik_masraf(self):
return 0.0
def __repr__(self):
return f"{type(self).__name__}({self.no})"
class Vadesiz(Hesap):
def aylik_masraf(self):
return 15.0
class Kredili(Hesap):
def aylik_masraf(self):
return 25.0 if self.bakiye < 0 else 0.0
class Vadeli(Hesap):
pass # masraf yok, üst sınıftan 0.0 gelir
hesaplar = [Vadesiz("TR01", 500), Kredili("TR02", -300), Vadeli("TR03", 10000), Kredili("TR04", 50)]
for h in hesaplar:
print(h, h.aylik_masraf())
print("Toplam masraf:", sum(h.aylik_masraf() for h in hesaplar))
aylik_masraf() metodu olan herhangi bir nesne bu döngüye girebilir. "Ördek gibi yürüyüp ördek gibi ses çıkarıyorsa ördektir." Kalıtım ortak kodu paylaşmak için, ördek tipleme esneklik için kullanılır.isinstance: ne zaman, ne zaman değil
isinstance(nesne, Sinif) alt sınıfları da kapsar; type(nesne) is Sinif kapsamaz, bu yüzden neredeyse hep isinstance tercih edilir. Ama kodunun içinde if isinstance(h, Vadeli): ... elif isinstance(h, Kredili): ... zinciri görüyorsan bu bir koku: o dallanma aslında her sınıfın kendi metodunda olmalı (polimorfizm).
class Hesap: pass
class Vadeli(Hesap): pass
v = Vadeli()
print(isinstance(v, Hesap), type(v) is Hesap)
print(isinstance(v, (Vadeli, int))) # demet: herhangi biri
print(issubclass(Vadeli, Hesap), issubclass(Hesap, Vadeli))
# Meşru kullanım: dış girdiyi doğrulama
def tutar_dogrula(x):
if not isinstance(x, (int, float)) or isinstance(x, bool):
raise TypeError(f"sayı bekleniyordu: {x!r}")
return float(x)
print(tutar_dogrula(42))
try:
tutar_dogrula("42")
except TypeError as e:
print(e)
Kompozisyon mu kalıtım mı?
Kalıtım "-dır" ilişkisidir: Vadeli hesap bir Hesaptır. Kompozisyon "-ı var" ilişkisidir: Müşterinin hesapları vardır; Müşteri Hesap'tan türemez, hesapları nitelik olarak tutar. Kalıtımı yalnızca gerçek -dır ilişkisinde ve ortak kod paylaşımı için kullan; "birkaç metodunu kullanmak istiyorum" diye kalıtım alma.
class Hesap:
def __init__(self, no, bakiye):
self.no, self.bakiye = no, bakiye
class Bildirim:
def gonder(self, kime, mesaj):
print(f"[SMS {kime}] {mesaj}")
class Musteri:
def __init__(self, ad, telefon, bildirim):
self.ad = ad
self.telefon = telefon
self.hesaplar = [] # kompozisyon: hesapları var
self.bildirim = bildirim # kompozisyon: bildirim kanalı var
def hesap_ac(self, no, bakiye=0.0):
h = Hesap(no, bakiye)
self.hesaplar.append(h)
self.bildirim.gonder(self.telefon, f"{no} hesabınız açıldı")
return h
@property
def toplam_varlik(self):
return sum(h.bakiye for h in self.hesaplar)
m = Musteri("Ayşe Kaya", "05xx", Bildirim())
m.hesap_ac("TR01", 1500)
m.hesap_ac("TR02", 250)
print(m.toplam_varlik)
| Soru | Kalıtım | Kompozisyon |
|---|---|---|
| İlişki | Alt, Üst'ün bir türüdür (-dır) | Nesne başka nesneyi içerir (-ı var) |
| Bağlılık | Sıkı: üst sınıf değişince alt etkilenir | Gevşek: parça değiştirilebilir |
| Test | Üst sınıfla birlikte test edilir | Parça sahte (mock) ile değiştirilir |
| Örnek | Vadeli(Hesap) | Musteri.hesaplar, Musteri.bildirim |
| Varsayılan tercih | Ortak kod + gerçek -dır ilişkisi | Diğer her durum |
ABC: soyut sınıfa giriş
Bazen üst sınıfın tek amacı "alt sınıfların şu metotları tanımlaması zorunludur" demektir. abc modülünden ABC ve @abstractmethod bunu zorlar: soyut metodu tanımlamayan alt sınıf örneklenemez.
from abc import ABC, abstractmethod
class OdemeYontemi(ABC):
@abstractmethod
def ode(self, tutar):
"""Ödemeyi yapar, işlem referansı döndürür."""
def makbuz(self, tutar): # somut metot, miras alınır
return f"{type(self).__name__}: {tutar:.2f} TL -> {self.ode(tutar)}"
class KrediKarti(OdemeYontemi):
def ode(self, tutar):
return "KK-" + str(int(tutar * 100))
class Havale(OdemeYontemi):
pass # ode tanımlanmadı
print(KrediKarti().makbuz(149.9))
try:
Havale()
except TypeError as e:
print("Hata:", e)
Derinleş: ezmenin incelikleri ve hiyerarşi tasarımı
Kalıtımın sözdizimini artık biliyorsun. Gerçek kodda asıl sorular şunlardır: Üst sınıftaki bir metot alt sınıfın metodunu nasıl çağırır? Bir davranışı tamamen mi değiştirmeli, yoksa genişletmeli miyim? dict ya da list'ten türetmek neden beklenmedik sonuçlar verir? Bu bölüm bu soruları yanıtlıyor. Çoklu kalıtım ve MRO'nun ayrıntısını "Çoklu kalıtım" dersinde, __init_subclass__ ile otomatik kayıt sistemlerini "Metaprogramlama" dersinde göreceksin.
1. Dinamik bağlama: self her zaman en alttaki sınıftır
Üst sınıftaki bir metot self.baska_metot() çağırdığında Python bu adı nesnenin gerçek sınıfından başlayarak arar. Yani üst sınıf, henüz yazılmamış alt sınıfların metotlarını çağırabilir. Buna dinamik bağlama (dynamic dispatch, late binding) denir ve şablon metot (template method) kalıbının temelidir: üst sınıf algoritmanın iskeletini sabitler, alt sınıflar yalnızca "kanca" (hook) metotları doldurur.
class Rapor:
def olustur(self, satirlar): # iskelet: sıra sabit
parcalar = [self.baslik()]
parcalar += [self.satir(s) for s in satirlar]
parcalar.append(self.altbilgi(len(satirlar)))
return "\n".join(parcalar)
def baslik(self):
return "RAPOR"
def satir(self, s):
return str(s)
def altbilgi(self, n):
return f"{n} kayıt"
class SubeRaporu(Rapor): # yalnız farkı yaz
def baslik(self):
return "== Şube Cirosu =="
def satir(self, s):
return f"{s[0]:<8}{s[1]:>8,} TL"
print(SubeRaporu().olustur([("Ankara", 125000), ("İzmir", 98500)]))
2. Ezmenin dört biçimi
| Biçim | Kod | Ne zaman |
|---|---|---|
| Tamamen değiştir | def cek(self, t): ... (super yok) | Davranış kökten farklı |
| Öne kural ekle | kontrol + super().cek(t) | Ek kısıt (vade, limit) |
| Sonucu genişlet | return super().ozet() + " (VIP)" | Üst sonucu süsle/zenginleştir |
| Sınıf niteliğini ez | masraf = 0.0 alt sınıf gövdesinde | Yalnız bir sabit farklı |
Son satır çok güçlüdür ve çoğu zaman metot ezmekten daha temizdir: üst sınıfın metodu self.ORAN okursa alt sınıf yalnız sabiti değiştirerek davranışı uyarlar.
class Hesap:
FAIZ = 0.30
ETIKET = "Standart"
def __init__(self, no, bakiye):
self.no, self.bakiye = no, bakiye
def yillik_faiz(self):
return round(self.bakiye * self.FAIZ, 2) # self.FAIZ: alt sınıfınki bulunur
def ozet(self):
return f"{self.no} [{self.ETIKET}] faiz={self.yillik_faiz()}"
class Vip(Hesap):
FAIZ = 0.40
ETIKET = "VIP"
def ozet(self): # sonucu genişlet
return super().ozet() + " + ücretsiz EFT"
for h in [Hesap("TR01", 10000), Vip("TR02", 10000)]:
print(h.ozet())
print(Hesap.FAIZ, Vip.FAIZ)
Hesap.FAIZ diye okursan alt sınıfın ezdiği değer hiç kullanılmaz. Ezilebilir olmasını istediğin her sabiti self. (ya da type(self).) üzerinden oku.3. Yerleşik türlerden türetmek: dict ve list tuzağı
class Ayarlar(dict) yazmak cazip gelir, ama yerleşik türler C ile yazılmıştır ve kendi iç metotları senin ezdiğin metodu çağırmaz. Örneğin __setitem__'ı ezsen bile kurucu (dict(...)), update ve setdefault onu atlar. Çözüm collections.UserDict / UserList'tir: saf Python ile yazıldıkları için tüm yollar senin metodundan geçer.
from collections import UserDict
class AnahtarBuyuk(dict):
def __setitem__(self, k, v):
super().__setitem__(k.upper(), v)
d = AnahtarBuyuk(ankara=1) # kurucu __setitem__'ı ÇAĞIRMAZ
d["izmir"] = 2 # bu çağırır
d.update(bursa=3) # bu da çağırmaz
print(d)
class AnahtarBuyuk2(UserDict):
def __setitem__(self, k, v):
super().__setitem__(k.upper(), v)
u = AnahtarBuyuk2(ankara=1)
u["izmir"] = 2
u.update(bursa=3)
print(u, isinstance(u, dict))
print(u.data) # gerçek sözlük .data içinde
UserDict bir dict alt sınıfı değildir (isinstance(u, dict) False); json.dumps gibi gerçek sözlük isteyen yerlere u.data ya da dict(u) verirsin. Çoğu durumda en iyisi ise türetmek değil, sözlüğü nitelik olarak tutmaktır (kompozisyon).
4. NotImplementedError mı, ABC mi?
Eski kodlarda soyut metot şöyle yazılır: def ode(self): raise NotImplementedError. Fark, hatanın ne zaman ortaya çıktığıdır: NotImplementedError ancak metot çağrıldığında patlar (belki üretimde, gece 3'te); ABC + @abstractmethod ise eksik alt sınıfı daha nesne oluşturulurken TypeError ile durdurur. Soyut property de yazabilirsin: @property ve @abstractmethod birlikte, @property en üstte.
from abc import ABC, abstractmethod
class Eski:
def ode(self, tutar):
raise NotImplementedError("alt sınıf yazmalı")
class EskiHavale(Eski):
pass
h = EskiHavale() # sorun yok gibi...
try:
h.ode(100) # ...ta ki çağrılana kadar
except NotImplementedError as e:
print("Geç fark edildi:", e)
class Odeme(ABC):
@property
@abstractmethod
def komisyon(self): ...
def net(self, tutar):
return round(tutar - tutar * self.komisyon, 2)
class Kart(Odeme):
komisyon = 0.02 # sınıf niteliği de soyut property'yi karşılar
class Eksik(Odeme):
pass
print(Kart().net(1000))
try:
Eksik()
except TypeError as e:
print("Erken fark edildi:", type(e).__name__)
5. Liskov: alt sınıf sözü bozmasın
Liskov yerine koyma ilkesi (LSP) pratikte şu kontrol listesidir. Alt sınıfta bir metodu ezerken: aynı parametreleri kabul et (daha azını değil), aynı türde değer döndür, üst sınıfın fırlatmadığı yeni hata türleri fırlatma, üst sınıfın garanti ettiği değişmezleri (bakiye tutarlılığı gibi) koru. Klasik ihlal "Kare bir Dikdörtgendir" örneğidir: dikdörtgende genişliği değiştirmek yüksekliği etkilemez, karede etkiler; dikdörtgen bekleyen kod kareyle bozulur. Tersine bakarsan: alt sınıfta bir metodu raise NotImplementedError("bu türde desteklenmez") ile "kapatıyorsan" o sınıf aslında o üst sınıfın türü değildir.
TestCase'i ve scikit-learn'ün tahminleyicileri (fit/predict) şablon metot ve sınıf niteliği ezme üzerine kuruludur: sen yalnız template_name = "..." ya da setUp yazarsın, iskeleti çerçeve çalıştırır. Bir kütüphanenin sınıfından türetmeden önce belgesinde "hangi metotları ezmen bekleniyor" kısmını oku; dokümante edilmemiş iç metotları ezmek sürüm yükseltmede kırılır.Çoklu kalıtım ve mixin
Python birden çok üst sınıfa izin verir: class Rapor(JsonMixin, CsvMixin, Temel). Mixin, tek bir yetenek (JSON'a çevirme gibi) ekleyen, kendi başına örneklenmeyen küçük sınıftır. MRO soldan sağa, derinlemesine ama tutarlı bir sırayla arar (C3 doğrusallaştırma). Çoklu kalıtımda super() "üst sınıf" değil "MRO'daki sonraki" demektir; bu yüzden mixin'lerde super().__init__(**kwargs) zincirini bozmamak gerekir.
Liskov yerine koyma ilkesi
Alt sınıf, üst sınıfın kullanıldığı her yerde sorunsuz kullanılabilmelidir. Vadeli hesabın cek metodu hata yükseltiyorsa bu, Hesap'ın da hata yükseltebildiği bir sözleşmeyle tutarlıdır. Ama alt sınıfta cek'in imzasını değiştirmek ya da üst sınıfın garanti ettiği bir davranışı bozmak kalıtımın yanlış kullanıldığını gösterir; o zaman kompozisyona dön.
Kalıtım derinliği
İki-üç seviye derin hiyerarşiler okunabilir sınırdır. Beş seviye derin bir "Hesap → Mevduat → Vadeli → DövizVadeli → AltınVadeli" zinciri, bir metodun nerede ezildiğini bulmayı işkenceye çevirir. Derinleşiyorsa stratejiyi (vade kuralı, masraf kuralı) ayrı küçük nesnelere taşı ve kompozisyonla birleştir; "Tasarım kalıpları" dersinde bunun adı Strategy olacak.