İçeriğe geç
academia.sh

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ığı is ile 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 is ile 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 is ile 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 TypeError verir; 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; find bulamadığında -1 döndürür, istisna fırlatmaz.
  • Dizgi bir dizidir: dilim __getitem__ çağırır ve yeni nesne verir, öğeleri karakterdir, in sınaması ise listeden farklı olarak alt dizgi arar.
  • Yedi parçayı artırmalı birleştirmek 7 ara nesne kurar, join 1 kurar; fark join’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__; !r gibi 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat