İçeriğe geç
academia.sh

Ders 06 / 16

Tip Dönüşümü

Dönüşüm yerleşikleri sekiz yazımda ayrı özel yöntemler çağırır ve karşılığı olmayan iki yazım düşer; yirmi dizgi denemesinde 7 `ValueError` ile 4 `TypeError` ayrılır ve sayısal yükseltmenin sihir olmadığı `NotImplemented` ile gösterilir.

İçindekiler

Önceki ders str(n) yazımının __str__ çağırdığını ölçtü: bir nesneyi metne çevirmek de bir protokol çağrısıymış. Aynı soru ters yönde de sorulabilir. int(n) yazıldığında ne çağrılıyor, float(n) nesneden neyi istiyor ve karşılığı olmayan bir dönüşüm istendiğinde ne oluyor?

Programlama Temelleri kursu açık ve örtük dönüşümü, kayıplı daraltmayı ve dış girdiyi dönüştürmeden önce doğrulamayı zaten kurmuştu; gerçel sayıdan tam sayıya geçişin yuvarlamayıp kestiği de orada gösterildi. Bunlar tekrarlanmaz. Burada ölçülen şey dönüşümün düzeneğidir: hangi yerleşik hangi adı arıyor, aradığını bulamazsa hangi istisnayı fırlatıyor ve iki ayrı sayısal tip toplandığında dilin yaptığı şey gerçekten bir “dönüşüm” mü?

Dönüşüm de Bir Protokol Çağrısıdır

int, float, str ve list birer yerleşik işlevdir ve hepsi aynı biçimde çalışır: verilen nesneden belli adı taşıyan bir yöntem isterler. Ad yoksa dönüşüm de yoktur.

  • DT39 — Ölçülen nesne dört dönüşüm protokolüne birden katılır ve her katılımını kaydeder; kayıttaki ünlem önekli öğe bir çağrıyı değil, bir istisnayı gösterir.
  • DT40 — İki karşı örnek kullanılır: hiçbir dönüşüm yöntemi tanımlamayan bir nesne ve yalnız __int__ tanımlayıp dizin protokolünü taşımayan bir nesne.
  • DT41 — Dizinleme ölçümünde beş harfli sabit bir dizgi kullanılır; dönen harf, nesnenin dizin olarak verdiği sayıya bağlıdır.
"""Donusum yerlesikleri hangi ozel yontemi cagiriyor?"""

KAYIT = []


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


class Olcum:
    """Donusum protokollerine katilir ve her katilimini kaydeder."""

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

    def __int__(self):
        kaydet("__int__")
        return sum(self.ogeler)

    def __float__(self):
        kaydet("__float__")
        return float(sum(self.ogeler))

    def __str__(self):
        kaydet("__str__")
        return f"olcum {self.ogeler}"

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

    def __iter__(self):
        kaydet("__iter__")
        self._i = 0
        return self

    def __next__(self):
        kaydet("__next__")
        if self._i >= len(self.ogeler):
            raise StopIteration
        deger = self.ogeler[self._i]
        self._i += 1
        return deger


class Sade:
    """Hicbir donusum yontemi tanimlamaz."""


class YalnizTamSayi:
    """Yalniz __int__ tanimlar; dizin protokolunu tasimaz."""

    def __int__(self):
        kaydet("__int__")
        return 2


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


BICIMLER = (
    ("int(n)", lambda: int(Olcum())),
    ("float(n)", lambda: float(Olcum())),
    ("str(n)", lambda: str(Olcum())),
    ("list(n)", lambda: list(Olcum())),
    ("'kuzey'[n]", lambda: "kuzey"[Olcum((1, 2))]),
    ("int(Sade())", lambda: int(Sade())),
    ("'kuzey'[m]", lambda: "kuzey"[YalnizTamSayi()]),
    ("int(m)", lambda: int(YalnizTamSayi())),
)

print(f"{'yazım':<14s} {'çağrı':>5s} {'protokol':<32s} sonuç")
for ad, islev in BICIMLER:
    c, sonuc = olc(islev)
    print(f"{ad:<14s} {len(c):5d} {' '.join(c):<32s} {sonuc}")
yazım          çağrı protokol                         sonuç
int(n)             1 __int__                          6
float(n)           1 __float__                        6.0
str(n)             1 __str__                          'olcum [1, 2, 3]'
list(n)            5 __iter__ __next__ __next__ __next__ __next__ [1, 2, 3]
'kuzey'[n]         1 __index__                        'z'
int(Sade())        1 !TypeError                       —
'kuzey'[m]         1 !TypeError                       —
int(m)             1 __int__                          2

İlk üç satır kuralı kuruyor: int yerleşiği __int__, float yerleşiği __float__, str yerleşiği __str__ çağırıyor. Yerleşiğin adı ile yöntemin adı arasındaki karşılık tesadüf değil, sözleşmedir. Dönüşümü yapan şey yerleşik değildir — yerleşik yalnız kimin yapacağını bilir; işi nesnenin kendisi yapar ve sonucu döndürür.

Dördüncü satır bir dönüşümü daha ayırıyor. list(n) bir __list__ yöntemi aramaz; yineleme protokolünü kullanır ve öğeleri tek tek toplayarak yeni bir liste kurar. Üç öğe için 5 çağrı ödeniyor: bir __iter__ ve dört __next__ — ilk dersteki “öğe sayısından bir fazla” örüntüsünün aynısı. Buradan genel kural çıkar: bir koleksiyona dönüşüm ayrı bir protokol istemez, var olanın üzerine kurulur. Aynı gerekçeyle tuple(n) ve set(n) de yeni bir yöntem aramaz; üçü de aynı yineleme protokolünü tüketir ve yalnız kurdukları kabın tipiyle ayrılırlar.

Beşinci satır ile yedinci satır birlikte okunmalı. Bir nesnenin dizin olarak kullanılması __int__ değil, __index__ ister. İkisi ayrı sözleşmelerdir: __int__ “bu nesne bir tam sayıya çevrilebilir” demektir, __index__ “bu nesne zaten bir tam sayı konumdur” demektir. Ayrım keyfi değildir — bir gerçel sayı __int__ taşır ama dizin olamaz, çünkü dizinlemede kesme yapmak sessiz bir kusur olurdu. Yalnız __int__ tanımlayan nesne son iki satırda tam olarak bunu gösteriyor: int(m) çalışıyor, 'kuzey'[m] TypeError ile düşüyor.

Altıncı satır ise ilk dersteki sonucun aynısı. Hiçbir dönüşüm yöntemi tanımlamayan nesnede int çağrısı TypeError fırlatıyor. Sekiz yazımın 6’sı bir yöntem buluyor, 2’si bulamıyor ve düşüyor. Dönüşüm bir hak değil, bir yöntem meselesidir.

Başarısız Dönüşüm Bir İstisnadır

Yukarıdaki tablo nesneden sayıya dönüşümü ölçtü. Dış dünyadan gelen veri ise dizgidir ve orada durum ayrıdır: str tipi __int__ tanımlamaz. int("12") yazıldığında yapılan şey bir protokol çağrısı değil, metnin çözümlenmesidir — ve çözümleme başarısız olabilir.

  • DT42 — On girdi denenir: sekizi dizgi, ikisi başka tiptendir. Her girdi hem int hem float ile denenir; ölçülen şey dönen değer ya da istisnanın türüdür.
  • DT43 — Sayım yalnız istisnaları toplar; başarılı dönüşümler sayıma girmez.
"""Basarisiz donusum: hangi girdi hangi istisnayi uretir?"""

GIRDILER = ("12", " 12 ", "+12", "1_2", "12.5", "", "kuzey", "0x1f", None, [1])


def dene(islev, girdi):
    try:
        return repr(islev(girdi)), "—"
    except Exception as e:
        return "—", type(e).__name__


print(f"{'girdi':<10s} {'int(...)':>10s} {'istisna':<12s}"
      f" {'float(...)':>12s} {'istisna':<12s}")
sayim = {}
for g in GIRDILER:
    i_deger, i_hata = dene(int, g)
    f_deger, f_hata = dene(float, g)
    for h in (i_hata, f_hata):
        if h != "—":
            sayim[h] = sayim.get(h, 0) + 1
    print(f"{repr(g):<10s} {i_deger:>10s} {i_hata:<12s}"
          f" {f_deger:>12s} {f_hata:<12s}")

print(f"\ntoplam deneme {2 * len(GIRDILER)}, "
      + ", ".join(f"{h} {n}" for h, n in sorted(sayim.items())))
print("taban verilirse:", "int('0x1f', 16) ->", int("0x1f", 16))
girdi        int(...) istisna        float(...) istisna     
'12'               12 —                    12.0 —           
' 12 '             12 —                    12.0 —           
'+12'              12 —                    12.0 —           
'1_2'              12 —                    12.0 —           
'12.5'              — ValueError           12.5 —           
''                  — ValueError              — ValueError  
'kuzey'             — ValueError              — ValueError  
'0x1f'              — ValueError              — ValueError  
None                — TypeError               — TypeError   
[1]                 — TypeError               — TypeError   

toplam deneme 20, TypeError 4, ValueError 7
taban verilirse: int('0x1f', 16) -> 31

Yirmi denemenin 11’i istisnayla bitiyor ve istisnalar iki türe ayrılıyor: 7 ValueError, 4 TypeError. Ayrım rastgele değildir ve öğrenilmesi gereken şey tam olarak budur.

TypeError girdinin tipi dönüştürülemediğinde doğar. None ve liste satırlarında sorun değerin ne olduğu değil, o tipin sayıya çevrilecek bir yöntem taşımamasıdır — birinci tablonun Sade satırıyla aynı kusur sınıfıdır. ValueError ise tip doğru ama değer uygun olmadığında doğar. 'kuzey' bir dizgidir ve dizgi sayıya çevrilebilir; çevrilemeyen şey o dizginin taşıdığı metindir.

Kabul edilen girdiler de öğreticidir. Baştaki ve sondaki boşluk atılır, artı işareti kabul edilir, alt çizgi basamak ayracı olarak okunur. Ama '12.5' dizgisi int için geçersiz, float için geçerlidir: int bir tam sayı gösterimi bekler ve kesirli metni sessizce kesmez. Önceki kursta ölçülen kesme davranışı gerçel sayı nesnesinden tam sayıya geçişte olur, dizgiden geçişte olmaz. '0x1f' satırı da aynı sınıftandır; on altılık gösterim ancak taban açıkça verildiğinde okunur ve son satır bunu gösteriyor.

Buradan çıkan kural, önceki dersin find ile index ayrımının aynısıdır: başarısız dönüşüm bir dönüş değeri değil, bir istisna üretir. Hiçbir yerleşik “çeviremedim” diyen özel bir sayı döndürmez; dönmüş olsaydı o değer gerçek bir sonuçtan ayırt edilemezdi. Sessiz bir kusur yerine gürültülü bir istisna seçilmiştir.

Tablonun kabul sütunu bir uyarı daha taşıyor. Programlama Temelleri kursu dönüşümden önce biçimi sınamayı iki kalıptan biri olarak vermişti; ölçüm o kalıbın sınırını gösteriyor. Yalnız basamaklardan oluşmayı sınayan bir ön denetim ' 12 ' ve '+12' girdilerini reddeder — oysa ikisi de sorunsuz çevriliyor. Ön denetimin kabul ettiği küme ile dönüşümün kabul ettiği küme aynı değildir, ve iki kümeyi elde tutarlı kılmak dönüşüm kurallarını ikinci kez yazmak demektir. Denemek ve düşen dönüşümü yakalamak bu yüzden daha dar bir sözleşmedir: kabul kuralını tek bir yerde, dönüşümün kendisinde bırakır.

Dönüşüm Tersinir mi

Başarısız dönüşüm gürültü çıkarır ve bulunur. Asıl tehlike başarılı görünen dönüşümdedir: bir değer çevrilir, hiçbir istisna doğmaz ve geri çevrildiğinde eskisi çıkmaz. Programlama Temelleri kursu bu kaybı gösterdi; burada ölçülen şey kaybın sınırıdır — hangi yol güvenceli, hangisi değil.

  • DT44 — Beş tam sayı iki ayrı yoldan gidip döner: dizgi üzerinden ve gerçel sayı üzerinden. Ölçülen şey dönen değerin başlangıçtakine eşit olup olmadığıdır.
  • DT45 — Gerçel sayı ölçümünde dönüş yolu nesneyi tanıtan metin üzerinden kurulur; seçilen dört değer büyüklük ve kesir bakımından ayrı uçlardandır.
"""Donusum tersinir mi: gidip donunce ayni deger mi?"""

TAM = (12, -3, 2 ** 53, 2 ** 53 + 1, 10 ** 23)
GERCEL = (0.1, 3.5, 1 / 3, 1e300)

print(f"{'tam sayı':<26s} {'dizgi üzerinden':>16s} {'gerçel üzerinden':>17s}")
for n in TAM:
    print(f"{n!s:<26s} {str(int(str(n)) == n):>16s} "
          f"{str(int(float(n)) == n):>17s}")

print(f"\n{'gerçel sayı':<26s} {'dizgi üzerinden':>16s}")
for x in GERCEL:
    print(f"{x!r:<26s} {str(float(repr(x)) == x):>16s}")
tam sayı                    dizgi üzerinden  gerçel üzerinden
12                                     True              True
-3                                     True              True
9007199254740992                       True              True
9007199254740993                       True             False
100000000000000000000000               True             False

gerçel sayı                 dizgi üzerinden
0.1                                    True
3.5                                    True
0.3333333333333333                     True
1e+300                                 True

Dizgi sütunu beş satırda da True. Metin üzerinden gidip dönmek tam sayıda her zaman tersinirdir, çünkü üçüncü derste ölçüldüğü gibi tam sayının sabit bir genişliği yoktur: basamak sayısı ne olursa olsun metin de o kadar uzar. Aynı güvence gerçel sayı için de geçerlidir — alt tablonun dördü de True. Bir gerçel sayıyı tanıtan metin, o sayıyı geri verecek kadar basamak taşıyacak biçimde üretilir.

Üst tablonun gerçel üzerinden sütunu ise ikiye ayrılıyor: ilk üç değer sorunsuz dönüyor, son ikisi dönmüyor. Sınır tabloda görünüyor: iki üzeri elli üç sorunsuz, onun bir fazlası değil. Nedeni, gerçel sayı gösteriminin taşıyabileceği anlamlı basamak sayısının sınırlı olmasıdır; o sınırın üstünde bitişik iki tam sayı aynı gerçel sayıya düşer ve geri dönüşte ikisi ayırt edilemez. Bu, Bilgisayarlar Nasıl Çalışır kursunda kurulan gösterim sınırının Python tarafındaki karşılığıdır. Tabloda eşitlik sınamasının False çıkması, dönüşümün bir istisna fırlatmadığını da gösteriyor: iki dönüşüm de başarıyla tamamlandı, yalnız sonuç başlangıçtaki değer değildi.

Pratik sonuç tektir: bir tam sayı değeri korunacaksa gerçel sayıya uğratılmaz. Ne bir istisna doğar ne bir uyarı çıkar; yalnız bir eşitlik sınaması yanlış çıkar. Bu ders boyunca ölçülen istisnalar bu sınıfın tam tersidir — istisna, kusuru sesli hâle getirir.

Sayısal Yükseltme Sihir Değildir

Geriye örtük dönüşüm kalıyor. 1 + 2.5 yazıldığında tam sayı gerçel sayıya “yükseltilir” ve sonuç gerçel çıkar. Bu, dilin sayılar için ayrı bir kural taşıdığı izlenimini verir. Ölçüm bunun doğru olmadığını gösteriyor.

  • DT46 — İşlecin arkasındaki yöntemler doğrudan çağrılır; ölçülen şey dönen değerdir. Yöntem bir sonuç üretemediğinde döndürdüğü şey bir istisna değil, özel bir belirteçtir.
  • DT47 — Dört toplama denenir; ikisinde iki ayrı sayısal tip, ikisinde bir sayı ile bir dizgi karşılaşır.
"""Sayisal yukseltme sihir degil, isleclerdeki dusustur."""

print("(1).__add__(2.5) ->", (1).__add__(2.5))
print("(2.5).__radd__(1) ->", (2.5).__radd__(1))
print("1 + 2.5 ->", 1 + 2.5, "| tipi", type(1 + 2.5).__name__)

print()
print(f"{'ifade':<14s} {'sonuç':>8s}  istisna")
for sol, sag in (("3", 4), (3, "4"), (3, 2.5), (True, 1)):
    try:
        sonuc, hata = repr(sol + sag), "—"
    except TypeError as e:
        sonuc, hata = "—", type(e).__name__
    print(f"{repr(sol) + ' + ' + repr(sag):<14s} {sonuc:>8s}  {hata}")
(1).__add__(2.5) -> NotImplemented
(2.5).__radd__(1) -> 3.5
1 + 2.5 -> 3.5 | tipi float

ifade             sonuç  istisna
'3' + 4               —  TypeError
3 + '4'               —  TypeError
3 + 2.5             5.5  —
True + 1              2  —

İlk satır belirleyici. Tam sayının __add__ yöntemi bir gerçel sayı aldığında bir sonuç değil, NotImplemented döndürüyor — “bu işi ben yapamam” anlamına gelen özel bir değer. Bunun üzerine, dördüncü derste ölçülen düşüş işler: sağdaki işlenenin ters yöndeki yöntemi denenir ve ikinci satırda görüldüğü gibi gerçel sayı işi üstlenir. Üçüncü satır sonucun gerçel çıkmasının nedenini veriyor — hesabı yapan taraf gerçel sayı tipidir.

Yani “yükseltme” diye ayrı bir düzenek yoktur. Sayısal tipler birbirinin ters yöndeki yöntemlerini karşıladıkları için birlikte çalışırlar; bu, dördüncü dersteki __rmul__ düşüşünün aynısıdır. Alt tablo sınırı da çiziyor: dizgi ile sayı hiçbir yönde birbirinin yöntemini karşılamaz, dolayısıyla iki yazım da TypeError verir. Kimin solda kimin sağda durduğu sonucu değiştirmiyor. Dizgi ile sayıyı toplamak isteyen kod, dönüşümü kendisi yazmak zorundadır; dilin bu boşluğu kendiliğinden doldurmaması bir eksiklik değil, sessiz sonuç üretmemek için verilmiş bir karardır.

Son satır bir Python ayrıntısını kapatıyor. True + 1 ifadesi 2 veriyor, çünkü üçüncü derste ölçüldüğü gibi doğruluk değerleri tam sayının bir alt tipidir. Burada bir dönüşüm yapılmaz; nesne zaten bir tam sayıdır.

Özet

  • Dönüşüm yerleşikleri adı belli özel yöntemler çağırır: int __int__, float __float__, str __str__; yöntem yoksa dönüşüm TypeError ile düşer.
  • list(n) ayrı bir yöntem istemez, yineleme protokolünü kullanır ve üç öğe için 5 çağrı öder.
  • Dizin olarak kullanılmak __int__ değil __index__ ister; ilki çevrilebilirliği, ikincisi nesnenin zaten bir konum olduğunu bildirir.
  • Yirmi dizgi denemesinin 11’i istisnayla biter: tip çevrilemiyorsa 4 TypeError, tip doğru ama değer uygunsuzsa 7 ValueError.
  • Başarısız dönüşüm özel bir dönüş değeri değil, bir istisna üretir; aksi hâlde o değer gerçek bir sonuçtan ayırt edilemezdi. Sessiz olan kusur, gerçel sayıya uğrayıp geri dönen büyük tam sayıdadır.
  • Sayısal yükseltme ayrı bir kural değildir: tam sayının __add__ yöntemi NotImplemented döndürür ve işi gerçel sayının ters yöndeki yöntemi üstlenir.

Sonraki Adım

Bu ders bir konuyu kapatırken bir soruyu açık bıraktı. Dönüşüm başarısız olduğunda ortaya bir istisna çıkıyor ve istisna, akışı bulunduğu yerden koparıp başka bir yere taşıyor — yani bir kusur bildirimi değil, bir akış aracı. Sonraki konu akışın kendisini ele alacak ve ilk dersi şu soruyla açacak: bir koşul yazıldığında sınanan şey nedir? if satırına bir sayı, bir dizgi ya da boş bir liste konabilir; hepsi kabul edilir. Peki bir nesnenin doğru mu yanlış mı sayıldığına kim karar veriyor — ve bu kararı veren yöntem yoksa ne oluyor?

İ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