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

Çoklu kalıtım, MRO ve mixin'ler

Birden fazla üst sınıftan kalıtımı, elmas problemini, C3 ile MRO hesaplamayı, super() zincirini ve **kwargs ile işbirlikçi kurucuları, JSON/log/karşılaştırma mixin'leri tasarlamayı ve duck typing ile kompozisyonu ne zaman seçeceğini öğrenirsin.

Uygulamada aç, 18 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?

Kalıtım ve polimorfizm dersinde tek bir üst sınıftan türetmeyi, super() ile kurucuyu genişletmeyi ve mixin fikrine kısa bir girişi gördün. Gerçek kod tabanlarında ise sınıfların çoğu birden fazla kaynaktan yetenek alır: Anadolu Market'in Siparis sınıfı hem bir Kayit'tır, hem JSON'a çevrilebilir, hem işlemlerini loglar, hem de tutarına göre sıralanabilir. Django'nun sınıf tabanlı görünümleri, scikit-learn'ün tahmin edicileri, socketserver.ThreadingMixIn hep bu kalıpla yazılmıştır.

Çoklu kalıtım (multiple inheritance) güçlüdür ama bir soruyu doğurur: iki üst sınıfta aynı adlı metot varsa hangisi çalışır? Python bu soruyu MRO (Method Resolution Order, metot çözümleme sırası) ile kesin olarak yanıtlar. Bu dersin sonunda bir sınıfın MRO'sunu kâğıt üzerinde hesaplayabilecek, super()'nün aslında "üst sınıf" değil "MRO'da sıradaki sınıf" demek olduğunu bilecek ve temiz mixin'ler tasarlayabileceksin. Bunlar PCAP 4.5 konusunun tamamıdır.

Tekli ve çoklu kalıtım sözdizimi

Parantez içine birden fazla sınıf yazarsın. Arama soldan sağa yapılır: önce sınıfın kendisi, sonra ilk yazılan üst sınıf ve onun ataları (ortak ata hariç), sonra ikinci üst sınıf...

class Kargolanabilir:
    def teslimat(self):
        return "Kargo ile 2 gün"
    def etiket(self):
        return "KARGO"

class Indirilebilir:
    def teslimat(self):
        return "Anında indirme"
    def boyut(self):
        return "120 MB"

class EKitapPaketi(Indirilebilir, Kargolanabilir):   # basılı kitap + PDF
    pass

p = EKitapPaketi()
print(p.teslimat())            # soldaki kazanır
print(p.etiket(), p.boyut())   # ikisinin yetenekleri de var
print([k.__name__ for k in EKitapPaketi.__mro__])

__mro__ sınıfın arama sırasını demet olarak tutar; EKitapPaketi.mro() aynı bilgiyi liste olarak verir. Her sınıfın zinciri object ile biter.

Elmas problemi

İki üst sınıf ortak bir atadan türüyorsa kalıtım grafiği elmas biçimini alır. Eski dillerde ortak atanın metodu iki kez mi çalışacak, hangi yoldan mı gelinecek belirsizdi. Python'da ortak ata MRO'da bir kez ve tüm alt sınıflarından sonra yer alır.

class Hesap:
    def aciklama(self):
        return "Hesap"

class Vadeli(Hesap):
    def aciklama(self):
        return "Vadeli"

class Dovizli(Hesap):
    def aciklama(self):
        return "Dövizli"

class DovizliVadeli(Vadeli, Dovizli):
    pass

print([k.__name__ for k in DovizliVadeli.__mro__])
print(DovizliVadeli().aciklama())
print(Hesap in DovizliVadeli.__mro__, DovizliVadeli.__mro__.count(Hesap))

C3 doğrusallaştırma: MRO nasıl hesaplanır?

Python 2.3'ten beri MRO, C3 doğrusallaştırma (C3 linearization) algoritmasıyla bulunur. Üç kuralı garanti eder:

  1. Çocuk önce: bir sınıf her zaman üst sınıflarından önce gelir.
  2. Yerel sıra korunur: class D(B, C) yazdıysan B, C'den önce gelir.
  3. Tutarlılık (monotonluk): üst sınıfların kendi MRO'larındaki sıra bozulmaz.

Hesap şöyle yapılır: L[D] = D + birleştir(L[B], L[C], [B, C]). Birleştirme adımında listelerin başlarına bakarsın; başka hiçbir listenin kuyruğunda (ilk eleman dışındaki kısmında) geçmeyen ilk baş adayı seçilir, tüm listelerden çıkarılır, tekrar edilir. Aday kalmazsa sıralama imkânsızdır ve Python sınıfı oluşturmayı reddeder:

class A: pass
class B(A): pass

try:
    class C(A, B):      # A, B'den önce olsun diyorsun ama B, A'nın çocuğu!
        pass
except TypeError as e:
    print("TypeError:", e)

class C(B, A):          # tutarlı sıra
    pass
print([k.__name__ for k in C.mro()])
TanımMRONot
class B(A)B, A, objectTekli kalıtım: düz zincir
class D(B, C); B(A), C(A)D, B, C, A, objectElmas: A en sonda, bir kez
class E(C, B); B(A), C(A)E, C, B, A, objectYerel sıra değişti, MRO da değişti
class X(A, B); B(A)TypeErrorÇocuk-önce kuralıyla çelişir

super() üst sınıfı değil, MRO'da sıradakini çağırır

Bu cümle çoklu kalıtımın anahtarıdır. super().metot(), nesnenin MRO'sunda, içinde bulunduğun sınıftan sonra gelen sınıfın metodunu çağırır. Aynı Vadeli sınıfındaki super(), tek başına kullanıldığında Hesap'a, DovizliVadeli nesnesinde ise Dovizli'ye gider.

class Hesap:
    def kaydet(self):
        print("Hesap: veritabanına yazıldı")

class Vadeli(Hesap):
    def kaydet(self):
        print("Vadeli: vade tablosu güncellendi")
        super().kaydet()

class Dovizli(Hesap):
    def kaydet(self):
        print("Dövizli: kur kaydı eklendi")
        super().kaydet()

class DovizliVadeli(Vadeli, Dovizli):
    def kaydet(self):
        print("DövizliVadeli: başlıyor")
        super().kaydet()

DovizliVadeli().kaydet()
print("---")
Vadeli().kaydet()

Hesap'ın kaydet'i bir kez çalıştı ve her sınıf yalnız kendi işini yaptı. Bu düzene işbirlikçi çoklu kalıtım (cooperative multiple inheritance) denir. Şartı basittir: zincirdeki her sınıf super() çağırır ve kimse üst sınıfı adıyla (Hesap.kaydet(self)) çağırmaz. Adıyla çağırırsan zincir kopar ya da ortak ata iki kez çalışır.

Kurucularda **kwargs ile işbirliği

Farklı sınıfların kurucuları farklı parametreler ister. Çözüm: her sınıf kendi parametresini adıyla alır, kalanını **kwargs ile super().__init__'e iletir. Zincirin sonunda object'e boş kwargs ulaşmalıdır.

class Kayit:
    def __init__(self, *, kimlik, **kwargs):
        super().__init__(**kwargs)
        self.kimlik = kimlik

class ZamanDamgasi:
    def __init__(self, *, tarih="2026-01-15", **kwargs):
        super().__init__(**kwargs)
        self.tarih = tarih

class Sahipli:
    def __init__(self, *, sahip, **kwargs):
        super().__init__(**kwargs)
        self.sahip = sahip

class Fatura(ZamanDamgasi, Sahipli, Kayit):
    def __init__(self, *, tutar, **kwargs):
        super().__init__(**kwargs)
        self.tutar = tutar

f = Fatura(kimlik="F-1001", sahip="Anadolu Market", tutar=1250.0)
print(vars(f))
print([k.__name__ for k in Fatura.__mro__])
Yalnız-anahtar parametreParametre listesindeki *, sonrasındaki argümanların adla verilmesini zorunlu kılar. İşbirlikçi kurucularda konuma bağlı argüman sırası MRO'ya göre değişeceği için adla geçmek tek güvenli yoldur.

Mixin tasarımı

Mixin, tek başına örneklenmesi amaçlanmayan, tek bir yeteneği (JSON'a çevirme, loglama, karşılaştırma) başka sınıflara "karıştırmak" için yazılmış küçük bir sınıftır. İyi bir mixin:

import json
from functools import total_ordering

class JsonMixin:
    def to_json(self):
        return json.dumps(vars(self), ensure_ascii=False, sort_keys=True)

class LogMixin:
    def log(self, mesaj):
        self.__dict__.setdefault("_kayitlar", [])
        self._kayitlar.append(f"[{type(self).__name__}] {mesaj}")

@total_ordering
class KarsilastirmaMixin:
    # Ev sahibi _anahtar() tanımlamalı
    def __eq__(self, other):
        if not isinstance(other, KarsilastirmaMixin):
            return NotImplemented
        return self._anahtar() == other._anahtar()
    def __lt__(self, other):
        if not isinstance(other, KarsilastirmaMixin):
            return NotImplemented
        return self._anahtar() < other._anahtar()

class Urun(JsonMixin, KarsilastirmaMixin):
    def __init__(self, ad, fiyat):
        self.ad, self.fiyat = ad, fiyat
    def _anahtar(self):
        return self.fiyat
    def __repr__(self):
        return f"Urun({self.ad!r}, {self.fiyat})"

urunler = [Urun("Termos", 349.9), Urun("Kalem", 25.0), Urun("Çanta", 899.0)]
print(sorted(urunler))
print(urunler[0] > urunler[1], max(urunler).ad)
print(urunler[1].to_json())

functools.total_ordering, __eq__ ve __lt__'den diğer dört karşılaştırmayı türetir. to_json ise vars(self) ile nesnenin sözlüğünü kullanır; kapsülleme dersindeki iç gözlem araçlarının pratik bir uygulaması.

Metot ezme, __str__ ve __repr__

Alt sınıf bir metodu yeniden tanımlayınca (ezme, overriding) MRO'da önce o bulunur. __repr__'ı taban sınıfta bir kez, type(self).__name__ kullanarak yazarsan tüm alt sınıflar doğru adı kendiliğinden gösterir. __str__ son kullanıcıya, __repr__ geliştiriciye yöneliktir; __str__ yoksa print __repr__'a düşer.

Polimorfizm ve duck typing

Polimorfizmin Python'daki en yaygın biçimi duck typing'dir: "Ördek gibi yürüyor ve ötüyorsa ördektir." Bir fonksiyon nesnenin türünü değil, yapabildiklerini önemser. Ortak üst sınıf zorunlu değildir.

class KargoUcreti:
    def __init__(self, desi): self.desi = desi
    def ucret(self): return 30 + self.desi * 4.5

class Sigorta:
    def __init__(self, deger): self.deger = deger
    def ucret(self): return round(self.deger * 0.01, 2)

class HediyePaketi:
    def ucret(self): return 25

class Not:                      # ucret() yok
    metin = "Kapıya bırakın"

kalemler = [KargoUcreti(3), Sigorta(2000), HediyePaketi(), Not()]
toplam = 0
for k in kalemler:
    try:
        toplam += k.ucret()
    except AttributeError:
        print("Ücretsiz kalem atlandı:", type(k).__name__)
print("Toplam ek ücret:", toplam)

try/except AttributeError yaklaşımına EAFP ("izin isteme, af dile") denir ve Python'da hasattr ile önceden sormaktan (LBYL) genellikle daha deyimseldir.

Ne zaman kompozisyon?

Kalıtım "bir ... dır" (is-a), kompozisyon "bir ... sahiptir" (has-a) ilişkisidir. Çoklu kalıtım cazip gelse de şu durumlarda kompozisyonu seç:

DurumTercihNeden
Küçük, durumsuz yetenek (JSON, log, karşılaştırma)MixinKod tekrarı yok, MRO basit
Davranış çalışma anında değişecek (SMS mi e-posta mı)KompozisyonNesneyi değiştirmek sınıf değiştirmekten kolay
Parça kendi durumunu taşıyor (adres, sepet)KompozisyonYaşam döngüleri ayrı
Aynı yetenekten iki tane gerekiyor (iki adres)KompozisyonBir sınıftan iki kez kalıtım alınamaz
Çerçeve (Django, unittest) sınıf bekliyorKalıtım + mixinÇerçevenin sözleşmesi
Gerçek işteKurumsal Python projelerinde sık görülen bir kod inceleme (code review) yorumu: "Bu mixin neden kendi __init__'inde super() çağırmıyor?" İşbirliğini bozan tek bir sınıf, zincirin geri kalanını sessizce devre dışı bırakır ve hata ancak belirli bir sınıf bileşiminde ortaya çıkar. Ekipler bu yüzden mixin'leri durumsuz tutar, __mro__'yu testte doğrular ve kalıtım derinliğini sınırlar.
Uzmanlaş

super() perde arkasında

Argümansız super(), derleyicinin eklediği __class__ hücresi ve metodun ilk argümanı (self) ile super(SuAnkiSinif, self)'e dönüşür. Bu nesne type(self).__mro__'yu alır, SuAnkiSinif'ı bulur ve aramaya ondan sonraki sınıftan başlar. Bu yüzden aynı satır, farklı nesnelerde farklı sınıflara gidebilir. super(Vadeli, self).kaydet() biçimi hâlâ geçerlidir ama yalnız bir sınıfı bilerek atlamak istediğinde kullanılır.

Mixin ile ABC birlikte

Mixin'in ev sahibinden beklediği metodu belgelemek yetmezse abc.abstractmethod ile zorunlu kılabilirsin; ya da tip denetleyicileri için typing.Protocol tanımlarsın (İleri OOP dersinde). isinstance(x, JsonMixin) sorgusu çalışır ama duck typing ruhuna aykırıdır; yeteneği kullanmayı dene, yoksa hatayı yakala.

PCAP için hızlı kontrol listesi

  • issubclass(D, A) dolaylı atalar için de True'dur; D.__bases__ yalnız doğrudan üstleri gösterir.
  • Aynı adlı nitelik hem sınıf değişkeni hem metot olarak farklı üstlerde varsa MRO'da önce gelen kazanır; türü önemli değildir.
  • Tutarsız MRO, sınıf tanımlanırken TypeError verir; nesne oluşturulurken değil.
  • Tüm sınıflar örtük olarak object'ten türer; class A: ile class A(object): Python 3'te aynıdır.
Alıştırmalara geç (18)