Kalıtım ve metot ezme
Kalıtım (inheritance), bir sınıfın başka bir sınıfın niteliklerini ve metotlarını yeniden yazmadan devralmasıdır. Devralan sınıfa alt sınıf, devredilen sınıfa üst sınıf denir. Alt sınıf, devraldığı metotlardan gerekenleri kendine göre değiştirebilir. Kalıtım yanlış yerde kullanılırsa sınıflar birbirine sıkı bağlanır ve üst sınıftaki küçük bir değişiklik alt sınıfları bozabilir. Bu hafta kalıtımı geçen hafta gördüğünüz kompozisyonla da karşılaştıracağız.
Bu sayfadaki kod hücreleri kendiliğinden çalışmaz. Her hücrede önce çıktıyı tahmin edin, sonra Run Code düğmesine basın. Kodu değiştirip yeniden çalıştırabilir, Start Over ile ilk hâline döndürebilirsiniz.
Bu bölümün kapsamı
- Alt sınıfın üst sınıftan davranış devralması ve tür ilişkisi
- Bir metodu override ederek davranışı özelleştirme
super()çağrısının normal metotta da kullanılması- Kalıtım kararını yerine geçme sorusuyla sınama
is-ailehas-aayrımı, ortak alanın kalıtım gerekçesi olmaması- Derin kalıtım zincirinin kırılganlığı
- Override edilen ve devralınan metotların çağrıda hangi sınıftan geldiğini izleme
- Çoklu kalıtım ve MRO terimleri
- Çoklu kalıtım tasarımı ve mixin kalıpları
- MRO algoritmasının (C3) ayrıntısı
- Soyut temel sınıf (ABC) yazımı
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Personel programındaki beş işi C’de nasıl yapardınız?
Bir şirketin personel programını yazıyorsunuz. Program her çalışanın adını tutuyor ve çalışanı tek satırla tanıtıyor: Çalışan: Ada. Şirkette yazılımcılar ve yöneticiler de var. Program şu beş işi yapacak:
- Yazılımcının adını tutmak ve onu tanıtmak, ama çalışan için yazılmış kodu kopyalamamak
- Yöneticiyi başka bir unvanla tanıtmak:
Yönetici: Can - Yazılımcının programlama dilini de tutmak ve onu diliyle tanıtmak:
Çalışan: Ada (Python) - Bütün personeli, yazılımcısıyla ve yöneticisiyle birlikte, tek bir fonksiyonla tanıtmak
- Şirket arabalarını da kaydetmek: her arabanın bir adı var ve onu bir çalışan kullanıyor
Programlama Temelleri’nde C ile yazdığınız struct’ları ve fonksiyonları düşünün. Bu işlerin her birini C’de nasıl yapardınız? Okumaya devam etmeden önce her iş için bir satır yazın. Bu haftanın Python konusunu bilmeniz gerekmiyor: “struct’ı kopyalayıp yeni bir alan eklerim” de bir cevap.
Bölümde bu beş işin Python’da nasıl yazıldığını göreceğiz. Bölümün sonunda listenize döneceğiz.
Alt sınıf, üst sınıfın metotlarını devralır
Birinci iş: yazılımcı, çalışanın kodunu kopyalamadan adını tutsun ve kendini tanıtsın. Python’da class Developer(Employee): yazdığınızda Developer sınıfını Employee sınıfından türetirsiniz: parantez içindeki Employee üst sınıf, Developer alt sınıf olur. Hücredeki isinstance(nesne, Sınıf) fonksiyonu, nesnenin o sınıftan sayılıp sayılmadığını sorar ve True ya da False döndürür.
Developer sınıfının gövdesinde yalnız pass var. dev.describe() çalışır mı? İki isinstance() ne yazar?
Developer sınıfında ne __init__ var ne describe(). Python bir metodu önce nesnenin kendi sınıfında arar, orada bulamazsa üst sınıfa bakar. Alt sınıfın nesnesi üst sınıfın da örneği sayılır: her Developer aynı zamanda bir Employee’dir. Bu ilişkiye İngilizcede is-a (“bir … türüdür”) denir.
Override: davranışı özelleştirmek
İkinci iş: yönetici başka bir unvanla tanıtılsın. Aşağıdaki Manager alt sınıfı, üst sınıftaki describe() ile aynı adda bir metot yazıyor. Manager sınıfında __init__ yok. İki satır ne yazar? İkinci satırdaki ad nereden geliyor?
Alt sınıftaki describe(), üst sınıftaki aynı adlı metodu ezer (override): bir Manager nesnesinde describe() çağrılınca alt sınıftaki sürüm çalışır. Manager sınıfında yazılmayan __init__ ise yine üst sınıftan gelir ve name niteliğini kurar. Override ederken metodu çağıran kodun beklentisini bozmayın. Burada iki sürüm de parametre almıyor ve bir metin döndürüyor.
super() ile üst sınıfın metodunu çağırmak
Üçüncü iş: yazılımcı programlama dilini de tutsun. Bunun için Developer kendi __init__ metodunu yazmalı. Ama self.name = name satırını alt sınıfta yeniden yazmak, çalışanın kurulum kodunu kopyalamak olur. Alt sınıfın içinden üst sınıfın bir metodunu super() ile çağırırsınız.
Hücredeki Developer sınıfında self.name = name satırı yok. İki print() ne yazar?
super().__init__(name) üst sınıfın __init__ metodunu aynı nesne için çalıştırır ve name niteliğini kurar. language niteliğini ise alt sınıf kendisi kurar. Kendi __init__ metodunu yazan alt sınıf super().__init__() çağrısını unutursa üst sınıfın kurulumu hiç çalışmaz ve self.name oluşmaz.
super() normal metotlarda da çalışır. Yazılımcının tanıtımı, çalışanın tanıtımının sonuna dili eklesin. Üst sınıfın sonucunu alıp üstüne bir şey eklemeye davranışı genişletmek denir. Son satır ne yazar?
Alt sınıf, üst sınıfın describe() metodunu silip baştan yazmaz. Onu çağırır ve dönen metnin sonuna dili ekler. Çalışanın tanıtım biçimi değişirse yazılımcının tanıtımı da kendiliğinden değişir.
Yerine geçme: Liskov ilkesi
Dördüncü iş: bütün personeli tek fonksiyonla tanıtmak. introduce_all() fonksiyonu çalışanlar için yazıldı ve listedeki her kişinin describe() metodunu çağırıyor. Listede bir çalışan, bir yönetici ve bir yazılımcı var. Fonksiyon ne yazar?
Fonksiyon kimin hangi sınıftan olduğunu sormadı. Her nesnede describe() çağrıldı ve her nesne kendi sınıfındaki sürümü çalıştırdı. Bu güvenle yapılabiliyor, çünkü üç sınıfta da describe() parametre almıyor ve bir metin döndürüyor.
- haftada bir fonksiyonun sözleşmesini, yani ne alıp ne döndürdüğünü ve neyi değiştirdiğini anlatan tanımı görmüştünüz. Bir sınıfın metotlarının sözleşmesi de o sınıfı kullanan kodun güvendiği sözdür. “Bir
Developer, birEmployee’dir” cümlesi kalıtım kararını tek başına vermez. Şunu da sorun:
Bir
Employeebeklenen yerdeDeveloperkullanıldığında program, üst sınıfın sözleşmesi bozulmadan çalışıyor mu?
Yazılım tasarımında bu soru Liskov yerine geçme ilkesi (Liskov Substitution Principle, LSP) adıyla bilinir. İlkenin biçimsel tanımını ezberlemeyeceğiz. Alt sınıfın, üst sınıftan beklenen davranışı bozup bozmadığına bakacağız.
Bu soru bazen “bir … türüdür” cümlesinden farklı bir cevap verir. Kare matematikte bir dikdörtgendir. Aşağıda Square sınıfı Rectangle sınıfından türetiliyor ve kenarları eşit kalsın diye set_width() metodunu override ediyor. widen() fonksiyonu dikdörtgenler için yazıldı: genişliği 10 yapıp alanı döndürüyor. İki nesnenin de yüksekliği 5. İki satır ne yazar?
widen(), genişliği değiştirince yüksekliğin aynı kalacağına güveniyor. Rectangle sınıfının sözü de bu: set_width() yalnız genişliği değiştirir. Square kendi kuralını, yani kenarların eşit kalmasını korurken bu sözü bozuyor. “Kare bir dikdörtgendir” cümlesi doğru olsa da Square bu programda Rectangle sınıfının yerine geçemiyor.
İlk satır 50 yazar: dikdörtgenin genişliği 10, yüksekliği 5 olur. İkinci satır 100 yazar: Square.set_width() yüksekliği de 10 yapar. Yüksekliği 5 olan bir dikdörtgen bekleyen widen() için bu şaşırtıcı bir sonuçtur. Square sınıfı set_width() metodunu override etmeseydi bu kez kare 10’a 5 olur, kare olmaktan çıkardı.
is-a ve has-a
Beşinci iş: şirket arabaları. Arabanın da çalışan gibi bir adı var. name niteliğini yeniden yazmamak için Car sınıfını Employee sınıfından türetmek akla gelebilir. Ama “araba bir çalışandır” cümlesi anlamsız. Araba bir çalışana sahiptir: onu kullanan sürücüye. Geçen hafta bu ilişkinin adının has-a olduğunu ve kompozisyonla kurulduğunu gördünüz.
Hücredeki Car sınıfı Employee sınıfından türemiyor, sürücüyü bir nitelikte tutuyor. Üç satır ne yazar?
Car sınıfı tanıtma işini sürücüye bırakır, ama kendisi bir çalışan sayılmaz.
| İlişki | Cümle | Örnek | Araç |
|---|---|---|---|
| is-a | “X bir Y türüdür” | Developer bir Employee’dir |
kalıtım |
| has-a | “X’in bir Y’si vardır” | Car bir sürücüye (Employee) sahiptir |
kompozisyon |
İki sınıfta aynı adlı bir nitelik ya da metot bulunması, birini ötekinden türetmek için gerekçe olmaz. Car ve Employee sınıflarının ikisinde de name var, ama araba bir çalışan değildir.
Tanımanız yeterli: kırılgan hiyerarşi ve MRO
Üst ve alt sınıfların oluşturduğu ağaca sınıf hiyerarşisi denir. Kalıtım zinciri derinleştikçe, yani B sınıfı A’dan, C sınıfı B’den, D sınıfı C’den türetildikçe, A sınıfındaki küçük bir değişiklik bütün alt sınıflara yayılır. Bu derste iki üç seviyeden derin hiyerarşi tasarlamayacağız.
Python bir sınıfın birden fazla üst sınıftan türetilmesine de izin verir. Buna çoklu kalıtım denir:
class C(A, B):
passA ve B sınıflarında aynı adlı bir metot varsa Python önce hangisine bakar? Bu arama sırasına method resolution order (MRO) denir. Sırayı C.mro() ile görebilirsiniz. Çoklu kalıtım ve MRO alıştırmalarda ve sınavda sorulmaz. İnternette bu terimlerle karşılaştığınızda ne olduklarını bilmeniz yeter.
Bir tasarımda üst ve alt sınıf sayısı arttıkça tasarım iyileşmez. Kompozisyon çoğu durumda daha gevşek ve kolay değiştirilen bir bağ kurar. Car sürücüsünü bir nitelikte tutar. Sürücüyü değiştirmek için car.driver niteliğine başka bir çalışan atamanız yeter. Car sınıfına dokunmanız gerekmez.
Soruya dönelim: personel programı Python’da
Bölümün başında personel programının beş işini C’de nasıl yapacağınızı sormuştuk. Python’daki karşılıkları şöyle:
| İş | C’de | Bu haftaki Python yolu | Dikkat |
|---|---|---|---|
| Yazılımcıyı çalışanın kodunu kopyalamadan tanıtmak | struct Developer içine Employee alanlarını kopyalarsınız ya da bir struct Employee alanı koyarsınız |
class Developer(Employee): |
Alt sınıfın nesnesi üst sınıfın da örneğidir: isinstance(dev, Employee) True döndürür |
| Yöneticiyi başka bir unvanla tanıtmak | struct’a bir tür alanı ekler, switch ile doğru printf() satırını seçersiniz |
Manager içinde aynı adlı describe() metodu (override) |
Override, üst sınıftaki metodu kendiliğinden çağırmaz |
| Yazılımcıyı diliyle tanıtmak | Çalışanı yazdıran fonksiyonu çağırır, ardından dili yazdırırsınız | super().__init__(name) ve super().describe() |
Kendi __init__ metodunu yazan alt sınıf super().__init__() çağırmazsa self.name oluşmaz |
| Bütün personeli tek fonksiyonla tanıtmak | Döngüde tür alanına göre switch yazarsınız |
Döngüde person.describe() |
Alt sınıf üst sınıfın sözünü bozarsa (kare ve dikdörtgen) fonksiyon yanlış sonuç alır |
| Şirket arabalarını kaydetmek | struct Car içine sürücü için bir struct Employee * alanı koyarsınız |
self.driver = driver (kompozisyon) |
Ortak name niteliği, Car sınıfını Employee sınıfından türetmek için gerekçe olmaz |
Listenizi tabloyla karşılaştırın. Birinci iş için Employee alanlarını yeni bir struct’a kopyaladıysanız, çalışana yeni bir alan eklendiğinde iki yeri birden değiştirmeniz gerekirdi. Kalıtımda alan ve metot tek yerde durur. İkinci ve dördüncü iş için tür alanı ve switch yazdıysanız, Python’da bu dallanmayı hiç yazmadığımıza dikkat edin: hangi describe() metodunun çalışacağını nesnenin sınıfı seçti. Gelecek hafta bu fikre polimorfizm adıyla döneceğiz. Beşinci iş için arabaya sürücüyü gösteren bir alan koyduysanız, C’de de kompozisyon kurmuşsunuz.
Alıştırma: Uygun ilişkiyi seç
Aşağıdaki her çift için is-a, has-a ya da “ikisi de değil” cevaplarından birini seçin:
DogveAnimalOrderveCustomerLaptopveBatteryTeacherveCourseAdminUserveUser
is-a seçtiğiniz her çift için “Üst tür beklenen yerde alt tür güvenle kullanılabilir mi?” sorusunu da cevaplayın.
Alıştırma: Override tamamla
return self.width * self.heightBir alt sınıf area() metodunu yazmazsa, o sınıfın nesnesinde area() çağrılınca NotImplementedError yükselir. Böylece “bu metodu alt sınıf yazmalı” beklentisi kodda açıkça görünür. Yine de Shape soyut (nesnesi oluşturulamayan) bir sınıf olmaz: Shape() ile nesne oluşturulabilir. Nesne oluşturmayı engelleyen soyut temel sınıfı (ABC) gelecek hafta tanıyacaksınız. NotImplemented ile NotImplementedError arasındaki farkı 12. haftada göreceğiz.
Alıştırma: Normal metotta super() kullan
Bir Logger.log(message) metodu "LOG: ..." döndürsün. TimestampLogger sınıfı log() metodunu override etsin. Önce super().log(message) çağırsın, sonra sonuca " [zaman damgalı]" eklesin. Üst sınıfın metodunu kopyalamadan davranışı genişletin.
Sıra sizde: Ödeme türleri
Bir Payment üst sınıfı ile CardPayment ve CashPayment alt sınıflarını tasarlayın. İkisinde de pay(amount) metodu olsun. En az bir alt sınıfta super() ile üst sınıfın normal bir metodunu genişletin.
Çalışır kod görevi: İki alt sınıftan birer nesneyi aynı listeye koyun ve bir döngüde her birinin pay() metodunu çağırın. Her nesne için isinstance(nesne, Payment) sonucunu assert ile doğrulayın. Son olarak yanlış kurulmuş bir kalıtım örneği yazın ve yerine geçme ilkesini (LSP) neden bozduğunu iki cümleyle açıklayın.
Tek sayfa özet
- Kalıtımda alt sınıf, üst sınıfın niteliklerini ve metotlarını yeniden yazmadan devralır. Bu bir
is-ailişkisidir. - Bir metot önce nesnenin kendi sınıfında aranır, orada yoksa üst sınıftan gelir.
isinstance()alt sınıfın nesnesini üst sınıfın örneği olarak da tanır.- Alt sınıf aynı adla bir metot yazarak üst sınıftaki metodu override eder. Üst sınıftaki metot bu durumda kendiliğinden çağrılmaz.
super()üst sınıfın metodunu çağırır. Kurucuda üst sınıfın kurulumunu, normal bir metotta üst sınıfın sonucunu kopyalamadan kullanırsınız.- “Bir … türüdür” cümlesi kalıtım kararını tek başına vermez. Üst sınıfın beklendiği yerde alt sınıf, sözleşmeyi bozmadan çalışmalıdır (Liskov yerine geçme ilkesi).
SquareveRectanglebu sözün bozulduğu bir örnektir. - Ortak bir nitelik kalıtım gerekçesi olmaz. “Sahiptir” cümlesi daha doğru duruyorsa kompozisyon seçin.
- Derin kalıtım zincirinde üst sınıftaki küçük bir değişiklik bütün alt sınıflara yayılır.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Alt sınıfın üst sınıftan davranış devraldığını ve üst sınıfın bir alt türü sayıldığını gösterir.
- Kalıtım kararını
is-asezgisiyle değil, yerine geçme sorusuyla sınar. - Bir metodu override ederek davranışı özelleştirir.
super()çağrısını yalnız kurucuda değil normal metotta da davranışı genişletmek için kullanır.is-ailehas-ailişkisini ayırır ve ortak bir niteliği kalıtım gerekçesi saymaz.- Derin kalıtım zincirinin kırılganlığını açıklar.
- Override edilen ve devralınan metotların çağrıda hangi sınıftan geldiğini kod izleyerek belirler.