İçeriğe geç
academia.sh

Ders 07 / 11

Soyut Temel Sınıflar ve Protokoller

Üç adayın 1'i soy bağı sınamasını, 3'ü yöntem sınamasını geçer ve 3'ü de çağrıyı karşılar; iki sınamanın kör noktalarında üç adayın 3'ü kabul edilir ama çağrıyı yalnız 1'i karşılar.

İçindekiler

Önceki ders bir sınıfın ne yazdığını sözlüğüne bakarak saydı; ondan öncekiler yanıtı sınıf ağacında aradı. İki yöntem de aynı varsayıma dayanıyordu: bir nesnenin ne yapabildiğini bilmek için hangi sınıftan geldiğini bilmek gerekir. Oysa dördüncü ders bunun aksini gösterdi — katılım tipin adından değil, yazılan yöntemlerden geliyordu. İkinci dersin Sarmal sınıfı bu ikiliğin somut hâlidir: hiçbir şeyden türemez ve dört yeteneğin dördünü de karşılar.

Bu ders ikiliği bir sınamaya çevirir. Bir sözleşmenin sağlandığını iki ayrı biçimde sorabiliriz. Nominal sınama soy bağı arar: “bu nesne şu sözleşmeden türedi mi?” Yapısal sınama yöntem arar: “bu nesnede şu adlar var mı?” Aynı nesne ikisinden hangisini geçer, ve geçtiği sınama çağrıyı gerçekten karşılayacağını söyler mi? Sorunun ölçülebilir olması için her adayda üç şey ayrı ayrı sorulur: iki sınamanın sonucu ve çağrının kendisi. Üçünün de aynı çıkmadığı yerler bu dersin bulgusudur.

İki Sınama, Bir Nesne

İki sözleşme kurulur ve ikisi de aynı iki yeteneği ister: kaynak ve bicimle. Birincisi bir soyut temel sınıftır (abstract base class); onu sağlayan bir sınıf ondan türemek zorundadır. İkincisi bir protokoldür; sınama sırasında yalnız adların varlığına bakar.

Üç aday sınanır. Belge kalıtım ağacından gelir ve iki yeteneği de üç ayrı sınıftan devralır. Sarmal hiçbir şeyden türemez ve iki yeteneği de kendi gövdesinde yazar. Bildiren soyut tabandan türer ve iki gövdeyi de yazar.

  • SN72 — Kurgunun dört sınıflı ağacı ve Sarmal ortak tanımdaki biçimindedir; sözleşmeler ayrıca kurulur ve ağaca dokunulmaz.
  • SN73 — İki sözleşme de aynı iki adı ister; aralarındaki tek fark sınamanın neye bakmasıdır.
  • SN74 — Her aday için üç şey ayrı ayrı ölçülür: adların gerçekten bulunup bulunmadığı, iki sınamanın sonucu, ve çağrının verdiği sonuç.
  • SN75 — Son sütun bicimle() çağrısının döndürdüğü dizgidir; ikinci derste ölçülen sonucun aynısıdır ve burada yeniden basılır.
"""Nominal ve yapisal sozlesme: ayni nesne hangi sinamayi geciyor."""

from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable


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:
    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()}>"


class Bicimlenebilir(ABC):
    """Nominal sozlesme: soy bagi ister."""

    @abstractmethod
    def kaynak(self):
        ...

    @abstractmethod
    def bicimle(self):
        ...


@runtime_checkable
class BicimlenebilirYapi(Protocol):
    """Yapisal sozlesme: yontem adi arar."""

    def kaynak(self): ...

    def bicimle(self): ...


class Bildiren(Bicimlenebilir):
    """Sozlesmeyi soy bagiyla bildirir ve iki govdeyi de yazar."""

    def kaynak(self):
        return "Bildiren"

    def bicimle(self):
        return f"<{self.kaynak()}>"


ADAYLAR = (("Belge", Belge()), ("Sarmal", Sarmal()), ("Bildiren", Bildiren()))

print(f"{'aday':<10s} {'iki yöntem de var':>18s} {'soy bağı sınaması':>18s} "
      f"{'yöntem sınaması':>16s} {'bicimle() sonucu':>17s}")
for ad, n in ADAYLAR:
    print(f"{ad:<10s} "
          f"{str(all(hasattr(n, y) for y in ('kaynak', 'bicimle'))):>18s} "
          f"{str(isinstance(n, Bicimlenebilir)):>18s} "
          f"{str(isinstance(n, BicimlenebilirYapi)):>16s} {n.bicimle():>17s}")

nominal = sum(1 for _, n in ADAYLAR if isinstance(n, Bicimlenebilir))
yapisal = sum(1 for _, n in ADAYLAR if isinstance(n, BicimlenebilirYapi))
print(f"\naday {len(ADAYLAR)}, soy bağı sınamasını geçen {nominal}, "
      f"yöntem sınamasını geçen {yapisal}, çağrıyı karşılayan {len(ADAYLAR)}")
aday        iki yöntem de var  soy bağı sınaması  yöntem sınaması  bicimle() sonucu
Belge                    True              False             True          <Imzali>
Sarmal                   True              False             True          <Imzali>
Bildiren                 True               True             True        <Bildiren>

aday 3, soy bağı sınamasını geçen 1, yöntem sınamasını geçen 3, çağrıyı karşılayan 3

Üç adayın üçü de çağrıyı karşılıyor. Soy bağı sınamasını geçen 1, yöntem sınamasını geçen 3. Aradaki iki nesne — Belge ve Sarmal — sözleşmenin istediği her şeyi yapıyor ve nominal sınamadan geçemiyor; çünkü o sınama yapılan işe değil, bildirilen soya bakıyor.

İki adayın nominal sınamayı geçememesi ayrı nedenlere dayanıyor ve bu, sınamanın ne kadar dar olduğunu gösteriyor. Sarmal sözleşmeden haberdar bile değil. Belge ise sözleşmenin istediği iki gövdeyi kalıtımla taşıyor — ikinci derste ölçüldüğü gibi bicimle gövdesi Kayit’te, kaynak gövdesi Imzali’de. İkisi de yerinde duruyor, ikisi de çalışıyor, ve sınama yine False veriyor.

Yapısal sınama bu iki nesneyi kabul ediyor, çünkü sorduğu soru başka: sözleşmenin istediği adlar bu nesnede bulunuyor mu? Bulunuyorsa nereden geldiği önemsizdir — kendi gövdesinden, kalıtımla ya da bileşimle. Sözleşme bir soy değil, bir yüzey olarak tanımlanmıştır.

İki sınamanın maliyeti de aynı değildir. Nominal sınama tek bir soy ilişkisine bakar; sonucu sınıf kurulduğu anda bellidir ve her sınamada yeniden hesaplanmaz. Yapısal sınama sözleşmenin istediği her adı nesnede aramak zorundadır ve sözleşme büyüdükçe aranan ad sayısı artar. Ölçümdeki sözleşme iki ad istiyor; on ad isteyen bir sözleşmede aynı sınama on arama yapar. Ayrım burada da yön değiştirmiyor: nominal sınama ucuz ve dar, yapısal sınama pahalı ve geniştir.

Arayüz ve soyut sınıf seçimi, arayüzlerin istemciye göre daraltılması ve sözleşmenin alt tiplerde korunması Yazılım Tasarımı ve Mimari İlkeler müfredatında kuruldu; o tartışmalar tekrarlanmaz. Burada ölçülen tek şey, aynı nesnenin hangi sınamayı geçtiğidir.

Ölçüm bir seçim de dayatmıyor; iki sınamanın hangi durumda doğru olduğunu söylüyor. Sözleşmenin iki tarafını da biz yazıyorsak nominal bildirim kazanç sağlar: bildirimi yapan sınıf ne sağlamak zorunda olduğunu açıkça söyler ve eksik bıraktığında durdurulur. Sözleşmeyi karşılayacak nesneleri başkaları yazacaksa nominal bildirim bir yük olur — kabul edilecek her nesnenin bizim tanımımızdan türemesini istemek, Belge ve Sarmal gibi işi eksiksiz yapan nesneleri dışarıda bırakmak demektir. Standart kitaplığın kap sözleşmelerinin çoğu bu yüzden gövde arayan bir kanca taşır: bir sınıf gerekli yöntemleri yazdığında, hiçbir soy bağı kurmadan sözleşmenin sağlayıcısı sayılır.

Sözleşme Ne Zaman Uygulanır

Nominal sözleşmenin bir de uygulama tarafı vardır. Soyut bir tabandan türeyen bir sınıf, bildirilen gövdelerin hepsini yazmak zorundadır. Ama bu zorunluluk hangi anda denetlenir — sınıf tanımlanırken mi, yoksa örnek var edilirken mi?

  • SN76 — İki sınıf da aynı soyut tabandan türer; biri iki gövdeyi de yazar, öteki yalnız birini.
  • SN77 — “Tanım anı” sütunu bloğun çalışmasından okunur: iki sınıf da tanımlanabildiği için değeri sabittir. “Örnek var etme” sütunu çağrının sonucudur ve düşen çağrı ünlem önekiyle istisnanın adını kaydeder.
class Eksik(Bicimlenebilir):
    """Sozlesmeyi bildirir ama iki govdeden yalniz birini yazar."""

    def kaynak(self):
        return "Eksik"


def dene(islev):
    try:
        islev()
    except Exception as e:
        return f"!{type(e).__name__}"
    return "geçti"


print(f"{'sınıf':<10s} {'tanım anı':>10s} {'örnek var etme':>16s} "
      f"{'yazılmamış yöntem':>18s}")
for sinif in (Bildiren, Eksik):
    print(f"{sinif.__name__:<10s} {'kuruldu':>10s} {dene(sinif):>16s} "
          f"{str(sorted(sinif.__abstractmethods__)):>18s}")
sınıf       tanım anı   örnek var etme  yazılmamış yöntem
Bildiren      kuruldu            geçti                 []
Eksik         kuruldu       !TypeError        ['bicimle']

Eksik sınıf tanımlanabiliyor. Hata tanım anında değil, ilk örnek var edilmeye çalışıldığında alınıyor ve son sütun eksiğin adını veriyor. Ayrım pratikte önemlidir: eksik sınıf bir dosyada durabilir, içe aktarılabilir, başka sınıflara taban olabilir. Kendisi hiç var edilmediği sürece hiçbir şey olmaz — nitekim soyut tabanın kendisi de bu yüzden var edilemez. Denetimin bu noktaya konması yerinde bir seçimdir: eksik bir sınıf hâlâ yararlı bir tanım olabilir, ama eksik bir nesne sözleşmeyi taşıdığını iddia ederken çağrıyı karşılayamaz. Durdurulan şey tanım değil, iddiadır.

Bu, uygulanan tek sözleşme olması bakımından kayda değerdir. Üçüncü ders adlandırmanın 0 erişim engellediğini ölçmüştü; burada bildirilen sözleşme gerçekten uygulanıyor ve eksik bir gerçekleştirim çalışma zamanında durduruluyor. Uygulama noktası ise dar: yalnız gövdenin varlığı sınanır. Yazılan gövdenin doğru şeyi yapıp yapmadığı, doğru değeri döndürüp döndürmediği bu sınamaya girmez.

Denetimin örnek var etmeye bağlanması bir kolaylık da sağlıyor. Yazılmamış yöntemlerin kümesi her alt sınıf için yeniden hesaplanır; bir sınıf ara katman olarak kalıp bazı gövdeleri bilerek yazmayabilir ve zincirin sonundaki sınıf eksikleri tamamlar. Ara sınıf var edilmediği sürece hiçbir engelle karşılaşmaz. Sözleşme böylece birden çok sınıfa bölünebilir — denetlenen şey bir sınıfın kendi gövdesi değil, çözümleme sırası boyunca toplanan gövdelerin tamamıdır.

İki Sınamanın Kör Noktaları

Her iki sınamanın da kabul ettiği ama çağrıyı karşılamayan nesneler kurulabilir. İki kör nokta birbirinin aynası gibidir: nominal sınama soyu okur ve gövdeye bakmaz; yapısal sınama adı okur ve imzaya bakmaz.

  • SN78 — Birinci aday hiçbir gövde yazmaz ve sözleşmeye elle kaydedilir; kayıt yalnızca soy bilgisini değiştirir.
  • SN79 — İkinci aday iki adı da taşır ama iki yöntemi de fazladan bir argüman ister; adlar doğru, çağrı biçimi yanlıştır.
  • SN80 — Üçüncü aday önceki ölçümün doğru sınıfıdır ve karşılaştırma tabanı olarak eklenmiştir. Çağrı sütunu her adayda argümansız yapılır.
class Bos:
    """Hicbir govde yazmaz; sozlesmeye elle kaydedilir."""


Bicimlenebilir.register(Bos)


class YanlisImza:
    """Iki adi da tasir ama ikisi de fazladan bir arguman ister."""

    def kaynak(self, ek):
        return f"YanlisImza-{ek}"

    def bicimle(self, ek):
        return f"<{self.kaynak(ek)}>"


KOR_NOKTALAR = (("Bos", Bos()), ("YanlisImza", YanlisImza()),
                ("Bildiren", Bildiren()))

print(f"{'aday':<12s} {'soy bağı sınaması':>18s} {'yöntem sınaması':>16s} "
      f"{'bicimle() çağrısı':>18s}")
for ad, n in KOR_NOKTALAR:
    print(f"{ad:<12s} {str(isinstance(n, Bicimlenebilir)):>18s} "
          f"{str(isinstance(n, BicimlenebilirYapi)):>16s} "
          f"{dene(n.bicimle) if hasattr(n, 'bicimle') else '!AttributeError':>18s}")

kabul = sum(1 for _, n in KOR_NOKTALAR
            if isinstance(n, Bicimlenebilir) or isinstance(n, BicimlenebilirYapi))
karsilayan = sum(1 for _, n in KOR_NOKTALAR
                 if hasattr(n, "bicimle") and dene(n.bicimle) == "geçti")
print(f"\naday {len(KOR_NOKTALAR)}, en az bir sınamayı geçen {kabul}, "
      f"çağrıyı karşılayan {karsilayan}")
aday          soy bağı sınaması  yöntem sınaması  bicimle() çağrısı
Bos                        True            False    !AttributeError
YanlisImza                False             True         !TypeError
Bildiren                   True             True              geçti

aday 3, en az bir sınamayı geçen 3, çağrıyı karşılayan 1

Üç adayın 3’ü en az bir sınamayı geçiyor; çağrıyı karşılayan 1. İlk satır nominal sınamanın körlüğüdür: Bos hiçbir gövde yazmaz, sözleşmeye elle kaydedilmiştir ve sınama True verir. Kayıt yalnız soy bilgisini değiştirir; yazılan gövdeleri hiç sormaz. Çağrı AttributeError ile düşer — sınamayı geçmiş bir nesne, aranan adı bile taşımıyor.

İkinci satır yapısal sınamanın körlüğüdür. YanlisImza iki adı da taşıyor ve sınama True veriyor; ama iki yöntem de fazladan bir argüman istiyor ve argümansız çağrı TypeError ile düşüyor. Yapısal sınama adın varlığına bakar; parametrelerine, dönüş değerine ya da gövdesinin ne yaptığına bakmaz. Yüzeyin adı eşleşiyor, biçimi eşleşmiyor.

İki satır birlikte dersin sonucunu veriyor. Nominal sınama bildirilmiş bir niyeti doğrular; niyetin yerine getirildiğini doğrulamaz. Yapısal sınama var olan adları doğrular; o adların çağrılabilir olduğunu doğrulamaz. Hiçbiri “bu nesne çağrıyı karşılar” demez ve ikisinin de kabul ettiği bir nesne yanlış olabilir.

Bunun ölçülen karşılığı ilk tabloyla yan yana konduğunda tamamlanıyor. Birinci ölçümde üç adayın 3’ü çağrıyı karşılıyordu ve nominal sınama bunların 1’ini kabul ediyordu — sınama gereğinden dardı. Üçüncü ölçümde iki sınama birer nesneyi kabul etti ve o nesneler çağrıyı karşılamadı — sınama gereğinden genişti. Aynı araç bir yönde eksik, öteki yönde fazla eleme yapıyor. Doğru sözleşme, sınamanın kendisinde değil, sınamanın neyi ölçtüğünü bilerek seçilmesindedir.

Kör noktaların kapanma yeri de bellidir ve ikisi için ayrıdır. Nominal sınamanın kör noktası elle kayıt yazımına özgüdür; soy bağı kalıtımla kurulduğunda ikinci ölçümdeki denetim devreye girer ve eksik gövdeli bir sınıfın örneği hiç var edilemez. Elle kayıt bu denetimi atlayan bir bildirimdir ve sorumluluğu kaydı yapana bırakır. Yapısal sınamanın kör noktası ise kapanmıyor: çalışma zamanında yapılan bir ad taraması, imzayı hiçbir koşulda göremez. İmzayı görebilecek olan taraf, kodu çalıştırmadan okuyan bir taraftır.

Bu ayrım pratikte bir yazım kuralına dönüşür. Bir sınamanın geçmesi çağrının karşılanacağının güvencesi değildir; sınama yalnız bir ön elemedir. Gelen nesneyi sınamak, onu çağırmanın yerine geçmez — ve sınamayı geçmiş bir nesnenin çağrısı yine de düşebileceği için, sınama ile çağrı arasındaki boşluğun kim tarafından kapatıldığı ayrıca kararlaştırılmalıdır.

Özet

  • Nominal sınama soy bağı arar, yapısal sınama yöntem adı arar; aynı üç adayın 1’i birinciyi, 3’ü ikinciyi geçer ve 3’ü de çağrıyı karşılar.
  • Belge ile Sarmal sözleşmenin istediği iki gövdeyi de taşır — biri kalıtımla, öteki bileşimle — ve ikisi de nominal sınamadan geçemez; sınama yapılan işe değil, bildirilen soya bakar.
  • Soyut tabanın bildirdiği sözleşme tanım anında değil, ilk örnek var edilirken uygulanır: eksik sınıf kurulur, örneği TypeError ile düşer ve eksik gövdenin adı bildirilir.
  • Nominal sınamanın kör noktası gövdedir: hiçbir gövde yazmayan bir sınıf sözleşmeye elle kaydedildiğinde sınamayı geçer ve çağrı AttributeError ile düşer.
  • Yapısal sınamanın kör noktası imzadır: iki adı da taşıyan ama fazladan argüman isteyen bir sınıf sınamayı geçer ve çağrı TypeError ile düşer.
  • Üç kör nokta adayının 3’ü en az bir sınamayı geçer, çağrıyı karşılayan 1’dir: sınamayı geçmek, çağrının karşılanacağını söylemez.

Sonraki Adım

Bu konu boyunca sözleşme hep kodda bildirildi. Soyut taban gövdelerin yazılmasını istedi ve istemediğinde örnek var edilmedi; protokol adların bulunmasını istedi ve sınama sonucunu çalışma zamanında verdi. İkisi de eksik kaldı, ama ikisi de çalışan birer denetimdi: bildirilen şeyle var olan şey karşılaştırıldı ve sonuç bir değer olarak döndü.

Son ölçümün açtığı boşluk başka türden bir bildirimi gerektiriyor. YanlisImza sınamayı geçti, çünkü sınamanın gördüğü tek şey adlardı; yöntemin kaç argüman aldığı, argümanların neye benzediği ve ne döndürdüğü hiçbir sınamaya girmedi. Bu bilgiler koda yazılabilir — Python bunun için bir yazım biçimi verir ve önceki dersin kısa tanımı zaten o yazımı alan bildirimi olarak kullandı. Bir sonraki konu tam buradan başlar: sözleşme yazıya döküldüğünde onu kim sınar, çalışma zamanı bu yazıyla ne yapar, ve yazılan şeyle yapılan şey ayrıştığında hata nerede görünür?

İ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