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
inthemfloatile 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üşümTypeErrorile 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 7ValueError. - 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öntemiNotImplementeddö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.