İçeriğe geç
academia.sh

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.
  • TP2Tip 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 isinstance sı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 üçü isinstance sı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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat