Ders 08 / 11
Tip İpuçları
Açıklama koda yazılır, yorumlayıcı onu saklar ve sınamaz: beş çağrının dördü ihlallidir ve çalışma zamanında hata veren sayısı sıfırdır, ama açıklamaların tamamı çalışma zamanında okunabilir.
İçindekiler
Önceki ders sözleşmeyi kodda bildirdi ve sınadı. Soy bağı arayan sınama bir sınıfın bildirilmiş atasını sordu, yöntem arayan sınama nesnenin gerçekten taşıdığı adlara baktı; ikisi de bir çalıştırma sırasında, elde duran nesne üzerinde sonuç verdi. Sözleşme koddaydı ve sınayan da koddu.
Bu dersin sorusu sözleşmenin başka bir yazılış biçiminden doğar. Bir işlevin imzasına “buraya bir dizgi gelir, buradan bir tam sayı çıkar” yazmak da bir sözleşmedir — ama bu sözleşme bir sınıf ağacına ya da bir yöntem adına değil, yazının kendisine işlenmiştir. Kursun ekseni burada da aynı: sözü kim denetler? Bu ders yanıtın bir katmanını ölçer, en alttakini: çalışma zamanı.
Açıklamanın Yazılışı
Tip ipucu (type hint) bir adın hangi tipte değer taşımasının beklendiğini bildiren
sözleşmedir. Açıklama (annotation) o ipucunun koda yazılmış hâlidir: işlev
parametresinden sonra iki nokta ve tip, dönüş için -> ve tip.
def yas_yaz(ad: str, yas: int) -> str: return f"{ad}:{yas}"
Aynı yazım işlev dışında da geçerlidir. Bir sınıf gövdesinde ad: str bir alanı bildirir,
modül düzeyinde sayac: int = 0 bir değişkeni bildirir. Dördüncü bir yer daha vardır: bir
işlevin gövdesindeki yerel değişken. Ölçüm bu dördünü ayırıyor, çünkü yazımın aynı olması
sonucun aynı olduğu anlamına gelmiyor.
Programlama Temelleri kursu tip dönüşümünü ve dinamik tiplemeyi kurdu; bir adın çalışma zamanında hangi nesneye bağlandığı orada ölçüldü ve burada tekrarlanmaz. TypeScript kursu da aynı soruyu, tip katmanı çalıştırmadan önce denetlenen bir dilde sordu. Buradaki fark tek cümleyle şudur: Python’da açıklama çalıştırmayı durdurmaz; bu dersin ölçtüğü şey tam olarak o durdurmamanın sayısıdır.
Yorumlayıcı Açıklamayla Ne Yapar
Yorumlayıcı açıklamayı okur, değerlendirir ve saklar. Sakladığı yer işlevin
__annotations__ sözlüğüdür; sınıfın ve modülün de aynı adlı bir sözlüğü olur. Bu sözlük
sıradan bir sözlüktür ve çalışma zamanında okunabilir.
Yapmadığı şey ise sınamaktır. Çağrı geldiğinde parametreye bağlanan nesnenin açıklamaya uyup uymadığına bakılmaz; uymayan bir nesne de bağlanır, işlev gövdesi de çalışır. Bunun doğrudan bir sonucu vardır: açıklamaya yazılan şeyin bir tip olması bile gerekmez. Ölçüm bunu da gösteriyor — dönüş açıklaması olarak bir sayı yazılmış bir işlev sorunsuz tanımlanır, sorunsuz çağrılır ve o sayı sözlükte olduğu gibi durur.
Ölçüm iki soruyu ayrı ayrı soruyor. Birincisi: açıklama nereye yazılırsa saklanır? İkincisi: açıklamaya uymayan çağrılar çalıştırıldığında kaçı hata verir?
Ölçümün varsayımları:
- TP1 — Kâhin kurgunun kendisidir: beş çağrının hangisinin tip ihlali, hangisinin alan ihlali taşıdığını dersin yazarı belirledi. Ölçüm bu sınıflandırmayı bir araçtan değil, kurgudan alır.
- TP2 — Tip ihlali, bağlanan nesnenin açıklamadaki tipten olmamasıdır. Alan ihlali (domain violation), nesnenin tipi doğru olduğu hâlde değerinin alan kuralını bozmasıdır: boş ad, negatif yaş.
- TP3 — Beş çağrının ikisi tip ihlali, ikisi alan ihlali taşır, biri temizdir. Diziler bu kursun ortak kurgusundan gelir ve derste değiştirilmez.
- TP4 — Çalışma zamanı katmanının ölçüsü, çağrının bir istisna fırlatıp fırlatmadığıdır. Gövde bir dizgi biçimlemesinden ibarettir ve biçimleme her tipi kabul eder; ölçülen şey gövdenin ne yaptığı değil, açıklamanın çağrıya karışıp karışmadığıdır.
- TP5 — Saklama ölçümünde sayılan şey
__annotations__sözlüğünün öğe sayısıdır; dönüş açıklaması da bir öğedir. - TP6 — Yerel değişken açıklaması için “hiçbir yerde” denmesi, işlev nesnesinde böyle
bir kaydın bulunmaması demektir; ölçü
__annotations__üzerinden alınır. - TP7 — Bileşik açıklama ölçümünde sorulan tek soru, saklanan nesnenin
isinstancesınamasına doğrudan verilip verilemeyeceğidir. Sınama başarısız olursa değil, sınamanın kendisi bir istisna fırlatırsa “hayır” yazılır. - TP8 — Son satırdaki hata, açıklamanın değil gövdenin ürettiği hatadır; ölçüm bu ikisini ayırmak için o çağrıyı ayrıca çalıştırır.
Ölçüm
"""Tip ipuclari: aciklama nerede saklanir, calisma zamaninda ne denetlenir.""" sayac: int = 0 def yas_yaz(ad: str, yas: int) -> str: return f"{ad}:{yas}" class KayitAlani: ad: str yas: int = 0 def yerel_aciklama(): gecici: int = 1 return gecici def saklanan(nesne): return len(getattr(nesne, "__annotations__", {})) print(f"{'açıklamanın yeri':<22s} {'saklandığı yer':<24s} {'okunabilen':>10s}") YERLER = (("işlev imzası", "islev.__annotations__", saklanan(yas_yaz)), ("sınıf gövdesi", "Sinif.__annotations__", saklanan(KayitAlani)), ("modül düzeyi", "modül __annotations__", len(__annotations__)), ("işlev gövdesi", "hiçbir yerde", saklanan(yerel_aciklama))) for yer, nerede, sayi in YERLER: print(f" {yer:<20s} {nerede:<24s} {sayi:10d}") print(f"işlev açıklaması: {yas_yaz.__annotations__}") def deneme(x: "sayi olmayan bir aciklama") -> 3: return x print(f"tip olmayan açıklama saklandı: {deneme.__annotations__}") print(f"deneme(41) -> {deneme(41)}") CAGRILAR = ( ("ad", 30), # uygun ("ad", "otuz"), # tip ihlali: yas dizgi (7, 30), # tip ihlali: ad sayi ("ad", -5), # tip uygun, alan ihlali: yas negatif ("", 30), # tip uygun, alan ihlali: ad bos ) def alan_kurali(ad, yas): hatalar = [] if isinstance(ad, str) and not ad: hatalar.append("ad bos") if isinstance(yas, int) and yas < 0: hatalar.append("yas negatif") return hatalar def calisma_zamani(cagrilar): hata, sonuc = 0, [] for arg in cagrilar: try: sonuc.append(yas_yaz(*arg)) except Exception: hata += 1 sonuc.append(None) return hata, sonuc tip_ihlali = [i for i, a in enumerate(CAGRILAR) if not isinstance(a[0], str) or not isinstance(a[1], int)] alan_ihlali = [i for i, a in enumerate(CAGRILAR) if alan_kurali(*a)] hata, sonuc = calisma_zamani(CAGRILAR) print() print(f"{'çağrı':>6s} {'tip ihlali':>11s} {'alan ihlali':>12s} " f"{'çalışma zamanı':>15s} {'dönen değer':>13s}") for i, arg in enumerate(CAGRILAR): print(f"{i:6d} {str(i in tip_ihlali):>11s} {str(i in alan_ihlali):>12s} " f"{'hata yok':>15s} {sonuc[i]!r:>13s}") print() print(f"çağrı {len(CAGRILAR)}, ihlalli {len(set(tip_ihlali) | set(alan_ihlali))}, " f"tip ihlali {len(tip_ihlali)}, alan ihlali {len(alan_ihlali)}, " f"çalışma zamanında hata veren {hata}") def toplam(sayilar: list[int]) -> int: return sum(sayilar) BILESIK = (("list[int]", list[int], [1, 2]), ("str | None", str | None, "ad"), ("dict[str, int]", dict[str, int], {"a": 1}), ("tuple[int, ...]", tuple[int, ...], (1, 2))) print() print(f"{'bileşik açıklama':<18s} {'isinstance ile sınanır':>23s}") for ad, nesne, ornek in BILESIK: try: isinstance(ornek, nesne) sinanir = "evet" except TypeError: sinanir = "hayır" print(f" {ad:<16s} {sinanir:>23s}") print(f"toplam açıklaması: {toplam.__annotations__}") try: toplam(["a", "b"]) print("toplam(['a', 'b']) -> hata yok") except TypeError: print("toplam(['a', 'b']) -> gövde hata verdi, açıklama değil")
açıklamanın yeri saklandığı yer okunabilen
işlev imzası islev.__annotations__ 3
sınıf gövdesi Sinif.__annotations__ 2
modül düzeyi modül __annotations__ 1
işlev gövdesi hiçbir yerde 0
işlev açıklaması: {'ad': <class 'str'>, 'yas': <class 'int'>, 'return': <class 'str'>}
tip olmayan açıklama saklandı: {'x': 'sayi olmayan bir aciklama', 'return': 3}
deneme(41) -> 41
çağrı tip ihlali alan ihlali çalışma zamanı dönen değer
0 False False hata yok 'ad:30'
1 True False hata yok 'ad:otuz'
2 True False hata yok '7:30'
3 False True hata yok 'ad:-5'
4 False True hata yok ':30'
çağrı 5, ihlalli 4, tip ihlali 2, alan ihlali 2, çalışma zamanında hata veren 0
bileşik açıklama isinstance ile sınanır
list[int] hayır
str | None evet
dict[str, int] hayır
tuple[int, ...] hayır
toplam açıklaması: {'sayilar': list[int], 'return': <class 'int'>}
toplam(['a', 'b']) -> gövde hata verdi, açıklama değil
Saklanan ve Saklanmayan
Üst tablo dört yeri yan yana koyuyor ve üçü ile biri arasında keskin bir sınır var.
İşlev imzası 3 açıklama saklıyor: iki parametre ve dönüş. Sınıf gövdesi 2, modül düzeyi 1 saklıyor. Bu üç yerde açıklama, yazıldığı nesnenin bir özniteliğine dönüşüyor ve program çalışırken oradan okunabiliyor.
İşlev gövdesindeki yerel açıklama ise 0 veriyor. gecici: int = 1 satırı geçerli bir
Python satırıdır, atamayı yapar, hata vermez — ama geride bir kayıt bırakmaz. Yerel adın
ömrü çağrı ile sınırlıdır ve açıklamasının ömrü de o kadardır. Aynı işlev bin kez
çağrılsaydı bin ayrı yerel ad doğup ölecek, saklanan açıklama sayısı yine 0 kalacaktı.
Sınıf ve modül düzeyinde durum tersidir: oradaki açıklama bir kez, tanımın yapıldığı anda
işlenir ve nesnenin ömrü boyunca durur.
Buradan çıkan ayrım sonraki derslerin dayanağıdır: kaynağa yazılan her açıklama çalışma zamanında görünmez. Kaynağı okuyan bir katman dört yerin dördünü de görür; çalışan programı okuyan bir katman yalnız üçünü.
Tip olmayan açıklamanın satırı da aynı noktayı başka yönden vuruyor. Dönüş açıklaması
olarak yazılan 3 sözlükte 3 olarak duruyor ve deneme(41) çağrısı 41 döndürüyor.
Yorumlayıcı açıklamanın anlamlı olup olmadığına bakmıyor, çünkü açıklamayı yorumlayan bir
tarafı yok.
Dört İhlal, Sıfır Hata
Alt tablo dersin ana sayısını veriyor. Beş çağrının ikisi tip ihlali taşıyor: birinde yaş yerine bir dizgi, öbüründe ad yerine bir sayı geçiyor. İkisi alan ihlali taşıyor: yaş -5, ad boş dizgi. Toplam 4 çağrı ihlallidir ve biri temizdir.
Çalışma zamanı sütunu beş satırın beşinde de “hata yok” diyor. Sayı 0. Dördüncü sütun
bunun nedenini de gösteriyor: her çağrı bir değer döndürüyor. 'ad:otuz' üretiliyor, çünkü
biçimleme bir dizgiyi de kabul eder. '7:30' üretiliyor, çünkü biçimleme bir sayıyı da
kabul eder. 'ad:-5' ve ':30' üretiliyor, çünkü -5 bir tam sayıdır ve boş dizgi bir
dizgidir.
Bu ölçümde ihlalleri yakalayan katman yok. Kim yakaladı sorusunun yanıtı bu derste tektir ve boştur: çalışma zamanı 0 yakalar. Kâhin dört ihlali biliyor çünkü kurguyu biz yazdık; program bilmiyor.
Bir itiraz burada haklıdır: gövde bir dizgi biçimlemesinden ibaret olduğu için hoşgörülü; başka bir gövde yanlış tiple hata verirdi. Doğrudur, ama ölçülen şey bu değil. Öyle bir gövde hata verseydi hatayı veren yine gövdedeki işlem olurdu, açıklama değil — ve hata işlev çağrıldıktan sonra, işin bir bölümü çoktan yapılmışken çıkardı. Açıklamanın sağlayabileceği tek şey çağrının hiç başlamamasıydı; ölçümde başlamayan çağrı yok.
İki ihlal türünün ayrı sayılması bir ayrıntı değil, kursun geri kalanının ekseni.
ad: str açıklaması boş dizgiyi dışarıda bırakmaz — boş dizgi bir dizgidir. yas: int
açıklaması negatif sayıyı dışarıda bırakmaz — -5 bir tam sayıdır. Tipin doğru olması
değerin geçerli olduğu anlamına gelmez ve bu, açıklamaların bir eksiği değil, tanımıdır:
açıklama tipi bildirir, değeri değil.
Bileşik Açıklama Bir Sınama Değildir
Açıklama tek bir tip adından ibaret olmak zorunda değil. list[int] “tam sayı listesi”,
dict[str, int] “anahtarları dizgi, değerleri tam sayı olan sözlük”, str | None “dizgi
ya da hiçbiri”, tuple[int, ...] “istenildiği kadar tam sayıdan oluşan demet” bildirir.
Dördü de geçerli açıklamadır ve dördü de __annotations__ içinde bir nesne olarak durur —
son satır bunu gösteriyor: toplam işlevinin açıklaması list[int] nesnesini taşıyor.
Üçüncü tablo bu nesnelerin çalışma zamanındaki sınırını ölçüyor. Dördünden üçü
isinstance sınamasına doğrudan verilemiyor; sınama bir sonuç döndürmüyor, istisna
fırlatıyor. Yalnız str | None “evet” veriyor, çünkü birleşim yalnız iki tip adını yan
yana koyar ve içeriğe bakmayı gerektirmez.
Neden böyle olduğu bir tasarım sorusu değil, bir maliyet sorusudur. isinstance(x, list)
tek bir bakışta yanıtlanır; isinstance(x, list[int]) bin öğeli bir listede bin sınama
demektir. Sınamanın bedeli kabın boyutuyla büyür ve dil bu bedeli sessizce üstlenmez.
Sonuç, dersin ana savını pekiştiriyor: açıklama bir sınama değil, bir kayıttır. Son
satır bunu doğrudan gösteriyor — toplam(["a", "b"]) çağrısı hata veriyor, ama hatayı
veren açıklama değil gövdedeki toplama işlemidir. Açıklama olmasaydı da aynı hata aynı
noktada çıkardı; açıklama olduğu hâlde çağrı engellenmedi.
Sınanmayan Açıklama Ne İşe Yarar
Sıfır sayısı bir soruyu doğuruyor: sınanmayan bir bildirim neden yazılsın?
Yanıt çıktının ortasındaki satırda duruyor. yas_yaz.__annotations__ çalışma zamanında
okunabiliyor ve ad için str, yas için int, dönüş için str veriyor. Açıklama
sınanmıyor ama kayboluyor da değil. Bir sözlükte, adı bilinen bir öznitelikte,
program çalışırken erişilebilir hâlde duruyor.
Bu, sonraki üç dersin tamamının dayandığı olgu. Açıklamayı okuyup tip uyuşmazlığı arayan bir program yazılabilir; kaynağı hiç çalıştırmadan okuyup aynı işi yapan bir program da yazılabilir; açıklamayı bir şema gibi kullanıp çağrı anında değeri sınayan bir katman da yazılabilir. Üçünün de hammaddesi bu sözlüktür.
İkinci bir yarar okuyanadır. def yas_yaz(ad, yas) imzası çağıranın hangi sırayla neyi
vereceğini söylemez; def yas_yaz(ad: str, yas: int) -> str söyler. Bu bir denetim değil,
bir belgedir — ve belge olduğu için de yanlış olabilir. Açıklamayla gövdenin uyuşmasını
sağlayan hiçbir düzenek yoktur; uyuşmayı sağlayan şey, açıklamayı okuyan başka bir
katmandır.
Kaydın kendisi de korumalı değildir. __annotations__ sıradan bir sözlüktür: okunabildiği
gibi değiştirilebilir de. Bir program çalışırken bir işlevin açıklamasını silebilir ya da
başka bir tiple değiştirebilir; işlevin gövdesi bundan etkilenmez ve çağrılar aynı sonucu
vermeye devam eder. Bu, kaydın güvenilmez olduğu anlamına gelmez — kaydın bağlayıcı
olmadığı anlamına gelir. Bağlayıcılık ancak kaydı okuyup bir karar veren bir katman
eklendiğinde doğar, ve o katmanı bu kursun geri kalanı kurar.
Özet
- Tip ipucu bir adın hangi tipte değer taşıyacağını bildiren sözleşmedir; açıklama o ipucunun koda yazılmış hâlidir ve işlev imzasında, sınıf gövdesinde, modül düzeyinde ve işlev gövdesinde yazılabilir.
- Açıklama üç yerde saklanır ve bir yerde saklanmaz: işlev 3, sınıf 2, modül 1 öğe verir; işlev gövdesindeki yerel açıklama 0 verir ve geride kayıt bırakmaz.
- Yorumlayıcı açıklamayı saklar, sınamaz; açıklamanın bir tip olması bile gerekmez — dönüş açıklaması olarak yazılan bir sayı sözlükte olduğu gibi durur ve çağrı çalışır.
- Beş çağrının 4’ü ihlallidir — 2 tip ihlali, 2 alan ihlali — ve çalışma zamanında hata veren sayısı 0’dır. Bu ölçümde ihlalleri yakalayan katman yoktur.
- Bileşik açıklamaların dördünden üçü
isinstancesınamasına doğrudan verilemez; açıklama bir sınama değil, bir kayıttır ve kayıt bağlayıcı değildir. - Tipin doğru olması değerin geçerli olduğu anlamına gelmez: boş dizgi bir dizgidir, -5 bir tam sayıdır. Açıklama tipi bildirir, değeri değil.
Sonraki Adım
yas_yaz işlevinin açıklaması iki somut tip adı taşıyor ve bu yüzden yalnız o iki tiple
çalışan bir sözleşme bildiriyor. Oysa çok sayıda işlev tipe kayıtsızdır: bir listenin ilk
öğesini veren işlev listenin neyle dolu olduğunu umursamaz, ama döndürdüğü şeyin
listenin içindekiyle aynı tipte olduğunu bilir. Bu bağ somut bir tip adıyla yazılamaz —
yazılırsa ya bağ kaybolur ya da işlev gereksiz yere daraltılır. Sonraki ders aynı işlevin
dört ayrı yazımını yan yana koyuyor ve tek bir sayıyı soruyor: her yazımın açıklaması, bir
çağrının dönüş tipini kaç kez bildirebiliyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.