Ders 05 / 16
Dizgilerle Çalışma
Dizgi değiştirilemez olduğu için her üretim yöntemi yeni bir nesne döndürür: yedi parçayı `+` ile birleştirmek 7 ara nesne kurar, `join` 1 kurar ve sekiz biçimlendirme yazımı 3 ayrı özel yöntem çağırır.
İçindekiler
Önceki ders + işlecinin __add__, += yazımının __iadd__ çağırdığını ölçtü ve
değiştirilemez tiplerin __iadd__ tanımlayamayacağını söyledi. Dizgi tam olarak böyle bir
tiptir; ondaki her işlem bu tek kısıttan türer.
Bu dersin sorusu dizgi yöntemlerinin listesi değildir. Soru şudur: bir tipin değiştirilemez olması, üzerinde tanımlanabilecek yöntemleri nasıl zorlar, ve bu zorlama sayı olarak ne kadar tutar?
Yerinde Yazma Diye Bir Şey Yok
Bir dizginin bir karakterini değiştirmenin yolu yoktur. Sözdizim vardır — köşeli ayraçla yazma — ama karşılığı olan yöntem tanımlı değildir.
- DT31 — Ölçümde tek bir başlangıç dizgisi kullanılır ve üzerinde art arda dört işlem
yapılır; her adımda sonucun bir önceki nesneyle aynı nesne olup olmadığı
isile sorulur. Hiçbir kimlik sayısı basılmaz. - DT32 — Seçilen dört işlem içeriği gerçekten değiştirir. Sonucu girdisiyle birebir aynı
olan bir işlemin (örneğin boşluksuz bir dizgide
strip) aynı nesneyi döndürüp döndürmemesi gerçekleştirime bağlıdır ve ölçüme sokulmamıştır. - DT33 — İkinci tablodaki yöntemler dizgiyi dönüştürmez, ona bir soru sorar; ölçülen şey dönen değerin tipidir.
"""Dizgi degistirilemez: uretim yontemleri yeni nesne, sorgu yontemleri baska tip dondurur.""" HAM = " 12; kuzey; 18 " try: HAM[0] = "x" except TypeError as e: print("dizgiye yerinde yazma:", type(e).__name__) ADIMLAR = (("başlangıç", lambda s: s), ("strip()", lambda s: s.strip()), ("upper()", lambda s: s.upper()), ("replace(';', ',')", lambda s: s.replace(";", ",")), ("split(',')", lambda s: s.split(","))) print(f"\n{'adım':<20s} {'tip':<5s} {'önceki nesne mi':>15s} değer") onceki = HAM for ad, islem in ADIMLAR: simdiki = islem(onceki) print(f"{ad:<20s} {type(simdiki).__name__:<5s} " f"{str(simdiki is onceki):>15s} {simdiki!r}") onceki = simdiki print("başlangıç dizgisi ölçümden sonra:", repr(HAM)) METIN = "12; kuzey; 18" SORGULAR = (("startswith('12')", lambda s: s.startswith("12")), ("find('kuzey')", lambda s: s.find("kuzey")), ("count(';')", lambda s: s.count(";")), ("isdigit()", lambda s: s.isdigit()), ("len(s)", lambda s: len(s))) print(f"\n{'sorgu yöntemi':<20s} {'dönen tip':<5s} değer") for ad, islev in SORGULAR: deger = islev(METIN) print(f"{ad:<20s} {type(deger).__name__:<5s} {deger!r}") print("sorgulardan sonra dizgi:", repr(METIN))
dizgiye yerinde yazma: TypeError
adım tip önceki nesne mi değer
başlangıç str True ' 12; kuzey; 18 '
strip() str False '12; kuzey; 18'
upper() str False '12; KUZEY; 18'
replace(';', ',') str False '12, KUZEY, 18'
split(',') list False ['12', ' KUZEY', ' 18']
başlangıç dizgisi ölçümden sonra: ' 12; kuzey; 18 '
sorgu yöntemi dönen tip değer
startswith('12') bool True
find('kuzey') int 4
count(';') int 2
isdigit() bool False
len(s) int 13
sorgulardan sonra dizgi: '12; kuzey; 18'
İlk satır kısıtı adıyla koyuyor: köşeli ayraçla yazma TypeError veriyor, çünkü dizgi
tipinde bu işlemi karşılayan yöntem yok. Önceki derste bölme işleci için alınan sonucun
aynısıdır — kusur bir yasak değil, bir yokluktur.
Zincirdeki dört adımın dördünde de “önceki nesne mi” sütunu False. Her yöntem yeni bir
nesne kurdu ve girdisine dokunmadı; son satır bunu doğruluyor — dört işlemden sonra
başlangıç dizgisi olduğu gibi duruyor. Bunun bir seçim olmadığını görmek önemlidir. Bir
dizgi yöntemi girdisini değiştirmek isteseydi bile yapamazdı; tek yapabileceği şey yeni
bir nesne kurup döndürmektir. Değiştirilemezlik, yöntemin sözleşmesini kendisi yazar; bu
yüzden dizgi tipinde listedeki append ya da sort gibi yerinde çalışan bir yöntem
bulunmaz.
Buradan doğrudan bir kural çıkar: dizgi yöntemlerinin dönüş değeri kullanılmazsa hiçbir şey
olmamıştır. metin.strip() satırı tek başına yazıldığında yeni bir nesne kurar, ada
bağlamaz ve atar; metin eskisi gibi kalır. Bu, dile yeni gelen kodda en sık görülen sessiz
kusurdur.
Alt tablo yöntemlerin ikinci sınıfını ayırıyor. startswith, find, count, isdigit ve
len bir dizgi üretmez; sırasıyla bir doğruluk değeri, bir konum, bir sayım verir. Bunlar
dizgiyi dönüştürmez, ona bir soru sorar — dolayısıyla yeni nesne tartışmasının dışındadır.
find yöntemi aranan parçanın konumunu, bulamazsa -1 döndürür; bir istisna fırlatmaz.
Aynı işi yapan index yöntemi ise bulamadığında ValueError fırlatır. İki yöntem arasındaki
seçim üslup değildir: bulunamamak beklenen bir durumsa dönüş değeriyle çalışan yazım, olmaması
gereken bir durumsa istisna fırlatan yazım seçilir. Aynı ayrımın dönüşüm tarafındaki
karşılığı sonraki dersin konusudur.
Dizgi Bir Dizidir
Üçüncü dersin yerleşik tip tablosunda str beş sütunun beşini de doldurmuştu: uzunluğu
sorulabiliyor, üzerinde döngü kurulabiliyor, üyelik sınanabiliyor, dilimlenebiliyor ve
toplanabiliyordu. Bu, dizginin yalnız bir metin değil, aynı zamanda bir dizi olduğu
anlamına gelir — ve dizi olmasının iki gözlenebilir sonucu vardır.
- DT34 — Dilim ölçümünde her sonuç kaynak dizgiyle
isile karşılaştırılır; kaynağın tamamını isteyen bir dilim kullanılmaz, çünkü onun aynı nesneyi döndürüp döndürmemesi gerçekleştirime bağlıdır. - DT35 — Kaçış ölçümünde yazılan üç yazımın karakter sayısı okunur; sayı, yazımın kaç karakter ürettiğini gösterir, kaynakta kaç karakter yazıldığını değil.
"""Dizgi bir dizidir: dilim, yineleme ve uyelik ayni protokollere dayanir.""" METIN = "12; kuzey; 18" DILIMLER = (("[0]", lambda s: s[0]), ("[-1]", lambda s: s[-1]), ("[4:9]", lambda s: s[4:9]), ("[:2]", lambda s: s[:2]), ("[::-1]", lambda s: s[::-1]), ("[::4]", lambda s: s[::4])) print(f"{'dilim':<8s} {'kaynakla aynı nesne mi':>22s} değer") for ad, islev in DILIMLER: parca = islev(METIN) print(f"{ad:<8s} {str(parca is METIN):>22s} {parca!r}") print(f"\nuzunluk {len(METIN)}, ilk dört öğe {list(METIN)[:4]}") print("üyelik:", "'kuz' in metin ->", "kuz" in METIN, "| 'zey' in metin ->", "zey" in METIN, "| 'yez' in metin ->", "yez" in METIN) print("listede aynı soru:", "[1, 2] in [1, 2, 3] ->", [1, 2] in [1, 2, 3], "| 2 in [1, 2, 3] ->", 2 in [1, 2, 3]) KACIS = (("'a\\tb'", "a\tb"), ("r'a\\tb'", r"a\tb"), ("'''iki\\nsatır'''", "iki\nsatır")) print(f"\n{'yazım':<18s} {'karakter':>8s} gerçek içerik") for ad, deger in KACIS: print(f"{ad:<18s} {len(deger):8d} {deger!r}")
dilim kaynakla aynı nesne mi değer [0] False '1' [-1] False '8' [4:9] False 'kuzey' [:2] False '12' [::-1] False '81 ;yezuk ;21' [::4] False '1ky8' uzunluk 13, ilk dört öğe ['1', '2', ';', ' '] üyelik: 'kuz' in metin -> True | 'zey' in metin -> True | 'yez' in metin -> False listede aynı soru: [1, 2] in [1, 2, 3] -> False | 2 in [1, 2, 3] -> True yazım karakter gerçek içerik 'a\tb' 3 'a\tb' r'a\tb' 4 'a\\tb' '''iki\nsatır''' 9 'iki\nsatır'
Altı dilimin altısı da kaynaktan ayrı bir nesne veriyor. Dilimleme ilk derste ölçülen
__getitem__ çağrısıdır ve dizgide de aynı kuralı taşır: yeni bir nesne kurulur, kaynak
olduğu gibi kalır. Eksi konum sondan sayar, üçüncü sayı adımı belirler; [::-1] yazımının
metni ters çevirmesi ayrı bir yöntemin işi değil, adımın eksi olmasının sonucudur.
Yineleme satırı dizginin öğelerinin karakter olduğunu gösteriyor: on üç uzunluğundaki metin on üç öğe verir ve her öğe yine bir dizgidir. Python’da tek bir karakteri temsil eden ayrı bir tip yoktur; bir karakter, uzunluğu bir olan dizgidir.
Üyelik satırı ise dizgiyi öteki dizilerden ayırıyor. "kuz" in metin True veriyor —
oysa aranan şey bir öğe değil, bir öğe dizisi. Aynı soru listede sorulduğunda
[1, 2] in [1, 2, 3] False verir, çünkü listede in yalnız öğe arar. Dizginin
__contains__ yöntemi bilerek başka türlü yazılmıştır: alt dizgi arar. Sözdizim iki tipte
aynıdır, sözleşme değildir.
Son tablo yazım biçimlerini ayırıyor. Ters bölülü kaçış dizileri kaynakta iki karakterle
yazılır ama tek karakter üretir: 'a\tb' yazımı üç karakterlik bir dizgidir. Başına
r konan ham yazımda kaçış çözülmez ve aynı kaynak dört karakter verir. Üç tırnaklı yazım
ise satır sonunu olduğu gibi taşır. Üçünde de kaynakta yazılan karakter sayısı ile üretilen
dizginin uzunluğu ayrıdır; dosya yolu ya da örüntü metni yazarken ham yazımın tercih edilme
nedeni budur.
Birleştirmenin Bedeli
Değiştirilemezliğin ölçülebilir bedeli, parça parça metin kurarken ortaya çıkar. Her +
işlemi yeni bir nesne kurduğu için, bir döngü içinde büyütülen dizgi yol boyunca kurulup
atılan ara nesneler bırakır.
- DT36 — İki yöntem aynı yedi parçadan aynı metni kurar; ölçülen şey yol boyunca
kurulan ara nesne sayısıdır, geçen süre değil. Artırmalı yöntemde her adımın sonucu
isile bir öncekiyle karşılaştırılarak sayılır; birleştirme tek nesne kurduğu için sayısı doğrudan yazılır.
"""Birlestirmenin bedeli: kac ara nesne kuruluyor?""" PARCALAR = ("kuzey", "; ", "12", "; ", "yamac", "; ", "18") def artirmali(parcalar): metin = "" ara = 0 for p in parcalar: onceki = metin metin = metin + p # her adimda yeni bir dizgi ara += metin is not onceki return metin, ara def birlestirerek(parcalar): metin = "".join(parcalar) # tek adimda tek nesne return metin, 1 print(f"{'yöntem':<14s} {'parça':>5s} {'kurulan ara nesne':>18s} sonuç") for ad, islev in (("+ ile artırma", artirmali), ("join ile", birlestirerek)): sonuc, ara = islev(PARCALAR) print(f"{ad:<14s} {len(PARCALAR):5d} {ara:18d} {sonuc!r}") print("iki sonuç eşit mi:", artirmali(PARCALAR)[0] == birlestirerek(PARCALAR)[0])
yöntem parça kurulan ara nesne sonuç + ile artırma 7 7 'kuzey; 12; yamac; 18' join ile 7 1 'kuzey; 12; yamac; 18' iki sonuç eşit mi: True
Yedi parça, artırmalı yöntemde 7 ara nesne kuruyor; join 1 kuruyor. Sonuçlar
birebir eşit. Örüntü parça sayısıyla doğrusal büyür: parça sayısı ikiye katlandığında
artırmalı yöntemin kurduğu nesne de ikiye katlanır, join’inki 1’de kalır.
Farkın nedeni yöntemin daha iyi yazılmış olması değil, bilgiye önceden sahip olmasıdır.
join bütün parçaları başta görür, toplam uzunluğu hesaplayabilir ve sonucu tek seferde
kurar. + işleci ise her çağrıda yalnız iki parça görür; sonraki adımda ne geleceğini
bilemediği için o adımın sonucunu tam olarak kurmak zorundadır.
Aynı gerekçe, listeye toplayıp sonunda birleştirmenin neden yaygın bir kalıp olduğunu da açıklar: liste değiştirilebilir bir tiptir, ona ekleme yapmak yeni nesne kurmaz. Metin yalnız en sonda, bir kez üretilir.
join yönteminin bir koşulu vardır ve gözden kaçmaya elverişlidir: parçaların hepsi
dizgi olmak zorundadır. Aralarına bir sayı karışırsa yöntem onu kendiliğinden metne
çevirmez, TypeError fırlatır. Yöntemin çağrıldığı dizgi de parçalardan biri değil,
ayraçtır: "; ".join(...) yazımında araya giren metin, çağrının yapıldığı dizgidir.
Biçimlendirme Hangi Protokolü Çağırır
Metin üretmenin yazımları birden çoktur ve aynı görünürler. Kursun ölçüsü burada da işler: her yazımın çağırdığı yöntem ayrıdır ve ürettikleri metin bu yüzden ayrışabilir.
- DT37 — Ölçülen nesne üç metin protokolüne de katılır:
__str__,__repr__ve__format__. Üçü ayrı metin üretir, böylece hangisinin çağrıldığı çıktıdan da okunabilir. - DT38 —
__format__yönteminin kendisi hiçbir başka protokolü çağırmaz; kalıp boşsa kendi metnini kurar, doluysa kalıbı taşıdığı sayıya uygular. Böylece kayıt yalnız yazımın çağırdığını gösterir.
"""Bicimlendirme yazimlari hangi ozel yontemi cagiriyor?""" KAYIT = [] def kaydet(ad): KAYIT.append(ad) class Olcum: """Uc metin protokolune de katilir ve her katilimini kaydeder.""" def __init__(self, deger=12): self.deger = deger def __str__(self): kaydet("__str__") return f"olcum {self.deger}" def __repr__(self): kaydet("__repr__") return f"Olcum({self.deger})" def __format__(self, kalip): kaydet("__format__") return format(self.deger, kalip) if kalip else f"olcum {self.deger}" BICIMLER = ( ('f"{n}"', lambda n: f"{n}"), ('f"{n:>6}"', lambda n: f"{n:>6}"), ('f"{n!s}"', lambda n: f"{n!s}"), ('f"{n!r}"', lambda n: f"{n!r}"), ('"{}".format(n)', lambda n: "{}".format(n)), ('"%s" % n', lambda n: "%s" % n), ('str(n)', lambda n: str(n)), ('repr(n)', lambda n: repr(n)), ) tekil = set() print(f"{'yazım':<16s} {'protokol':<12s} ürettiği metin") for ad, islev in BICIMLER: KAYIT.clear() metin = islev(Olcum()) tekil.update(KAYIT) print(f"{ad:<16s} {' '.join(KAYIT):<12s} {metin!r}") print(f"\n{len(BICIMLER)} yazım, ayrı protokol {len(tekil)}: " f"{', '.join(sorted(tekil))}")
yazım protokol ürettiği metin
f"{n}" __format__ 'olcum 12'
f"{n:>6}" __format__ ' 12'
f"{n!s}" __str__ 'olcum 12'
f"{n!r}" __repr__ 'Olcum(12)'
"{}".format(n) __format__ 'olcum 12'
"%s" % n __str__ 'olcum 12'
str(n) __str__ 'olcum 12'
repr(n) __repr__ 'Olcum(12)'
8 yazım, ayrı protokol 3: __format__, __repr__, __str__
Sekiz yazım 3 ayrı protokole dağılıyor. Süslü ayraçlı yazım ile format yöntemi
__format__ çağırıyor; yüzde işaretli yazım ile str yerleşiği __str__ çağırıyor;
repr yerleşiği ile !r işaretçisi __repr__ çağırıyor. Aynı nesne üç yazımda üç ayrı
metin veriyor ve hiçbiri ötekinin yerine geçmiyor.
İki nokta ayrıca okunmalı. Birincisi, __format__ kalıbı da alır: ikinci satırda
>6 kalıbı yönteme geçmiş ve yöntem onu kendi taşıdığı sayıya uygulamıştır. Hizalama,
basamak sayısı ve genişlik kuralları bu yolla nesnenin kendisine bırakılır. İkincisi,
işaretçiler bir kısayol değil, protokol seçicidir: !r yazımı çağrılacak yöntemi
__format__’tan __repr__’a çevirir.
Süslü ayraçlı yazımın ayrı bir yanı daha var: ayraçların içine bir değer değil, bir
ifade yazılabilir ve ifade orada değerlendirilir. İkinci dersin deyim–ifade ayrımı bu
yüzden burada da işler; ayraç içine bir atama konamaz, çünkü atama bir ifade değildir.
format yöntemi ise değerleri argüman olarak alır ve kalıbı ayrı tutar.
Üç yöntemin sözleşmeleri de ayrıdır. __str__ okunabilir metin verir; __repr__ nesneyi
tanıtan, çoğunlukla onu yeniden kuracak biçimde yazılmış metin verir — ölçümdeki
'Olcum(12)' bunun örneğidir. Bir nesne yalnız __repr__ tanımlarsa str çağrısı da ona
düşer; ilk derste çıplak nesnede görülen davranışın kaynağı budur.
Özet
- Dizginin bir karakterine yazmak
TypeErrorverir; karşılığı olan yöntem tanımlı değildir. - Üretim yöntemlerinin dördünde de sonuç yeni bir nesnedir ve girdi olduğu gibi kalır; dönüş değeri kullanılmazsa hiçbir şey olmamıştır.
- Sorgu yöntemleri dizgi üretmez, başka tipte bir yanıt verir;
findbulamadığında-1döndürür, istisna fırlatmaz. - Dizgi bir dizidir: dilim
__getitem__çağırır ve yeni nesne verir, öğeleri karakterdir,insınaması ise listeden farklı olarak alt dizgi arar. - Yedi parçayı artırmalı birleştirmek 7 ara nesne kurar,
join1 kurar; farkjoin’in bütün parçaları başta görmesinden gelir. - Sekiz biçimlendirme yazımı 3 ayrı protokole dağılır:
__format__,__str__ve__repr__;!rgibi işaretçiler çağrılacak yöntemi değiştirir.
Sonraki Adım
Bu derste str(n) yazımının __str__ çağırdığı ölçüldü — yani bir nesneyi metne çevirmek
de bir protokol çağrısıymış. Aynı soru ters yönde de sorulabilir: int("12") yazıldığında
ne çağrılıyor, float(n) bir nesneden neyi istiyor ve int("kuzey") gibi karşılığı olmayan
bir dönüşüm istendiğinde ne oluyor? Sonraki ders dönüşümün hangi özel yöntemlere dayandığını
sayar ve başarısız dönüşümün bir dönüş değeri değil, bir istisna ürettiğini gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.