İçeriğe geç
academia.sh

Ders 07 / 16

Koşullar

Koşul bir mantıksal değer beklemez, nesneyi doğruluk kuralına sokar: on iki sınamada sekiz protokol çağrısı ölçülür, dördü hiçbir yöntem çağırmadan doğru döner ve karşılaştırma zinciri orta terimi bir kez üretir.

İçindekiler

Önceki ders tip dönüşümünü ölçtü ve dönüşümün başarısız olduğu yerde bir istisna doğduğunu gösterdi: nesne kendisinden istenen tipe karşılık veremediğinde soru yanıtsız kalmaz, akış kesilir. Dönüşüm nesneye “sen bir tam sayı olarak neye karşılık geliyorsun?” diye sorar ve bu sorunun yanıtsız kalma hakkı vardır.

Koşul ifadesi de nesneye bir soru sorar, ama başka bir soru: “sen doğru mu sayılıyorsun?” Bu sorunun yanıtı, dönüşümdekinin tersine, neredeyse hiçbir zaman istisna olmaz — nesne yanıtı kendisi vermezse dil onun yerine bir yanıt üretir. Peki o yanıtı kim veriyor? if deyimi nesnenin içine bakıp öğe mi sayıyor, tipine mi bakıyor, yoksa nesneye mi soruyor? Bu dersin ölçtüğü şey budur.

Koşul Bir Mantıksal Değer Beklemez

Programlama Temelleri kursunun koşullu dallanma dersi koşulu mantıksal değer üreten bir ifade olarak tanımlamıştı; o tanım dilden bağımsızdı ve dallanmayı bir denetim yapısı olarak kurdu. Python’da tanım daha geniştir: if anahtar sözcüğünün arkasına herhangi bir nesne yazılabilir ve dil onu bir doğruluk değerine kendisi indirger. Burada ölçülen şey dallanmanın kendisi değil, o indirgemenin hangi özel yöntemi çağırdığıdır.

İndirgeme kuralına doğruluk kuralı (truthiness) denir ve üç basamaklıdır:

  1. Nesnede __bool__ tanımlıysa o çağrılır; döndürdüğü değer sonucun kendisidir.
  2. Tanımlı değilse __len__ çağrılır; sonuç sıfırdan büyükse nesne doğru sayılır.
  3. İkisi de tanımlı değilse hiçbir yöntem çağrılmaz ve nesne her zaman doğrudur.

Üçüncü basamak kuralın en çok gözden kaçan yeridir. Boş bir nesnenin yanlış sayılması bir dil kuralı değil, nesnenin kendi bildirimidir: bunu bildiren bir yöntem yoksa boşluk diye bir kavram da yoktur. Sıradan bir nesne, içi ne olursa olsun, doğrudur.

Zincir Bir Kısaltmadır

Karşılaştırma işleçleri Python’da zincirlenebilir: a < b < c geçerli bir ifadedir. Bu yazımın anlamı soldan sağa ikili karşılaştırma değildir — öyle olsaydı a < b bir doğruluk değeri üretir, o değer c ile karşılaştırılırdı ve sonuç anlamsız çıkardı.

Zincirin anlamı a < b and b < c ifadesidir; iki farkla. Birincisi, orta terim b bir kez değerlendirilir. Açık yazımda b iki kez yazılır ve bir işlev çağrısıysa iki kez çalışır. İkincisi, and gibi zincir de kısa devre yapar: ilk karşılaştırma yanlış çıkarsa ikincisi hiç kurulmaz, sağdaki terim üretilmez bile.

Zincirleme kuralı yalnız < ve > için değil, bütün karşılaştırma işleçleri için geçerlidir; üyelik ve kimlik işleçleri de aynı sınıftadır. Bu yüzden a < b <= c kadar a is b is c de geçerli bir zincirdir ve aynı iki güvenceyi taşır. Zincirin uzunluğunda bir sınır yoktur: her ek terim, bir öncekiyle kendi karşılaştırmasını kurar ve soldakilerin hepsi doğru çıkmadıkça sıraya hiç gelmez.

İkisi de gözle görülmez. Terimlerin kaç kez üretildiğini anlamanın tek yolu, üretimi sayan bir kurgu kurmaktır.

Kısa Devrenin Döndürdüğü

and ve or işleçleri bir mantıksal değer döndürmez; işlenenlerden birini döndürür. x or y ifadesi x doğruysa x’i, değilse y’yi verir. Döndürülen şey nesnenin kendisidir, onun doğruluk değeri değil.

Bu ikisinin çağırdığı protokol de ayrıdır: and ve or, işlenenler üzerinde bir aritmetik işleç gibi kendi özel yöntemini çağırmaz. Yaptıkları tek şey sol işleneni doğruluk kuralına sokmak ve sonuca göre hangi işleneni döndüreceğine karar vermektir. Sağ işlenen, gerekiyorsa döndürülür — sınanmaz.

Ölçümün varsayımları:

  • PA1 — Kâhin kurgunun kendisidir: hangi özel yöntemin çağrıldığını nesne kendi kaydettiği için bilinir; ölçüm dışarıdan bir gözlem değil, nesnenin kendi bildirimidir.
  • PA2 — Üç sınıf kullanılır. Izci yalnız __len__ tanımlar, Bosluksuz ona __bool__ ekler, Sessiz ikisini de tanımlamaz. Üç sınıf doğruluk kuralının üç basamağına birebir karşılık gelir.
  • PA3 — Her sınıf boş ve üç öğeli iki durumda sınanır; öğe sayısı sonucu değil, sonucu kimin belirlediğini ayırmak için değişir.
  • PA4 — Zincirin orta terimi bir işlev çağrısıyla üretilir ve her üretim kaydedilir; “kaç kez değerlendirildi” sorusunun ölçüsü bu kayıttır.
  • PA5__lt__ uzunluğa göre karşılaştırır. Karşılaştırmanın neye göre yapıldığı ölçüme girmez; ölçülen şey kaç kez çağrıldığıdır.
  • PA6Izci yalnız __lt__ tanımlar, karşıtı __gt__ tanımsız bırakılır; yansımanın ölçülebilmesi için tanımsızlık kasıtlıdır.
  • PA7 — Sayılan çağrılar yalnız bu dersin kurduğu biçimlerden gelir; ortak tanımın diğer biçimleri bu ölçüme dahil değildir.

Ölçüm

"""Dogruluk kurali: if n hangi ozel yontemi cagirir, hangi sirayla."""

KAYIT = []


def kaydet(ad):
    KAYIT.append(ad)


class Izci:
    """__len__ tanimli, __bool__ tanimsiz."""

    def __init__(self, ogeler=(1, 2, 3)):
        self.ogeler = list(ogeler)

    def __len__(self):
        kaydet("__len__")
        return len(self.ogeler)

    def __lt__(self, o):
        kaydet("__lt__")
        return len(self.ogeler) < len(o.ogeler)


class Bosluksuz(Izci):
    """__bool__ tanimli: dogruluk sinamasi __len__'e dusmez."""

    def __bool__(self):
        kaydet("__bool__")
        return len(self.ogeler) > 0


class Sessiz:
    """Ne __bool__ ne __len__ tanimli."""

    def __init__(self, ogeler=()):
        self.ogeler = list(ogeler)


def olc(islev):
    KAYIT.clear()
    try:
        deger = islev()
    except Exception as e:
        KAYIT.append(f"!{type(e).__name__}")
        deger = None
    return list(KAYIT), deger


NESNELER = (
    ("Izci(3 oge)", lambda: Izci((1, 2, 3))),
    ("Izci(0 oge)", lambda: Izci(())),
    ("Bosluksuz(3 oge)", lambda: Bosluksuz((1, 2, 3))),
    ("Bosluksuz(0 oge)", lambda: Bosluksuz(())),
    ("Sessiz(3 oge)", lambda: Sessiz((1, 2, 3))),
    ("Sessiz(0 oge)", lambda: Sessiz(())),
)

print(f"{'nesne':<18s} {'if n':>6s} {'not n':>6s}  çağrılan protokol")
toplam = sinama = sessiz = 0
for ad, kur in NESNELER:
    n = kur()
    c1, d1 = olc(lambda: bool(n))
    c2, d2 = olc(lambda: not n)
    toplam += len(c1) + len(c2)
    sinama += 2
    sessiz += (len(c1) == 0) + (len(c2) == 0)
    print(f"  {ad:<16s} {str(d1):>6s} {str(d2):>6s}  {' '.join(c1) or '(yöntem yok)'}")
print(f"\nsınama {sinama}, çağrı {toplam}, hiçbir yöntemi çağırmayan {sessiz}")

URETIM = []


def uret(ad, n):
    URETIM.append(ad)
    return Izci(range(n))


def zincir():
    return uret("a", 1) < uret("b", 2) < uret("c", 3)


def acilim():
    return (uret("a", 1) < uret("b", 2)) and (uret("b", 2) < uret("c", 3))


def kisa_devre():
    return uret("a", 3) < uret("b", 2) < uret("c", 9)


print()
print(f"{'biçim':<16s} {'sonuç':>6s} {'__lt__':>7s} {'üretim':>7s}  sıra")
for ad, f in (("a<b<c", zincir), ("a<b and b<c", acilim),
              ("a<b<c  (yanlış)", kisa_devre)):
    URETIM.clear()
    c, d = olc(f)
    print(f"  {ad:<14s} {str(d):>6s} {c.count('__lt__'):7d}"
          f" {len(URETIM):7d}  {' '.join(URETIM)}")

print()
print(f"{'karşılaştırma':<24s} {'sonuç':>6s}  çağrılan protokol")
for ad, f in (("Izci(1) < Izci(2)", lambda: Izci((1,)) < Izci((1, 2))),
              ("Izci(2) > Izci(1)", lambda: Izci((1, 2)) > Izci((1,))),
              ("Sessiz(1) < Sessiz(2)", lambda: Sessiz((1,)) < Sessiz((1, 2)))):
    c, d = olc(f)
    print(f"  {ad:<22s} {str(d):>6s}  {' '.join(c)}")

print()
print(f"{'ifade':<26s} {'çağrı':>6s}  {'döndürülen tip':<16s} protokol")
BOS, DOLU = Izci(()), Bosluksuz((1, 2))
for ad, f in (("Izci(0) or Bosluksuz(2)", lambda: BOS or DOLU),
              ("Bosluksuz(2) or Izci(0)", lambda: DOLU or BOS),
              ("Izci(0) and Bosluksuz(2)", lambda: BOS and DOLU),
              ("bool(Izci(0) or Izci(0))", lambda: bool(Izci(()) or Izci(())))):
    c, d = olc(f)
    print(f"  {ad:<24s} {len(c):6d}  {type(d).__name__:<16s} {' '.join(c)}")
nesne                if n  not n  çağrılan protokol
  Izci(3 oge)        True  False  __len__
  Izci(0 oge)       False   True  __len__
  Bosluksuz(3 oge)   True  False  __bool__
  Bosluksuz(0 oge)  False   True  __bool__
  Sessiz(3 oge)      True  False  (yöntem yok)
  Sessiz(0 oge)      True  False  (yöntem yok)

sınama 12, çağrı 8, hiçbir yöntemi çağırmayan 4

biçim             sonuç  __lt__  üretim  sıra
  a<b<c            True       2       3  a b c
  a<b and b<c      True       2       4  a b b c
  a<b<c  (yanlış)  False       1       2  a b

karşılaştırma             sonuç  çağrılan protokol
  Izci(1) < Izci(2)        True  __lt__
  Izci(2) > Izci(1)        True  __lt__
  Sessiz(1) < Sessiz(2)    None  !TypeError

ifade                       çağrı  döndürülen tip   protokol
  Izci(0) or Bosluksuz(2)       1  Bosluksuz        __len__
  Bosluksuz(2) or Izci(0)       1  Bosluksuz        __bool__
  Izci(0) and Bosluksuz(2)      1  Izci             __len__
  bool(Izci(0) or Izci(0))      2  bool             __len__ __len__

Üç Basamağın Bedeli

Üst tablo doğruluk kuralının üç basamağını yan yana koyuyor. On iki sınamada sekiz çağrı var; aradaki dört sınama hiçbir yöntemi çağırmadan sonuç üretiyor.

İlk iki satır ikinci basamak: Izci __bool__ tanımlamadığı için sınama __len__’e düşüyor ve sonucu öğe sayısı belirliyor. Üçüncü ve dördüncü satır birinci basamak: aynı sınıfa __bool__ eklendiği anda __len__ hiç çağrılmıyor. İkisi de tanımlıyken hangi yöntemin çağrılacağı sorusu yok — __bool__ varsa __len__ sınamaya katılmaz.

Son iki satır üçüncü basamak ve tablonun asıl sonucu burada. Sessiz(0 oge) boş, ama sınama doğru veriyor. Üstelik bunu bir yöntem çağırarak değil, hiç çağırmayarak yapıyor. Nesnenin üç öğesi mi var, sıfır mı — sonucu değiştirmiyor, çünkü sonucu üretecek bir yöntem yok. Bu satırın pratik karşılığı şudur: kendi yazdığı bir sınıfın boş örneğini if ile sınayan bir program, __len__ ya da __bool__ yazmayı unutmuşsa her zaman doğru dalını alır ve bunu bildiren hiçbir belirti çıkmaz.

not n sütunu aynı zincirin bir kez daha yürüdüğünü gösteriyor: olumsuzlama ayrı bir protokol değil, aynı sınamanın sonucunun tersidir. Sütunlardaki tek fark Sessiz satırlarında görülüyor — not bir nesne değil, gerçek bir mantıksal değer döndürüyor.

Zincir Kaç Terim Üretiyor

Orta tablo zincirin kısaltma olduğunu sayıyla gösteriyor. a<b<c ile açık yazımın __lt__ sayısı aynı: ikisi de 2. Karşılaştırma sayısında bir kazanç yok.

Fark üretim sütununda. Zincir üç terim üretiyor, açık yazım dört: b iki kez üretiliyor. Bu, okunabilirlik değil anlam farkıdır. Orta terim bir işlev çağrısıysa o işlev iki kez çalışır; yan etkisi varsa yan etki iki kez oluşur; her çağrıda başka bir değer döndürüyorsa iki karşılaştırma birbirinden habersiz iki ayrı değeri sınar. Açık yazım, zincirin verdiği güvenceyi vermez.

Üçüncü satır kısa devreyi ölçüyor. İlk karşılaştırma yanlış çıktığı için __lt__ 1 kez çağrılıyor ve üretim 2’de duruyor: c hiç kurulmuyor. Zincirin sağ ucundaki terim, sola bakılmadan üretilmeyecek bir şeydir.

Ters Yönde Bulunan Protokol

Üçüncü tablo karşılaştırmanın kendi düşme kuralını gösteriyor ve ortadaki satır dersin en sert sonucudur. Izci sınıfı __lt__ tanımlar, __gt__ tanımlamaz. Buna rağmen Izci(2) > Izci(1) çalışıyor — ve çağrılan yöntem __lt__.

Kural şudur: bir karşılaştırmada sol işlenenin yöntemi bulunamazsa dil sağ işlenenin karşıt yöntemini dener. a > b için önce a üzerinde __gt__ aranır; yoksa b üzerinde __lt__ çağrılır ve işlenenler yer değiştirir. Sonuç doğru çıkar, çünkü “b, a’dan küçüktür” ile “a, b’den büyüktür” aynı iddiadır.

Bunun sonucu doğrudan ikinci iddiadır: yazılan işleç, çağrılan yöntemin adını söylemez. İki satır sonucu True veriyor, ikisi de aynı yöntemi çağırıyor, ama biri < biri > yazıyor. Sözdizime bakarak hangi yöntemin çalıştığını kestirmek olanaksızdır; bunu yalnız ölçüm söyler.

Üçüncü satır kuralın sonunu gösteriyor. Sessiz ne __lt__ ne __gt__ tanımlar; ne sol işlenende ne sağ işlenende bir yöntem bulunuyor ve karşılaştırma bir sonuç değil, TypeError üretiyor. Doğruluk sınamasının üçüncü basamağı sessizce doğru dönerken, karşılaştırmanın son basamağı istisnayla biter: bir sözdizim için varsayılan davranış var, öbürü için yok. Protokolü olmayan nesne, o sözdizimi hiç taşımaz.

Koşul Deyimi ile Koşul İfadesi

Doğruluk kuralı yalnız if deyiminde çalışmaz. Aynı indirgeme while koşulunda, and ve or işleçlerinde, not işlecinde ve koşul ifadesinde de yürür.

Koşul ifadesi, önceki konunun deyim ile ifade ayrımını kullanır: if bir deyimdir, değer üretmez ve bir atamanın sağına yazılamaz. Bunun ifade karşılığı a if k else b yazımıdır ve bir değer üretir — dolayısıyla bir atamanın sağına, bir argümanın yerine ya da bir listenin içine konabilir.

İkisi arasındaki fark yalnız yazımdadır; doğruluk kuralı ikisinde de aynı üç basamağı yürütür ve k yerine yazılan nesne aynı ölçümden geçer. Kısa devre de korunur: koşul ifadesinde a ile b dallarından yalnız biri değerlendirilir. Bu, koşul ifadesini and ve or zincirlerinden ayıran şeydir — orada döndürülen değer işlenenlerden biriydi, burada seçilen dalın kendisidir ve seçilmeyen dal hiç kurulmaz.

Döndürülen Nesnenin Tipi

Alt tablo and ile or işleçlerinin ne döndürdüğünü gösteriyor ve üç satırının üçünde de dönen şey bir mantıksal değer değil.

İlk satır: sol işlenen Izci(0) boş, sınama __len__ ile yanlış çıkıyor, or sağ işleneni döndürüyor — dönen tip Bosluksuz. İkinci satır: sol işlenen bu kez doğru, __bool__ ile sınanıyor ve or sol işleneni döndürüyor. Üçüncü satırda and tersini yapıyor: sol işlenen yanlış olduğu için sonuç doğrudan o oluyor, tip Izci.

Üç satırın da çağrı sayısı 1. Sağ işlenen hiçbir zaman sınanmıyor; yalnız döndürülüyor. Bunun sonucu, or ile yazılan varsayılan değer kalıbının Python’da tam olarak ne yaptığıdır: sol işlenen doğruluk kuralından yanlış çıkarsa sağdaki verilir — boş olduğu için değil, yanlış sayıldığı için.

Son satır ikisini ayırıyor. bool() yerleşiği araya girdiğinde dönen tip gerçekten bool oluyor ve çağrı sayısı 2’ye çıkıyor: bir kez or sol işleneni sınıyor, bir kez de bool() sonucu sınıyor. Aynı ifadeyi mantıksal değere çevirmenin bedeli fazladan bir protokol çağrısıdır.

Özet

  • Python’da koşul mantıksal değer beklemez; if arkasındaki her nesne doğruluk kuralından geçirilir ve ölçülen şey bu indirgemenin çağırdığı özel yöntemdir.
  • Kural üç basamaklıdır: __bool__ varsa o çağrılır, yoksa __len__’e düşülür, o da yoksa hiçbir yöntem çağrılmadan nesne doğru sayılır — on iki sınamanın dördü bu basamakta ve boş nesne bile doğru döner.
  • __bool__ tanımlıyken __len__ sınamaya hiç katılmaz; iki yöntem yarışmaz, ilki ikincisini devre dışı bırakır.
  • Karşılaştırma zinciri a<b<c ile açık yazım aynı sayıda __lt__ çağırır (2), ama zincir orta terimi bir kez üretir, açık yazım iki kez; zincir yanlış çıktığında sağ terim hiç üretilmez.
  • Karşılaştırmanın kendi düşme kuralı vardır: sol işlenende yöntem yoksa sağ işlenenin karşıtı çağrılır — > yazılan bir ifade __lt__ çalıştırır; ikisi de yoksa sonuç bir değer değil, TypeError olur.
  • and ile or bir mantıksal değer değil, işlenenlerden birini döndürür; yalnız sol işleneni doğruluk kuralına sokarlar, sağ işleneni sınamazlar.

Sonraki Adım

Doğruluk kuralı tek bir soruyu bir kez sorar ve yanıtı bir dalla biter. Döngü ise aynı soruyu değil, başka bir soruyu tekrar tekrar sorar: sırada bir öğe var mı? Programlama Temelleri kursunda döngü bir denetim yapısıydı; kaç tur döneceği koşula ya da koleksiyonun uzunluğuna bağlıydı. Python’da döngü bir protokoldür ve turların bitmesine karar veren şey bir koşul değildir. Sonraki ders üç öğelik bir nesnede döngünün kaç kez __next__ çağırdığını sayar — ve o sayının neden öğe sayısından bir fazla olduğunu.

İ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