İçeriğe geç
academia.sh

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 Sarmal ortak kurgunun tanımladığı biçimdedir; yeteneklerin yerleşimi değiştirilmemiştir.
  • SN17 — Ölçülen dört yetenek kaynak, bicimle, damga ve imza’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.
  • Belge dört yeteneğin 0’ını kendi gövdesinde taşır, yanıt üç ayrı sınıftan gelir; Sarmal aynı dört yeteneğin 4’ünü de kendi gövdesinde yazar.
  • bicimle gövdesi Kayit’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 TypeError ile 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ı SolKatman gövdesi bir örnekte Katman’a, ötekinde SagKatman’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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat