Python sınıf tasarımı: kalıtım ve kompozisyon ne zaman seçilir?
Python'da kalıtım, MRO, super ve kompozisyonu kodla öğrenin; hangi durumda hangisini seçeceğinizi ve sık sınıf tasarım hatalarını görün.
Kalıtım mı kompozisyon mu: önce ilişkinin türünü sorun
Sınıf tasarımındaki ilk soru kod değil ilişkidir. A sınıfı gerçekten B'nin özel bir türü müdür ("bir türüdür"), yoksa B'yi kullanan bir parçaya mı sahiptir ("sahiptir")? Köpek, hayvanın bir türüdür; sipariş, fatura adresine sahiptir. İlk ilişki kalıtıma, sonraki kompozisyona uygundur.
Pratikte iş şöyle bozulur: sınıflar biraz kod paylaşıyor diye biri diğerinden türetilir. Paylaşılan şey kod ise ve ilişki "bir türüdür" değilse, kalıtım gereksiz bir bağımlılık yaratır. Alt sınıf, üst sınıfın iç ayrıntılarına bağlanır; üst sınıfta yapılan küçük bir değişiklik tüm alt sınıfları kırabilir. Kompozisyonda ise parça arkada bir nesne olarak durur ve yalnız açık arayüzü üzerinden kullanılır.
Python'da bu ayrım şu konulara iner: kalıtımın mekaniği (üst sınıf araması, super, çözüm sırası), kompozisyonla aynı işin nasıl kurulacağı ve hangi durumda hangisinin seçileceği.
Python'da kalıtım nasıl çalışır?
Türetilmiş sınıf, üst sınıfı parantez içinde belirtir. Bir nitelik ya da metot alt sınıfta bulunamazsa arama üst sınıfa geçer ve bu zincir yukarı doğru sürer. Alt sınıf bir metodu yeniden tanımlarsa (override) üst sınıfınki gölgelenir. Python belgesi bunu şöyle özetler: Python'da bütün metotlar fiilen sanaldır.
Üst sınıfın davranışını silmek yerine genişletmek için super() kullanılır:
class Hesap: def __init__(self, sahip): self.sahip = sahip self.hareketler = []
class VadeliHesap(Hesap): def __init__(self, sahip, vade): super().__init__(sahip) self.vade = vade
Bir nesnenin hangi türden olduğunu sorgulamak için isinstance ve issubclass kullanılır. isinstance(nesne, Hesap), nesnenin sınıfı Hesap ya da onun alt sınıflarından biriyse doğrudur. issubclass(VadeliHesap, Hesap) ise sınıflar arasındaki türeme ilişkisini sorgular. Belgedeki örnekte bool sınıfı int sınıfının alt sınıfıdır, float ise değildir.
Kalıtımın ilke olarak doğru çalışması için Liskov'un yerine koyma ilkesine uymak gerekir: alt sınıf nesnesi, üst sınıf nesnesinin beklendiği her yerde sürprizsiz çalışmalıdır. Alt sınıf üst sınıfın metodunu hata fırlatarak ya da anlamını değiştirerek ezerse, ilişki kalıtım için uygun değildir.
Çoklu kalıtım ve MRO: arama sırası nasıl belirlenir?
Python bir sınıfın birden çok üst sınıfı olmasına izin verir. Bu durumda bir metot çağrıldığında hangi sınıfın önce aranacağını belirleyen şey metot çözüm sırasıdır (MRO). Python bu sırayı C3 doğrusallaştırma kuralıyla hesaplar; sıra sınıfın kendisiyle başlar ve object ile biter.
class A: pass class B(A): pass class C(A): pass class D(B, C): pass
print(D.__mro__)
Çıktıda sıra D, B, C, A, object olarak görünür. Elmas biçimli bu yapıda A hem B hem C'nin atasıdır; C3 kuralı A'yı B ve C'nin ardına yerleştirir, böylece A bir kez ve en sonda aranır. super() çağrısı da bu sıraya göre çalışır: super, sınıfın kendi üst sınıfını değil, nesnenin türünün MRO'sundaki bir sonraki sınıfı bulur. Bu yüzden işbirlikçi çoklu kalıtımda B içindeki super().yontem() çağrısı A'ya değil, C'ye gidebilir.
Kurallar birbiriyle çelişirse Python sınıfı hiç oluşturmaz. Bir sınıf X'in Y'den önce gelmesini isterken diğeri tersini isterse ve bu sınıflardan birlikte türetirseniz TypeError alırsınız: tutarlı bir sıra kurulamıyor. Aynı sınıfı üst sınıf listesine tekrar yazmak da hatadır.
Pratik çıkarım: çoklu kalıtımı, küçük ve tek amaçlı yardımcı sınıflar (mixin) için kullanın, ağır durum taşıyan sınıfları birbirine kalıtımla bağlamayın. Karmaşık bir hiyerarşide MRO'yu ezberlemek yerine __mro__ ile bakın.
Aynı işi kompozisyonla kurmak
Bir bildirim servisi düşünün. Kalıtım yaklaşımında EpostaBildirim, SmsBildirim ve test için SahteBildirim sınıfları ortak bir üst sınıftan türer. Servis her yeni kanalda büyür, her sınıf üst sınıfın iç davranışına bağlı kalır. Kompozisyonda ise servis kanalı dışarıdan alır:
class BildirimServisi: def __init__(self, kanal): self.kanal = kanal
def uyar(self, kullanici, metin): self.kanal.gonder(kullanici.adres, metin)
class EpostaKanali: def gonder(self, adres, metin): ...
class TestKanali: def __init__(self): self.giden = []
def gonder(self, adres, metin): self.giden.append((adres, metin))
Servis, kanalın türünü sormaz; yalnız gonder metodu olup olmadığına bakar. Python bu yaklaşıma "ördek tipleme" der. Yeni bir kanal eklemek servisi değiştirmeyi gerektirmez, testte gerçek e-posta göndermeden TestKanali verip giden listesini denetlersiniz. Bağımlılığı dışarıdan vermek (dependency injection) kompozisyonun doğal sonucudur.
Beklentiyi açıkça belgelemek isterseniz typing modülündeki Protocol ile yapısal bir arayüz tanımlayabilirsiniz. Belgeye göre bir sınıf, protokolden türemese bile gereken metotlara sahipse protokolle uyumlu sayılır; böylece kalıtım zorunluluğu olmadan arayüz sözleşmesi kurarsınız.
Karar ölçütleri
Bir sınıfı kalıtımla mı kompozisyonla mı kuracağınıza karar verirken şu soruları sorun:
- İlişki "bir türüdür" diye doğal biçimde söylenebiliyor mu, yoksa yalnızca kod paylaşımı mı var?
- Alt sınıf, üst sınıfın tüm sözleşmesini bozmadan koruyor mu (yerine koyma ilkesi)?
- Davranış çalışma zamanında değişecek mi? Kompozisyonda parçayı değiştirmek kolaydır, kalıtımda sınıf sabittir.
- Test için parçayı sahteyle değiştirmek gerekiyor mu? Kompozisyon bunu kolaylaştırır.
- Hiyerarşi derinleşiyor mu? Alt sınıfın üst sınıfın üst sınıfına bağlanması, değişikliğin yayılma alanını büyütür.
- Üst sınıf sizin kontrolünüzde mi? Üçüncü taraf bir sınıftan türetmek, onun sürüm değişikliklerine bağlanmak demektir.
Varsayılan tercih kompozisyon olsun, kalıtım ise "bir türüdür" ilişkisi açık olduğunda ve üst sınıf kararlı bir sözleşme sunduğunda gelsin. Bu bir yasa değil, değişikliğe en az direnç gösteren yoldur. Küçük betiklerde tek bir sınıf ya da düz fonksiyonlar çoğu zaman her ikisinden de sade bir çözümdür.
Kalıtım mantıklı olduğu durumlar da vardır: çerçevelerin kendi taban sınıflarından türetmek (örneğin kendi hata sınıfınızı Exception'dan türetmek), soyut bir arayüzün birden çok gerçeklemesini aynı ağaçta tutmak ve yerleşik türleri özelleştirmek bunların başında gelir.
Sık hatalar ve düzeltme yolları
- Kalıtımı kod tekrarını önlemek için kullanmak. Düzeltme: ortak kodu bir yardımcı fonksiyona ya da küçük bir sınıfa çıkarın ve kompozisyonla kullanın.
- Alt sınıfta __init__ içinde super().__init__ çağrısını unutmak. Üst sınıfın durumu kurulmaz ve ilk nitelik erişiminde AttributeError alırsınız.
- Derin hiyerarşi. Düzeltme: hiyerarşiyi düzleştirin, ara sınıfları arayüze ya da fonksiyona dönüştürün.
- Çoklu kalıtımda aynı isimli metotlar: hangisinin çalıştığı MRO'ya bağlıdır. Düzeltme: __mro__ ile sırayı yazdırıp beklentiyle karşılaştırın, çakışma varsa birini kompozisyona çevirin.
- Alt sınıfın üst sınıf metodunu tamamen farklı anlamda ezmesi. Düzeltme: ilişki yanlış kurulmuştur, kalıtımı bırakın.
- Her şeyi sınıf yapmak. Durum taşımayan davranış için modül düzeyinde fonksiyon yeterlidir.
Mevcut bir hiyerarşiyi yeniden tasarlarken önce davranışı testlerle sabitleyin; ardından kalıtımı adım adım kompozisyona çevirin. Testler yeşil kaldığı sürece her adım güvenlidir.
Sınıf tasarımı pratiğini nerede çalışırsınız?
Mindset'in Python ile Nesne Yönelimli Programlama eğitiminde doğrudan bu konuya ayrılmış bir modül vardır: Kalıtım, Kompozisyon ve Çözüm Sırası. Eğitimin devamında veri modeli ve özel metotlar, veri sınıfları ve tipler, tasarım desenleri ve bağımlılık yönetimi ile metaclass, kalite ve refaktör modülleri gelir.
Bu içerik, mevcut kod tabanında hiyerarşi karmaşıklaşmış, test yazmakta zorlanan ya da yeni özellik eklerken eski sınıfları kıran Python ekipleri için uygundur. Bağımlılık yönetimi ve refaktör modülleri, kurumsal bir ekipte ortak tasarım ölçütleri oluşturmak isteyen takımlar için de somut bir zemin sağlar.
İlgili Mindset programı
Python ile Nesne Yönelimli Programlama
Sık sorulan sorular
super() tam olarak neyi çağırır?
super(), sınıfın tanımlandığı yerdeki doğrudan üst sınıfı değil, nesnenin türünün çözüm sırasında (MRO) bir sonraki sınıfı bulur. Tek kalıtımda bu fark görünmez, çünkü bir sonraki sınıf zaten üst sınıftır. Çoklu kalıtımda ise çağrı kardeş bir sınıfa gidebilir; bu yüzden işbirlikçi tasarımda her sınıf super() çağrısını sürdürmelidir.
Python'da mixin nedir, kalıtım sayılır mı?
Mixin, tek başına anlamlı bir nesne üretmeyen, yalnızca belirli bir yetenek ekleyen küçük sınıftır. Teknik olarak çoklu kalıtımla kullanıldığı için kalıtımdır; ama ilişki 'bir türüdür' yerine 'bu yeteneği taşır' anlamına gelir. Durum taşımayan, tek amaçlı ve diğer sınıfların iç ayrıntısına dokunmayan mixin sınıfları yönetilebilir kalır.
Kompozisyon her zaman kalıtımdan daha mı iyidir?
Hayır, varsayılan tercih olması her durumda üstün olduğu anlamına gelmez. Gerçekten 'bir türüdür' ilişkisi varsa ve üst sınıf kararlı bir sözleşme sunuyorsa kalıtım daha az kod ve daha açık bir tür hiyerarşisi sağlar. Sorun, kalıtımın kod paylaşmak için kullanıldığı yerde başlar.
Mevcut bir hiyerarşiyi kompozisyona nasıl çeviririm?
Önce mevcut davranışı testlerle sabitleyin. Sonra üst sınıfın işini yapan kısmı ayrı bir sınıfa çıkarın, alt sınıfa bu nesneyi nitelik olarak verin ve çağrıları ona yönlendirin. Testler geçtikten sonra türetmeyi kaldırın. Her adımı küçük tutarsanız arada hiçbir şey kırılmaz.
Tutarlı bir çözüm sırası kurulamıyor hatası neden çıkar?
Üst sınıf listesindeki sıra, atalarınızın kendi sıralarıyla çelişiyorsa C3 kuralı bir doğrusallaştırma üretemez ve Python TypeError verir. Örneğin bir ata X'i Y'den önce, diğeri Y'yi X'ten önce istiyorsa sorun çıkar. Çözüm, üst sınıf sırasını düzeltmek ya da çoklu kalıtımı kompozisyonla değiştirmektir.