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

Kalıtım (inheritance) ve polimorfizm

Ortak davranışı üst sınıfta toplar, super() ve metot ezmeyle özelleştirir, polimorfizmle farklı nesneleri aynı arayüzden kullanır; kompozisyon ve ABC ile tasarımı sağlamlaştırırsın.

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?

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____repr__ içinde sınıf adını sabit yazmak yerine 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))
Ördek tipleme (duck typing)Python'da polimorfizm için kalıtım şart değildir: 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)
SoruKalıtımKompozisyon
İlişkiAlt, Üst'ün bir türüdür (-dır)Nesne başka nesneyi içerir (-ı var)
BağlılıkSıkı: üst sınıf değişince alt etkilenirGevşek: parça değiştirilebilir
TestÜst sınıfla birlikte test edilirParça sahte (mock) ile değiştirilir
ÖrnekVadeli(Hesap)Musteri.hesaplar, Musteri.bildirim
Varsayılan tercihOrtak kod + gerçek -dır ilişkisiDiğ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ş

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)]))
Ne zaman şablon metot?Birden çok sınıfın adımları aynı sırada yürüyüp yalnız bazı adımların içeriği değişiyorsa (rapor üretme, dosya içe aktarma, ödeme akışı). Sırayı üst sınıf korur; böylece hiçbir alt sınıf "doğrulamayı atlayıp kaydetme" gibi bir hata yapamaz.

2. Ezmenin dört biçimi

BiçimKodNe zaman
Tamamen değiştirdef cek(self, t): ... (super yok)Davranış kökten farklı
Öne kural eklekontrol + super().cek(t)Ek kısıt (vade, limit)
Sonucu genişletreturn super().ozet() + " (VIP)"Üst sonucu süsle/zenginleştir
Sınıf niteliğini ezmasraf = 0.0 alt sınıf gövdesindeYalnı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 değil self.FAIZÜst sınıfın metodunda sabiti 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.

Gerçek işteDjango'nun sınıf tabanlı görünümleri (class-based views), unittest'in 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.
Uzmanlaş

Ç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.

Alıştırmalara geç (21)