Ders 02 / 11
Kalıtım ve Yöntem Çözümleme Sırası
Dört yeteneğin gövdesi kalıtımda üç ayrı sınıfa dağılır ve `Belge` bunların 0'ını kendi gövdesinde yazar, bileşimde `Sarmal` 4'ünü de yazar; gövdesi `Kayit`'te yazılı olan `bicimle` yöntemi `<Imzali>` basar.
İçindekiler
Önceki ders tek sınıflı bir ağaçta yanıtın hep aynı yerden geldiğini gösterdi: arama örneğin
sözlüğüne bakıyor, bulamazsa sınıfa gidiyor ve orada bitiyordu. Son ölçüm sınırı çoktan
çizmişti — gövdesi Bicimci’de yazılı bir sınıf yöntemi AltBicimci üzerinden çağrıldığında
cls’e bağlanan sınıf AltBicimci oldu. Gövdenin durduğu yer ile çağrının yanıtlandığı yer
ayrıldı.
Bu ders o ayrımı büyütür ve kursun ikinci iddiasını öder. Aynı dört yetenek iki ayrı tasarımla verilecek: biri kalıtım ağacı, öteki bileşim. İki tasarım da aynı sonuçları üretecek. Sorulan şey sonuç değil: her yeteneğin gövdesi hangi sınıfta yazılı? Soru ölçüye elverişlidir, çünkü yanıtı görüşe değil koda bakılarak verilir — bir sınıfın sözlüğünde bir ad ya vardır ya yoktur.
Aynı Dört Yetenek, İki Tasarım
Kalıtım tarafı dört sınıftan oluşur. Kayit iki yetenek taşır: kaynak ve onu çağıran
bicimle. Zamanli ondan türer ve damga ekler. Imzali da ondan türer, imza ekler ve
kaynak’ı yeniden yazar. Belge ikisinden birden türer ve gövdesi boştur.
Bileşim tarafı tek sınıftır. Sarmal hiçbir şeyden türemez; iki nesneyi kendi içinde tutar
ve dört yeteneğin dördünü de kendi gövdesinde yazıp çağrıyı içerideki nesneye iletir.
Bir yeteneğin gövdesinin nerede yazılı olduğunu yöntem çözümleme sırası (method resolution
order) söyler. Her sınıfın böyle bir sırası vardır: aramanın hangi sınıfları hangi düzende
gezeceğini veren, tanım anında bir kez hesaplanmış bir liste. saglayan işlevi bu listeyi
gezer ve adı ilk taşıyan sınıfı döndürür.
Belge’nin gövdesinin boş olması bir eksiklik değil, ölçümün kendisidir. O gövde tek bir şey
bildirir: hangi sınıflardan, hangi sırayla türendiği. Sınıfın bütün davranışı bu tek
bildirimden doğar ve bildirim değiştiğinde davranış da değişir — tabanların yeri değiştirilse
kaynak’ı yanıtlayan sınıf değişir, Belge’nin gövdesine tek satır eklense yanıt oraya
kayar. Bileşimde bu bildirim yoktur; Sarmal’ın davranışı yalnız kendi gövdesinden okunur.
Ölçümün varsayımları:
- SN16 — Dört sınıflı ağaç ve
Sarmalortak kurgunun tanımladığı biçimdedir; yeteneklerin yerleşimi değiştirilmemiştir. - SN17 — Ölçülen dört yetenek
kaynak,bicimle,damgaveimza’dır; ölçüm çağrının sonucuna değil, gövdenin yazıldığı sınıfa bakar. - SN18 — Bileşim sütunu sabit değildir,
Sarmal’ın kendi sözlüğünden okunmuş olsaydı da aynı çıkardı: dört adın dördü de o gövdede yazılıdır. - SN19 — Çözümleme sırası basılırken listenin son öğesi (bütün sınıfların ortak tabanı) kırpılır; ölçülen şey kurgunun kendi dört sınıfıdır.
"""Cagriyi kim yanitladi: kalitim ile bilesim yan yana.""" class Kayit: def kaynak(self): return "Kayit" def bicimle(self): return f"<{self.kaynak()}>" class Zamanli(Kayit): def damga(self): return "zaman" class Imzali(Kayit): def kaynak(self): return "Imzali" def imza(self): return "imza" class Belge(Zamanli, Imzali): pass class Sarmal: """Ayni yetenegi bilesimle veren nesne: yanit veren acikca yazilir.""" def __init__(self): self.zamanli = Zamanli() self.imzali = Imzali() def kaynak(self): return self.imzali.kaynak() def damga(self): return self.zamanli.damga() def imza(self): return self.imzali.imza() def bicimle(self): return f"<{self.kaynak()}>" def saglayan(sinif, ad): """Bir ozniteligi hangi sinif sagliyor: cozumleme sirasinda ilk bulan.""" for s in sinif.__mro__: if ad in s.__dict__: return s.__name__ return None YETENEKLER = ("kaynak", "bicimle", "damga", "imza") k = {ad: saglayan(Belge, ad) for ad in YETENEKLER} b = {ad: "Sarmal" for ad in YETENEKLER} print(f"{'yetenek':<10s} {'kalıtımda yanıtlayan':>21s} {'bileşimde yanıtlayan':>21s}") for ad in YETENEKLER: print(f"{ad:<10s} {k[ad]:>21s} {b[ad]:>21s}") print() print("çözümleme sırası: " + " -> ".join(s.__name__ for s in Belge.__mro__[:-1])) print(f"Belge().bicimle() -> {Belge().bicimle()} | Sarmal().bicimle() -> " f"{Sarmal().bicimle()}") print(f"kalıtımda gövdesi Belge'de yazılı yetenek: " f"{sum(1 for a in YETENEKLER if k[a] == 'Belge')}/{len(YETENEKLER)}; " f"bileşimde: {sum(1 for a in YETENEKLER if b[a] == 'Sarmal')}/{len(YETENEKLER)}")
yetenek kalıtımda yanıtlayan bileşimde yanıtlayan kaynak Imzali Sarmal bicimle Kayit Sarmal damga Zamanli Sarmal imza Imzali Sarmal çözümleme sırası: Belge -> Zamanli -> Imzali -> Kayit Belge().bicimle() -> <Imzali> | Sarmal().bicimle() -> <Imzali> kalıtımda gövdesi Belge'de yazılı yetenek: 0/4; bileşimde: 4/4
Son satır iddianın kendisidir. Belge dört yeteneğin 0’ını kendi gövdesinde taşır;
Sarmal 4’ünü de taşır. Belge’nin gövdesi okunduğunda görülen tek şey iki taban adıdır;
nesnenin ne yapabildiği o gövdede yazılı değildir. Sarmal’ın gövdesi okunduğunda ise
dört yetenek de, her birinin çağrıyı kime ilettiği de görünür.
Sol sütun yanıtın üç ayrı sınıfa dağıldığını gösteriyor: kaynak Imzali’den, bicimle
Kayit’ten, damga Zamanli’den geliyor. kaynak iki sınıfta birden yazılıdır — hem
Kayit, hem Imzali — ama çözümleme sırasında Imzali önce geldiği için yanıtı o veriyor.
Kayit’teki gövde hâlâ oradadır ve hiç çağrılmaz.
Asıl ince nokta ikinci satırdadır. bicimle gövdesi Kayit’te yazılıdır ve o gövdede
self.kaynak() yazar. Gövde Kayit’in içinde durduğu için oradaki kaynak’ı çağıracağı
düşünülebilir; çağırmıyor. Çağrı self üzerinden yapılır ve self bir Belge
örneğidir — arama Belge’nin sırasında yapılır ve Imzali’de durur. Sonuç <Imzali> çıkar.
Bir gövdeyi okumak, o gövdenin ne üreteceğini söylemez; onu, çağrı anındaki nesnenin
çözümleme sırası söyler. Sarmal aynı sonucu verir, ama orada iletimin nereye gittiği
gövdenin içinde yazılıdır.
Programlama Paradigmaları kursu bileşimin kalıtıma tercih edilmesini bir tasarım ilkesi olarak, kırılgan üst sınıf sorunuyla birlikte kurmuştu; bu tartışma tekrarlanmaz. Burada ölçülen şey ilkenin gerekçesi değil, ölçülebilir karşılığıdır: gövdenin nerede yazılı olduğu. 0/4 ile 4/4 aynı davranışın iki ayrı okunabilirlik bedelidir.
İki sayının yönü terstir ve seçim tam olarak burada yapılır. Kalıtımda beşinci bir yetenek
eklemenin bedeli 0 satırdır: Zamanli’ye yazılan bir yöntem Belge’de kendiliğinden
görünür. Bileşimde bedel 1 satırdır — Sarmal’a bir iletim yazmak gerekir. Buna karşılık
“bu çağrıyı kim yanıtlıyor” sorusunun bedeli kalıtımda dört sınıf gövdesini ve aralarındaki
sırayı okumak, bileşimde tek gövdeyi okumaktır. Kalıtım yazımı kısaltır ve okumayı uzatır;
bileşim yazımı uzatır ve okumayı kısaltır.
Ayrımın bir de nesne tarafı vardır ve iki tanımdan doğrudan okunur. Bir Belge örneği tek
nesnedir; dört sınıfın gövdeleri tek bir sözlük üzerinde çalışır ve Kayit’in tutacağı her
durum o tek sözlükte, bir kez bulunur. Bir Sarmal örneği ise üç nesnedir — kendisi ve
__init__ içinde var ettiği iki nesne — ve Kayit’ten gelen durum ikisinde ayrı ayrı
tutulur. İki tasarım aynı çağrılara aynı yanıtı verir ama durumu farklı sayıda yerde saklar:
kalıtımda paylaşım kaçınılmazdır, bileşimde paylaşım ancak açıkça yazılırsa olur.
Sıra Nasıl Kuruluyor
Belge -> Zamanli -> Imzali -> Kayit sırası keyfî değildir ve iki kuralla üretilir: her sınıf
kendi tabanlarından önce gelir, ve tabanlar sınıf gövdesinde yazıldıkları sırayla
korunur. Kayit en sonda durur, çünkü hem Zamanli’nin hem Imzali’nin tabanıdır ve ikisi
de ondan önce gelmek zorundadır.
Bu iki kural her taban sıralaması için sağlanamaz. Sağlanamadığında sınıf kurulamaz ve
hata örnek var edilirken değil, class deyimi çalışırken alınır.
- SN20 — Dört taban sıralaması aynı üç sınıftan kurulur; kurulan sınıfın kendisi çıktıdan düşürülür, yalnız ondan sonraki sıra basılır.
- SN21 — Kurulamayan sıralamada ünlem önekiyle istisnanın adı kaydedilir.
- SN22 — Sınıflar önceki bloğun tanımlarıdır; ağaç değiştirilmemiştir.
def kurmayi_dene(tabanlar): """Verilen taban sirasiyla bir sinif kurulabiliyor mu?""" try: s = type("Deneme", tabanlar, {}) except TypeError as e: return f"!{type(e).__name__}" return " -> ".join(t.__name__ for t in s.__mro__[1:-1]) print(f"{'taban sırası':<22s} {'kurulan sınıftan sonraki sıra':<34s}") for tabanlar in ((Zamanli, Imzali), (Imzali, Zamanli), (Zamanli, Kayit), (Kayit, Zamanli)): ad = "(" + ", ".join(t.__name__ for t in tabanlar) + ")" print(f"{ad:<22s} {kurmayi_dene(tabanlar):<34s}") print() print(f"Kayit'in Belge sırasındaki yeri: " f"{Belge.__mro__.index(Kayit)} (dört sınıfın sonuncusu)") print(f"Zamanli'nin doğrudan tabanı: " f"{', '.join(t.__name__ for t in Zamanli.__bases__)}") print(f"Belge sırasında Zamanli'den hemen sonra gelen: " f"{Belge.__mro__[Belge.__mro__.index(Zamanli) + 1].__name__}")
taban sırası kurulan sınıftan sonraki sıra (Zamanli, Imzali) Zamanli -> Imzali -> Kayit (Imzali, Zamanli) Imzali -> Zamanli -> Kayit (Zamanli, Kayit) Zamanli -> Kayit (Kayit, Zamanli) !TypeError Kayit'in Belge sırasındaki yeri: 3 (dört sınıfın sonuncusu) Zamanli'nin doğrudan tabanı: Kayit Belge sırasında Zamanli'den hemen sonra gelen: Imzali
Dört sıralamanın üçü kuruluyor, biri düşüyor. İlk iki satır taban yazım sırasının
sonucu doğrudan belirlediğini gösteriyor: yerler değişince kaynak’ı yanıtlayan sınıf da
değişir. Dördüncü satır sınırı çiziyor — Kayit önce, türevi Zamanli sonra yazıldığında
“her sınıf tabanlarından önce gelir” kuralı sağlanamaz ve sınıf hiç kurulamaz. Bu bir çalışma
zamanı hatası değildir; tanım anında alınır. Sıra her sınıf için bir kez hesaplanır ve
sınıfın üzerinde saklanır; her öznitelik erişiminde yeniden kurulmaz. Aramanın bedeli bu
yüzden ağacın biçimine değil, adın sırada kaçıncı sınıfta bulunduğuna bağlıdır.
Son üç satır ilk tablodaki sıranın nereden geldiğini kapatıyor. Kayit, Zamanli’nin
doğrudan tabanıdır; buna rağmen Belge sırasında Zamanli’den hemen sonra gelen sınıf
Kayit değil, Imzali’dir. Sıra ağaçta yukarı doğru tek dal izlemez; bir sınıfı geçince
o dalın tabanına atlamaz, ortak tabanı en sona bırakır.
Bu kuralın gerekçesi ilk ölçümde duruyor. Sıra Belge -> Zamanli -> Kayit -> Imzali olsaydı
kaynak araması Kayit’te dururdu ve Imzali’nin yazdığı gövde hiç bulunamazdı — bir
kardeşin yeniden yazdığı yöntem, öteki kardeşin tabanı yüzünden kaybolurdu. Ortak taban en
sona alınarak bu kayıp önlenir: taban ancak bütün türevleri denendikten sonra sorulur.
Sıranın iki kuralı bu yüzden bir üslup tercihi değil, çoklu kalıtımın anlamlı olmasının
koşuludur.
super() Ataya Değil, Sıraya Gider
Bunun en görünür sonucu super() çağrısındadır. Adı bir “üst sınıf” çağrısı gibi okunur ama
yaptığı iş budur değildir: super(), çağrıyı yapan nesnenin çözümleme sırasında, gövdenin
yazıldığı sınıftan hemen sonraki sınıfa gider. Gövde aynı kalır, sıra değişir, gidilen yer
değişir.
- SN23 — Ölçüm için dört yeni sınıf kurulur; ortak kurgunun ağacına dokunulmaz.
- SN24 — Dört sınıfın dördü de aynı adı yazar ve gövdesi tek satırdır: kendi adını listeye
koyup
super()üzerinden zinciri sürdürür. Taban sınıf zinciri kapatır. - SN25 — Ölçülen şey çağrının sonucu değil, koşan gövdelerin dizisidir; sayı gövde sayısıdır.
- SN26 — İkinci tablo
SolKatman’ın gövdesini iki ayrı örnek üzerinden okur; gövde tek, örnekler iki ayrı sınıftandır.
class Katman: def etiket(self): return ["Katman"] class SolKatman(Katman): def etiket(self): return ["SolKatman"] + super().etiket() class SagKatman(Katman): def etiket(self): return ["SagKatman"] + super().etiket() class Birlesik(SolKatman, SagKatman): def etiket(self): return ["Birlesik"] + super().etiket() def sonraki(sinif, govde): """Govdesi 'govde' sinifinda yazili super() cagrisi kime gider?""" m = sinif.__mro__ return m[m.index(govde) + 1].__name__ print(f"{'örneğin sınıfı':<12s} {'koşan gövde':>11s} zincir") for s in (SolKatman, Birlesik): z = s().etiket() print(f"{s.__name__:<12s} {len(z):>11d} {' -> '.join(z)}") print() print(f"{'gövdeyi yazan':<14s} {'örneğin sınıfı':<14s} super() kime gider") for s in (SolKatman, Birlesik): print(f"{'SolKatman':<14s} {s.__name__:<14s} {sonraki(s, SolKatman)}")
örneğin sınıfı koşan gövde zincir SolKatman 2 SolKatman -> Katman Birlesik 4 Birlesik -> SolKatman -> SagKatman -> Katman gövdeyi yazan örneğin sınıfı super() kime gider SolKatman SolKatman Katman SolKatman Birlesik SagKatman
SolKatman gövdesi iki ölçümde de aynıdır ve tek satırdır. Bir SolKatman örneğinde o
satırdaki super() çağrısı Katman’a gider ve zincir 2 gövdede biter. Bir Birlesik
örneğinde aynı satır SagKatman’a gider — SolKatman’ın tabanı olmayan, gövdesinde adı
bile geçmeyen bir sınıfa — ve zincir 4 gövdede biter.
SagKatman’ın hiç atlanmaması bu düzenin amacıdır: çoklu kalıtımda her sınıfın gövdesi
tam bir kez koşar ve ortak taban en sonda bir kez çalışır. Zincirin işlemesi için her
halkanın super() çağırması gerekir; bir sınıf çağırmayı bıraktığında ondan sonraki bütün
gövdeler sessizce düşer. Kurulum yöntemleri de aynı zincire tabidir: __init__ içinde
super().__init__(...) yazılmadığında ara sınıfların kurulum gövdeleri hiç koşmaz ve eksiklik
ancak eksik alan okunduğunda ortaya çıkar.
Zincirin ikinci koşulu imzalardır. Bir gövde super() üzerinden kime gideceğini bilmediğine
göre, aynı adı taşıyan bütün gövdelerin birbirini kabul eden parametrelerle yazılmış olması
gerekir. SolKatman gövdesini yazan taraf SagKatman’ı hiç görmemiş olabilir; buna rağmen
ikisi aynı çağrıda yan yana koşar. Bileşimde böyle bir örtük sözleşme yoktur — Sarmal.__init__
içerideki iki nesneyi adlarıyla ve argümanlarıyla açıkça var eder, gidilecek yer sıraya değil
gövdeye yazılıdır.
Buradan tasarım tarafındaki sonuç çıkar. Bir gövdeyi doğru okumak için o gövdenin bulunduğu
sınıfı bilmek yetmez; çağrının hangi nesneden geldiğini de bilmek gerekir. Bileşimde böyle
bir bilinmez yoktur: Sarmal.bicimle gövdesindeki çağrı self.imzali’ye gider ve o ad
gövdenin içinde yazılıdır. Kalıtım kısa yazım verir ve yanıtlayanı gizler; bileşim uzun yazım
ister ve yanıtlayanı yazar.
Gizlenen şeyin okunamaz olmadığını da eklemek gerekir. Çözümleme sırası bir tahmin değil,
sınıfın üzerinde duran ve okunabilen bir listedir; saglayan işlevi o listeyi gezen üç
satırdır. Bir yeteneğin gövdesini arayan biri sınıf ağacını gözle izlemek zorunda değildir,
sırayı doğrudan sorabilir. Kalıtımın bedeli bilginin yok olması değil, çağrıyı okuyan
kişinin onu ayrıca sorması gerekmesidir.
Özet
- Bir yeteneği hangi sınıfın yanıtladığını yöntem çözümleme sırası belirler; ad birden çok sınıfta yazılıysa sırada ilk gelen kazanır ve ötekiler hiç çağrılmaz.
Belgedört yeteneğin 0’ını kendi gövdesinde taşır, yanıt üç ayrı sınıftan gelir;Sarmalaynı dört yeteneğin 4’ünü de kendi gövdesinde yazar.bicimlegövdesiKayit’te yazılıdır ama içindeki çağrıselfüzerinden çözüldüğü için sonuç<Imzali>çıkar: gövdeyi okumak sonucunu söylemez.- Çözümleme sırası iki kuralla kurulur — her sınıf tabanlarından önce, tabanlar yazıldıkları
sırayla. Dört taban sıralamasının üçü kurulur, kural sağlanamayan biri tanım anında
TypeErrorile düşer. super()gövdenin yazıldığı sınıfın tabanına değil, örneğin sırasında o sınıftan sonra gelene gider: aynıSolKatmangövdesi bir örnekteKatman’a, ötekindeSagKatman’a gider ve zincir 2’ye karşı 4 gövde koşar.
Sonraki Adım
Buraya kadar bütün adlar dışarıdan görünür kabul edildi: Belge örneğinin kaynak’ı da,
Sarmal’ın içinde tuttuğu imzali nesnesi de aynı biçimde okunabilir. Oysa imzali,
Sarmal’ın iç düzenidir; dışarıdan ona dokunan bir kod, Sarmal‘ın yarın değiştirebileceği
bir ayrıntıya bağlanmış olur. İki tasarımın ayrıldığı yerlerden biri de budur: kalıtımda taban
sınıfın her adı türeve olduğu gibi geçer, bileşimde dışarıya ne verileceği gövdede tek tek
yazılır. Sonraki ders bu ayrımın Python’da nasıl bildirildiğini ölçer:
adın başına konan alt çizgi erişimi gerçekten engelliyor mu, iki alt çizgi ne değiştiriyor,
ve engellemiyorsa bu bildirimin ölçülebilir bir işi var mı?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.