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
Sarmalortak 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.
BelgeileSarmalsö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
TypeErrorile 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ı
AttributeErrorile 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ı
TypeErrorile 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.