Ders 09 / 11
Genellikler ve Tip Değişkenleri
Aynı işlevin dört yazımı dört çağrı yerinde ayrı sonuç verir: açıklamasız yazım iki ihlali kaçırır, gevşek yazım ikisini bulur ama iki yanlış alarm üretir, genellikli yazım ikisini de bulur ve hiç yanlış alarm vermez.
İçindekiler
Önceki ders açıklamanın saklandığını ve sınanmadığını ölçtü; beş çağrının dördü ihlalliydi
ve çalışma zamanında hata veren sıfırdı. Ölçülen işlevin açıklaması iki somut tip adı
taşıyordu: ad: str, yas: int. Böyle bir imza yalnız o iki tiple çalışan bir sözleşme
bildirir ve bildirdiği şey açıktır.
Çok sayıda işlev ise tipe kayıtsızdır. Bir listenin ilk öğesini veren işlev listenin neyle dolu olduğunu umursamaz — ama umursamadığı hâlde bir şey bilir: döndürdüğü şey, listenin içindekiyle aynı tiptedir. Bu bağ somut bir tip adıyla yazılamaz. Bu dersin sorusu, o bağın yazılıp yazılamayacağı ve yazıldığında ne kazandırdığıdır. Ölçü yine kursun ekseninde: hangi yazım hangi ihlali bildirebiliyor?
Somut Tip Adının İki Ucu
ilk işlevini yazmanın iki kolay yolu var ve ikisi de bir şey kaybettiriyor.
Birinci yol açıklamayı gevşetmektir: def ilk(dizi: list) -> object. Bu imza her
listeyi kabul eder, doğrudur ve hiçbir çağrıyı gereksiz yere reddetmez. Kaybettiği şey
bağdır — dönüş için “bir nesne” demek, çağıranın elindeki değer hakkında hiçbir şey
söylememektir.
İkinci yol açıklamayı somutlaştırmaktır: def ilk(dizi: list[int]) -> int. Bu imza
dönüşü tam olarak bildirir. Kaybettiği şey işlevin kendisidir — gövde her listeyle
çalıştığı hâlde imza yalnız tam sayı listelerini kabul eder, ve dizgi listesiyle yapılan
geçerli bir çağrı da ihlal sayılır.
İkisinin ortası yok gibi görünüyor: ya bağı bırakacaksınız ya işlevi daraltacaksınız. Genellik tam olarak bu ikilemi ortadan kaldıran yazımdır.
Tip Değişkeni
Tip değişkeni (type variable), bir tip adının yerini tutan addır. T = TypeVar("T")
satırı böyle bir ad üretir; T kendisi bir tip değildir, imzada bir tipin duracağı yeri
gösterir.
from typing import TypeVar T = TypeVar("T") def ilk(dizi: list[T]) -> T: return dizi[0]
Bu imza iki şey söylüyor. Birincisi: argüman herhangi bir tiple dolu bir listedir — kabul
edilen küme daralmadı. İkincisi: dönüş, o listenin içindekiyle aynı tiptedir. Aynı adın
iki yerde geçmesi bağı kuran şeydir; T bir yerde ne ise öbüründe de odur.
Aynı adı taşıyan bir imzaya sahip yapıya genellik (generic) denir. Genellik bir işlevde
olabildiği gibi bir sınıfta da olabilir: Generic[T]’ten türeyen bir sınıf, taşıdığı
değerin tipini imzalarına yayabilir.
Tip değişkeni tamamen serbest bırakılmak zorunda değil. Bağlı bir tip değişkeni bir üst
sınır bildirir — TypeVar("Yazi", bound=str) yalnız dizgi ve alt sınıflarını kabul eder.
Sınırlı bir tip değişkeni ise sonlu bir seçenek listesi bildirir:
TypeVar("Sayisal", int, float). İkisinin de sınırı çalışma zamanında okunabilir; ölçüm bu
sınırların okunmakla sınanmak arasındaki farkı da gösteriyor.
Ölçümün Kurgusu
Ölçüm dört yazımı dört çağrı yerinde karşılaştırıyor. Her çağrı yeri iki şeyden oluşuyor:
ilk çağrısı ve onun sonucuna uygulanan bir işlem. İşlem sonuçtan bir tip bekler —
+ 1 bir tam sayı, .upper() bir dizgi. Çağrı yerinin ihlalli olması, listenin öğe
tipiyle işlemin beklediği tipin uyuşmamasıdır.
Ölçümün varsayımları:
- TP9 — Kâhin kurgunun kendisidir: dört çağrı yerinden hangisinin ihlalli olduğunu listenin öğe tipi ile işlemin gerektirdiği tip belirler; ikisini de dersin yazarı yazdı.
- TP10 — Dört çağrı yerinin ikisi ihlallidir. Küme küçük tutulmuştur, çünkü ölçülen şey ihlal oranı değil, her yazımın bildirebildiği ihlal kümesidir.
- TP11 — Çıkarım adımı ders içinde modellenmiştir: bir dönüş açıklamasından çağrı yerinin dönüş tipini üretir ve tip değişkenini argümandaki öğe tipiyle değiştirir. Tam denetleyici bu derste kurulmaz.
- TP12 — “Bildirilen”, yazımın o çağrı yerinde bir sorun bildirmesidir; iki nedenden biriyle olur: argüman açıklamaya uymaz, ya da çıkarılan dönüş tipi işlemin beklediğinden başkadır.
- TP13 — “Doğru”, bildirimin gerçekten ihlalli bir çağrı yerine düşmesidir. “Kaçırılan”, ihlalin bildirilmemesidir. “Yanlış alarm”, uygun bir çağrı yerinde bildirim yapılmasıdır.
- TP14 —
objectdönüşü hiçbir işlemin beklediği tiple aynı sayılmaz; gevşek yazımın dört çağrı yerinde de bildirim yapması bu kuralın sonucudur. - TP15 — Çalışma zamanı sütunu dört çağrı yerinin gerçekten çalıştırılmasıyla ölçülür ve yalnız tip kaynaklı istisnalar sayılır.
- TP16 — Genel sınıf ölçümünde kimlik
isile gösterilir; hiçbir kimlik sayısı basılmaz. - TP17 — Bağlı ve sınırlı tip değişkenlerinin ölçüsü, sınırın okunabilir olması ve sınırı bozan bir çağrının çalışıp çalışmamasıdır. İki argümanlı ölçümde birleştirme adımı da ders içinde modellenmiştir: tip değişkeni ilk eşleşmede bağlanır, sonraki eşleşmelerde bağın tutarlılığı sınanır.
Ölçüm
"""Genellikler: hangi yazim hangi ihlali bildirebiliyor.""" from typing import Generic, TypeVar T = TypeVar("T") def ilk(dizi): return dizi[0] YAZIMLAR = (("açıklamasız", None, None), ("list -> object", list, object), ("list[int] -> int", list[int], int), ("list[T] -> T", list[T], T)) # cagri yeri: kaynak listenin oge tipi, sonraki islemin gerektirdigi tip YERLER = (("ilk([1, 2]) + 1", int, int), ("ilk(['a', 'b']) + 1", str, int), ("ilk(['a', 'b']).upper()", str, str), ("ilk([1, 2]).upper()", int, str)) def kabul(param, oge): """Aciklama bu cagri yerinin argumanini kabul ediyor mu.""" ic = getattr(param, "__args__", (None,))[0] return param is None or param is list or ic is T or ic is oge def cikarim(donus, oge): """Donus aciklamasindan cagri yerinin donus tipi cikarilabiliyor mu.""" if donus is None: return None return oge if donus is T else donus print(f"{'yazım':<18s} {'çıkarılan':>10s} {'bildirilen':>11s} {'doğru':>6s}" f" {'kaçırılan':>10s} {'yanlış alarm':>13s}") for ad, param, donus in YAZIMLAR: cikan = bildirilen = dogru = kacirilan = yanlis = 0 for yer, oge, gereken in YERLER: gercek_ihlal = oge is not gereken c = cikarim(donus, oge) cikan += c is not None bildirim = (not kabul(param, oge)) or (c is not None and c is not gereken) bildirilen += bildirim dogru += bildirim and gercek_ihlal kacirilan += gercek_ihlal and not bildirim yanlis += bildirim and not gercek_ihlal print(f" {ad:<16s} {f'{cikan}/4':>10s} {bildirilen:11d} {dogru:6d}" f" {kacirilan:10d} {yanlis:13d}") GERCEK = (lambda: ilk([1, 2]) + 1, lambda: ilk(["a", "b"]) + 1, lambda: ilk(["a", "b"]).upper(), lambda: ilk([1, 2]).upper()) calisan = 0 for f in GERCEK: try: f() except (TypeError, AttributeError): calisan += 1 print(f"çağrı yeri {len(YERLER)}, gerçek ihlal " f"{sum(o is not g for _, o, g in YERLER)}, " f"çalışma zamanında hata veren {calisan}") def esle(a, b): return a if a == b else b CIFTLER = ((int, int), (int, str), (str, str)) IMZALAR = (("a: object, b: object", (object, object)), ("a: T, b: object", (T, object)), ("a: T, b: T", (T, T))) def birlestir(imza, tipler): """Tip degiskenini ilk eslesmede baglar, sonra tutarliligi sinar.""" bag = {} for aciklama, tip in zip(imza, tipler): if aciklama is T: if T in bag and bag[T] is not tip: return False bag[T] = tip elif aciklama is not object and aciklama is not tip: return False return True print() print(f"{'iki argümanlı imza':<24s} {'bildirilen':>11s}") for ad, imza in IMZALAR: print(f" {ad:<22s} " f"{sum(not birlestir(imza, c) for c in CIFTLER):11d}") print(f"çift {len(CIFTLER)}, tipleri ayrı olan " f"{sum(x is not y for x, y in CIFTLER)}, " f"esle(1, 'a') -> {esle(1, 'a')!r} (çalışma zamanı hata vermedi)") class Kutu(Generic[T]): def __init__(self, deger: T): self.deger = deger def al(self) -> T: return self.deger k = Kutu[int]("dizgi") print() print(f"Kutu[int]('dizgi').al() -> {k.al()!r}") print(f"type(k) is Kutu -> {type(k) is Kutu}; " f"Kutu[int] is Kutu[str] -> {Kutu[int] is Kutu[str]}") print(f"isinstance(k, Kutu) -> {isinstance(k, Kutu)}") try: isinstance(k, Kutu[int]) print("isinstance(k, Kutu[int]) -> sınandı") except TypeError: print("isinstance(k, Kutu[int]) -> sınanamaz") print(f"al açıklaması: {Kutu.al.__annotations__}") Yazi = TypeVar("Yazi", bound=str) Sayisal = TypeVar("Sayisal", int, float) print() print(f"bağlı tip değişkeni sınırı: {Yazi.__bound__}") print(f"sınırlı tip değişkeni seçenekleri: {Sayisal.__constraints__}") def buyut(d: Yazi) -> Yazi: return d print(f"buyut(7) -> {buyut(7)!r} (bağ çalışma zamanında sınanmadı)")
yazım çıkarılan bildirilen doğru kaçırılan yanlış alarm
açıklamasız 0/4 0 0 2 0
list -> object 4/4 4 2 0 2
list[int] -> int 4/4 3 2 0 1
list[T] -> T 4/4 2 2 0 0
çağrı yeri 4, gerçek ihlal 2, çalışma zamanında hata veren 2
iki argümanlı imza bildirilen
a: object, b: object 0
a: T, b: object 0
a: T, b: T 1
çift 3, tipleri ayrı olan 1, esle(1, 'a') -> 'a' (çalışma zamanı hata vermedi)
Kutu[int]('dizgi').al() -> 'dizgi'
type(k) is Kutu -> True; Kutu[int] is Kutu[str] -> False
isinstance(k, Kutu) -> True
isinstance(k, Kutu[int]) -> sınanamaz
al açıklaması: {'return': ~T}
bağlı tip değişkeni sınırı: <class 'str'>
sınırlı tip değişkeni seçenekleri: (<class 'int'>, <class 'float'>)
buyut(7) -> 7 (bağ çalışma zamanında sınanmadı)
Dört Yazım, Dört Sonuç
Üst tablonun son üç sütunu birbirinden bağımsız üç kusuru sayıyor: kaçırılan ihlal, yanlış alarm ve çıkarılamayan dönüş tipi. Dört yazımdan yalnız biri üçünü birden sıfırlıyor.
Açıklamasız yazım hiçbir dönüş tipi çıkarmıyor: 0/4. Bildirim sayısı 0, kaçırılan 2. Yanlış alarm da 0 — hiçbir şey söylemeyen bir imza yanılamaz da. Bu satır bir ölçüt: aşağıdaki üç yazımın kazandırdığı şey bu satırdan uzaklaşmalarıdır.
Gevşek yazım dört çağrı yerinin dördünde de bir dönüş tipi çıkarıyor: 4/4. Ama
çıkardığı tip her seferinde object ve hiçbir işlem object beklemiyor. Bildirim 4,
doğru 2, yanlış alarm 2. Her çağrı yerinde bildirim yapan bir yazım, hiçbir çağrı
yerini ötekinden ayırmıyor demektir. Kaçırılan 0’a indi, karşılığında yanlış alarm
0’dan 2’ye çıktı.
Aşırı somut yazım üç bildirim yapıyor: 2 doğru, 1 yanlış alarm. Yanlış alarm dizgi listesiyle yapılan geçerli çağrıdan geliyor — imza o çağrıyı argüman düzeyinde reddediyor, oysa gövde onunla sorunsuz çalışır. Doğru bildirimlerden biri de aynı yerden geliyor, yani doğru sonucu yanlış gerekçeyle veriyor: ihlal dönüş tarafındayken imza argümanı suçluyor.
Genellikli yazım dört çağrı yerinin dördünde dönüş tipini çıkarıyor, 2 bildirim yapıyor, ikisi de doğru, kaçırılan 0, yanlış alarm 0. Kaçırılanı gevşek yazım gibi sıfırlıyor, yanlış alarmı açıklamasız yazım gibi sıfırlıyor. Tek satırda ikisi birden.
Kazancın kaynağı tek bir yerde: bağ. Öbür üç yazım argümanla dönüş arasında bir ilişki kurmuyor, her birine ayrı ayrı bir tip yazıyor ya da hiç yazmıyor. Genellikli yazım tek bir adı iki yerde kullanarak ilişkiyi bildiriyor ve çıkarım o ilişkiden çağrı yerine özel bir yanıt üretebiliyor.
Aynı Adın İki Argümanda Kullanılması
Bağın gücü dönüşle sınırlı değil. İkinci tablo iki argümanlı bir işlevi ölçüyor: iki değeri
karşılaştırıp birini döndüren esle. Sözleşme “iki argüman aynı tipte olmalı”
diyecekse, bunu yazmanın tek yolu aynı tip değişkenini iki parametrede kullanmaktır.
Ölçüm bunu bir birleştirme adımıyla modelliyor: tip değişkeni ilk eşleşmede bir tipe bağlanır, sonraki parametrede aynı değişken görülürse bağın tutup tutmadığına bakılır.
Üç imza, üç çift. object ile yazılmış imza 0 bildirim yapıyor — tipleri ayrı olan
çift var ama imza onları ayırt edecek hiçbir şey söylemiyor. a: T, b: object de 0
veriyor: tip değişkeni tek yerde geçtiğinde bağ kurulmaz, çünkü bağ iki uç ister.
a: T, b: T ise 1 bildirim yapıyor ve bu, kümedeki tek gerçek uyuşmazlıktır.
Son satır kim yakaladı sorusunu yine yanıtlıyor: esle(1, "a") çağrısı hata vermiyor, bir
değer döndürüyor. Çalışma zamanı bu uyuşmazlığı görmüyor; gören tek şey imzadaki tekrar
eden addır.
Bu Ölçümde Kim Yakaladı
Bildirimleri yapan katman açıklamayı okuyan taraftır ve bu derste onun yalnız çıkarım adımı modellendi; tam denetleyici bir sonraki dersin konusu.
Çalışma zamanı satırı ayrı bir sonuç veriyor: dört çağrı yerinde 2 hata çıkıyor, yani
gerçek ihlallerin ikisi de yakalanıyor. Bu, önceki dersin 0 sonucuyla çelişmiyor — orada gövde her tipi
kabul eden bir biçimlemeydi, burada gövdenin dışındaki işlem tipi gerçekten kullanıyor.
Fark üç noktada duruyor. Çalışma zamanı hatayı çağrıdan sonra, ilk çoktan döndükten
sonra veriyor. Yalnız çalıştırılan satırlarda veriyor; dört çağrı yerinden biri bir
koşulun içinde kalsaydı ve o koşul sağlanmasaydı, o ihlal görünmezdi. Ve hatayı veren şey
açıklama değil, işlemin kendisi — açıklama silinseydi aynı iki hata aynı yerlerde çıkardı.
Genellikli yazımın 2 bildirimi ise hiçbir şey çalıştırmadan, çağrı yerine bakılarak üretiliyor. İki katmanın yakaladığı ihlaller burada aynı; yakalama anları ayrı.
Genel Sınıf Çalışma Zamanında Ne Olur
Alt bloklar genelliğin çalışma zamanındaki karşılığını ölçüyor ve sonuç önceki dersin savını doğruluyor.
Kutu[int]("dizgi") bir tip ihlalidir: kutunun tam sayı taşıması bildirilmiş, içine dizgi
konmuştur. Çağrı çalışıyor, al() dizgiyi geri veriyor, hata yok. type(k) is Kutu
doğru — üretilen nesnenin sınıfı Kutu’dur; köşeli parantezdeki tip nesnenin sınıfına
işlenmez. Kutu[int] is Kutu[str] yanlış: ikisi ayrı nesnelerdir, ama ikisi de aynı
sınıfın örneklerini üretir.
isinstance(k, Kutu) doğru veriyor; isinstance(k, Kutu[int]) ise sınanamıyor.
Önceki dersteki list[int] sonucunun aynısı: parametreli bir açıklama çalışma zamanı
sınamasına doğrudan verilemez. al yönteminin açıklaması da tip değişkeninin kendisini
taşıyor — çözülmüş bir tip değil, çözülecek yerin adını.
Son iki satır bağlı ve sınırlı tip değişkenlerini ölçüyor. Sınırlar okunabiliyor: bağlı
değişkenin üst sınırı str, sınırlı değişkenin seçenekleri int ve float. Ama sınırı
bozan buyut(7) çağrısı çalışıyor ve 7 döndürüyor. Bağ da açıklamadır: kaydedilir,
okunur, sınanmaz.
Özet
- Tip değişkeni bir tip adının yerini tutan addır; aynı adın imzada iki kez geçmesi argüman ile dönüş arasında bir bağ kurar ve genellik bu bağı taşıyan yazımdır.
- Dört yazım dört çağrı yerinde ayrışıyor: açıklamasız yazım 0 bildirim ve 2 kaçırılan, gevşek yazım 4 bildirim ve 2 yanlış alarm, aşırı somut yazım 3 bildirim ve 1 yanlış alarm veriyor.
- Genellikli yazım 2 bildirim yapıyor, ikisi de doğru; kaçırılan ve yanlış alarm 0’dır. Kazancın kaynağı bağdır, tip adlarının somutluğu değil.
- Bağ iki uç ister: aynı tip değişkeni iki parametrede geçtiğinde uyuşmazlık bildirilir
(1), tek parametrede geçtiğinde ya da
objectyazıldığında bildirilmez (0). - Bağlı tip değişkeni bir üst sınır, sınırlı tip değişkeni sonlu bir seçenek kümesi bildirir; ikisinin de sınırı çalışma zamanında okunabilir ama sınanmaz.
- Genel bir sınıfın köşeli parantezdeki tipi örneğin sınıfına işlenmez:
type(k) is Kutudoğrudur,Kutu[int]ile yapılan bir örnek sınaması ise yapılamaz.
Sonraki Adım
Bu derste açıklamayı okuyan tarafın yalnız bir adımı modellendi: bir çağrı yerinin dönüş tipini çıkarmak. Gerçek bir statik denetleyici bundan fazlasını yapar — bütün çağrıları tarar, her birinin argümanlarını imzayla karşılaştırır ve uyuşmayanları çalıştırmadan bildirir. Sonraki ders o denetleyiciyi ders içinde modelliyor ve önceki dersin beş çağrısına uyguluyor. Sorulan sayı şudur: dört ihlalin kaçını bulur, kaçını bulamaz — ve bulamadıklarını neden bulamaz?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.