İçeriğe geç
academia.sh

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.
  • TP14object dö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 is ile 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 object yazı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 Kutu doğ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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat