İçeriğe geç
academia.sh

Ders 03 / 16

Değişkenler ve Veri Tipleri

Ad tip taşımaz, nesne taşır: tek bir ad yedi ayrı tipe bildirimsiz bağlanır, `+=` yazımı dört tipin üçünde adı yeniden bağlayıp birinde nesneyi değiştirir ve eşit iki değerin aynı nesne çıkması gerçekleştirime bağlıdır.

İçindekiler

Önceki ders girintinin blokları, blokların da deyimleri taşıdığını gösterdi ve en sık yazılan deyim olan atama tabloda yalnızca “ifade değil” olduğu için göründü. Bu ders o deyimin ne yaptığına bakar.

Programlama Temelleri kursu atamanın bir bağlama olduğunu, değişkenin ya adlandırılmış bir bellek gözü ya da nesneye takılmış bir etiket olarak modellenebileceğini, is ile == arasındaki ayrımı ve takma adı zaten kurmuştu. Bunlar tekrarlanmaz. Orada kurulan şey bir kavramdı: bir dil iki modelden birini seçer. Burada ölçülen şey Python’un seçtiği modelin gözlenebilir sonuçlarıdır — adın hiç tip taşımaması, += yazımının tipe göre bağ mı kurduğu yoksa nesneyi mi değiştirdiği, ve eşit iki değerin aynı nesne çıkıp çıkmamasının neye dayandığı.

Ad Tip Taşımaz

Python’da her değer bir nesnedir ve tip nesnenin özelliğidir. Ad bir bildirim değil, yalnızca bir bağdır; ne bir tip ilan eder ne de bir tipe kilitlenir. type yerleşiği bu yüzden nesneye sorulur, ada değil.

  • DT15 — Yedi ayrı tipten birer değer tek bir ada sırayla bağlanır; kaynakta hiçbir tip bildirimi yoktur. Ölçülen şey her bağlamadan sonra nesnenin bildirdiği tip adıdır.
  • DT16 — Son bağlama bir işlevedir; işlevin de bağlanabilir bir nesne olduğu böylece aynı tabloda görünür.
"""Ad bir etikettir: tipi nesne tasir, ad tasimaz."""

DEGERLER = (12, 3.5, "olcum", (1, 2), [1, 2], {"kuzey": 1}, None)


def uzunluk(metin):
    return len(metin)


olcum = None
tipler = []
for deger in DEGERLER:
    olcum = deger                      # tek ad, sirayla yedi nesne
    tipler.append(type(olcum).__name__)

print("tek ada sırayla bağlanan tipler:", " ".join(tipler))
print("bağlama sayısı", len(DEGERLER), "— hiçbirinde tip bildirimi yok")
print("son bağlamadan sonra adın tipi:", type(olcum).__name__)
olcum = uzunluk                        # ayni ad, simdi bir islev
print("aynı ad bir işleve de bağlanabilir:",
      type(olcum).__name__, "->", olcum("olcum"))
tek ada sırayla bağlanan tipler: int float str tuple list dict NoneType
bağlama sayısı 7 — hiçbirinde tip bildirimi yok
son bağlamadan sonra adın tipi: NoneType
aynı ad bir işleve de bağlanabilir: function -> 5

Tek bir ad 7 ayrı tipe bağlandı ve sekizincide bir işleve. Hiçbir bağlamada bir bildirim yazılmadı, çünkü yazılacak bir yer yok: olcum adı bir kap değil, üzerine takıldığı nesneye işaret eden bir addır. Adın tipini soran bir işlem de yoktur; type çağrısı adın bağlı olduğu nesneye gider ve yanıtı o nesne verir.

Son satır bir sınıfı daha kapatıyor. İşlev de bir nesnedir; tanımlanması, bir ada bir nesne bağlamaktan başka bir şey değildir. Bu yüzden bir işlev bir ada, bir listeye ya da bir sözlüğe konabilir ve oradan çağrılabilir.

Python’da bir bildirime benzeyen bir yazım vardır ve karıştırılmaya elverişlidir: bir parametrenin ya da dönüş değerinin yanına bir tip adı yazılabilir. Bu yazım bir bildirim değil, bir nottur; yorumlayıcı onu saklar ama zorlamaz.

"""Not bildirim degildir: yorumlayici yazilani zorlamaz."""


def iki_kati(n: int) -> int:
    return n * 2


print("tam sayı:", iki_kati(12))
print("dizgi:   ", repr(iki_kati("ol")))
print("liste:   ", iki_kati([1, 2]))
print("notların saklandığı yer:", list(iki_kati.__annotations__))
tam sayı: 24
dizgi:    'olol'
liste:    [1, 2, 1, 2]
notların saklandığı yer: ['n', 'return']

İşlev bir tam sayı beklediğini yazdığı hâlde dizgiyle de listeyle de çalıştı ve hiçbir uyarı üretmedi. Yazılan notlar kaybolmuş da değil; işlevin üzerinde iki ad altında duruyorlar. Yani not, yürütmeye değil okuyucuya ve dışarıdan çalışan denetleyicilere yazılır. Nota bakıp “burada tip denetleniyor” sanmak, dilin en pahalı yanlış okumalarından biridir.

Buna dinamik tipleme denir ve doğru okunması gerekir: tipin olmadığı anlamına gelmez, tip denetiminin yürütme aşamasına ertelendiği anlamına gelir. Bu, ilk dersteki sınırın aynısıdır. Bir adın var olup olmadığı derlenirken bilinemiyordu; bir nesnenin bir işlemi karşılayıp karşılamadığı da bilinemez, çünkü nesne ancak çalışırken ortaya çıkar. Kusur bulunduğunda ortaya çıkan istisna da ilk derste ölçülenle aynıdır — aranan özel yöntem yoksa TypeError.

Yerleşik Tipler ve Taşıdıkları Protokol

Tip nesnenindir; öyleyse tipleri birbirinden ayıran şey nedir? Programlama Temelleri kursu temel veri tiplerini bir sınıflandırma olarak kurmuştu — sayı, metin, mantıksal değer, koleksiyon. O sınıflandırma tekrarlanmaz. Burada sorulan şey başkadır ve ilk dersin ölçüsünden doğar: bir yerleşik tip, hangi sözdizim biçimlerini karşılayabildiğini tanımladığı özel yöntemlerle ilan eder. Tipin adı hiçbir şey söylemez; yöntem listesi her şeyi söyler.

  • DT17 — Her tip tek bir örnekle temsil edilir ve yoklama örneğin kendisine değil, tipine yapılır; sorulan şey tipin o yöntemi taşıyıp taşımadığıdır.
  • DT18 — “Değişebilir” sütunu tipin bilinen bir niteliğidir, ölçülmez; tabloda yalnız yöntem sütunları ölçümdür.
"""Yerlesik tipler ne yapabiliyor: tasidiklari ozel yontemlere bakilir."""

ORNEKLER = (("int", 12, False), ("float", 3.5, False), ("bool", True, False),
            ("str", "olcum", False), ("tuple", (1, 2), False),
            ("list", [1, 2], True), ("dict", {"kuzey": 1}, True),
            ("set", {1, 2}, True), ("NoneType", None, False))
PROTOKOL = ("__len__", "__iter__", "__contains__", "__getitem__", "__add__")

print(f"{'tip':<9s} {'değişebilir':>11s}  "
      + " ".join(f"{p.strip('_'):>10s}" for p in PROTOKOL))
for ad, ornek, degisebilir in ORNEKLER:
    tasidigi = " ".join(f"{'var' if hasattr(type(ornek), p) else '—':>10s}"
                        for p in PROTOKOL)
    print(f"{ad:<9s} {str(degisebilir):>11s}  {tasidigi}")

print()
print("bool bir int mi:", isinstance(True, int), "| True + True =", True + True)
buyuk = 2 ** 100
print("tam sayının genişliği sabit değil:", buyuk.bit_length(), "bit;",
      "karesi", (buyuk * buyuk).bit_length(), "bit")
tip       değişebilir         len       iter   contains    getitem        add
int             False           —          —          —          —        var
float           False           —          —          —          —        var
bool            False           —          —          —          —        var
str             False         var        var        var        var        var
tuple           False         var        var        var        var        var
list             True         var        var        var        var        var
dict             True         var        var        var        var          —
set              True         var        var        var          —          —
NoneType        False           —          —          —          —          —

bool bir int mi: True | True + True = 2
tam sayının genişliği sabit değil: 101 bit; karesi 201 bit

Tablo tipleri adlarına göre değil, yeteneklerine göre öbekliyor. Üç sayısal tip yalnız __add__ taşıyor: toplanabiliyorlar ama üzerlerinde döngü kurulamıyor, uzunlukları sorulamıyor. str, tuple ve list beş sütunun beşini de dolduruyor; aralarındaki tek fark ölçülen sütunlarda değil, değişebilirlik sütunundadır. dict ve set üstteki üç protokolü taşıyıp __add__ taşımıyor — iki sözlüğün + ile birleştirilememesinin nedeni bir yasak değil, o yöntemin tanımlı olmamasıdır. NoneType satırı ise boş: None üzerinde bu biçimlerin hiçbiri çalışmaz ve ilk dersteki çıplak nesneyle aynı sonucu verir.

set satırındaki tek boşluk da öğreticidir. Küme __contains__ taşıyor ama __getitem__ taşımıyor: içinde bir öğenin bulunup bulunmadığı sorulabilir, sırayla kaçıncı olduğu sorulamaz. Bu, kümenin “sırasız” olmasının kod düzeyindeki karşılığıdır.

Alt iki satır iki Python’a özgü noktayı kapatıyor. bool, int‘in bir alt tipidir — bu yüzden True + True bir TypeError değil, 2 verir. Ve tam sayının sabit bir genişliği yoktur: yüz bitlik bir değer 101 bitte durur, karesi 201 bite büyür. Bilgisayarlar Nasıl Çalışır kursunda sabit genişlikli tipin taşma davranışı kurulmuştu; burada ölçülen fark, Python’un tam sayısında böyle bir sınırın bulunmamasıdır. Sığmama diye bir durum yoktur, yalnızca büyüyen bir gösterim vardır.

Bağın Kimliği

Aynı nesneye birden çok ad bağlanabilir ve is işleci bunu sorar. Bu ders kimlik sayısını basmaz: sayı ortama bağlıdır, koşumdan koşuma değişir ve hiçbir programın doğruluğu ona dayanmamalıdır. Sorulacak soru “hangi nesne” değil, “aynı nesne mi”dir ve buna yalnız is yanıt verir.

Sorunun Python’a özgü karşılığı bileşik atamada görünür. x += ek tek bir yazımdır, ama iki ayrı sonucu vardır: ya adı yeni bir nesneye bağlar, ya da bağlı olduğu nesneyi yerinde değiştirir. Hangisinin olduğu nesneye ikinci bir ad takılarak ölçülebilir.

  • DT19 — Her ölçümde değer bir ada bağlanır, aynı nesneye ikinci bir ad takılır ve += yalnız birinci ad üzerinde çalıştırılır. is ile birinci adın hâlâ aynı nesnede olup olmadığına bakılır.
  • DT20 — Dört tip, üçü değiştirilemez biri değiştirilebilir olacak biçimde seçilmiştir; sonuç sütunu tipin bu niteliğini yansıtır, seçilen değerleri değil.
"""Ayni yazim, iki ayri sonuc: bag mi yeniden kuruldu, nesne mi degisti?"""


def yerinde_mi(deger, ek):
    x = deger
    izci = x                     # ayni nesneye takilan ikinci ad
    x += ek
    return x is izci, izci


print(f"{'tip':<9s} {'+= sonrası':<22s} ikinci adın gördüğü")
for ad, deger, ek in (("tam sayı", 10, 1),
                      ("dizgi", "ol", "cum"),
                      ("demet", (1, 2), (3,)),
                      ("liste", [1, 2], [3])):
    ayni, izci = yerinde_mi(deger, ek)
    print(f"{ad:<9s} {'nesne değişti' if ayni else 'ad yeniden bağlandı':<22s} {izci!r}")
tip       += sonrası             ikinci adın gördüğü
tam sayı  ad yeniden bağlandı    10
dizgi     ad yeniden bağlandı    'ol'
demet     ad yeniden bağlandı    (1, 2)
liste     nesne değişti          [1, 2, 3]

Dört tipin üçünde ad yeniden bağlandı, birinde nesne değişti. Ayrımı yaratan şey tipin değiştirilebilir olup olmamasıdır. Tam sayı, dizgi ve demet değiştirilemez (immutable) tiplerdir: bir kez kurulan nesnenin içeriği hiçbir yolla değiştirilemez. Böyle bir nesnede += yerinde bir işlem yapamaz; yeni bir nesne üretip adı ona bağlar ve ikinci ad eski nesnede kalır. Sağdaki sütun bunu doğrudan gösteriyor — üç satırda ikinci ad hâlâ başlangıç değerini görüyor.

Listede sonuç tersine döner. Nesnenin kendisi büyür; ada dokunulmaz, çünkü dokunmaya gerek yoktur. İkinci ad da aynı nesneye bağlı olduğundan büyümüş hâli görür. Aynı satırın iki tipte iki ayrı anlama gelmesi bir tutarsızlık değil, tek bir kuralın iki sonucudur: değiştirilemez bir nesne üzerinde yerinde işlem tanımlanamaz.

Bu, kursun ikinci iddiasının ilk belirtisidir. x = x + ek ile x += ek yazımları burada aynı sonucu vermez ve fark tesadüfi değildir; iki yazım iki ayrı özel yöntemi çağırır. Adları sonraki derste konacak.

Önbelleklenmiş Nesne

Geriye tek bir soru kalıyor: eşit iki değer aynı nesne midir? Yanıt, sorunun sanıldığından çok daha az güvence taşıdığıdır.

  • DT21 — Karşılaştırılan değerler bir işlev çağrısı içinde, çalışma sırasında üretilir; böylece kaynakta yan yana duran iki sabitin derleme aşamasında birleştirilmesi ölçüme karışmaz.
  • DT22 — “Güvence” sütunu, sonucun neye dayandığını söyler: dil tanımı satırı her yorumlayıcıda aynıdır, gerçekleştirim satırı değişebilir.
  • DT23 — Kimlik yalnız is ile sorulur; hiçbir kimlik sayısı basılmaz.
"""Onbelleklenmis nesne: sonucu GERCEKLESTIRIME BAGLIDIR."""


def uret(a, b):
    return a + b                 # deger calisma sirasinda uretilir


def bosluk():
    return None


def dogru():
    return 1 == 1


CIFTLER = (
    ("küçük tam sayı", uret(2, 3), uret(1, 4)),
    ("büyük tam sayı", uret(500, 1), uret(499, 2)),
    ("kısa dizgi", uret("ol", "cum"), uret("olc", "um")),
    ("liste", uret([1], [2]), uret([1], [2])),
)

print(f"{'çift':<16s} {'==':>6s} {'is':>6s}   güvence")
for ad, x, y in CIFTLER:
    guvence = "dil tanımı" if isinstance(x, list) else "gerçekleştirim"
    print(f"{ad:<16s} {str(x == y):>6s} {str(x is y):>6s}   {guvence}")

print("\ndil tanımının güvence verdiği tekil nesneler:")
print("  başka yerden dönen None aynı nesne mi:", bosluk() is None)
print("  başka yerden dönen doğruluk değeri aynı nesne mi:", dogru() is True)
çift                 ==     is   güvence
küçük tam sayı     True   True   gerçekleştirim
büyük tam sayı     True  False   gerçekleştirim
kısa dizgi         True  False   gerçekleştirim
liste              True  False   dil tanımı

dil tanımının güvence verdiği tekil nesneler:
  başka yerden dönen None aynı nesne mi: True
  başka yerden dönen doğruluk değeri aynı nesne mi: True

Dört çiftin dördünde de == True veriyor: değerler eşit. is sütunu ise dağılıyor. Küçük tam sayıda iki ayrı hesap aynı nesneyi verdi; büyük tam sayıda ve kısa dizgide vermedi.

Bu üç satır gerçekleştirime bağlıdır ve böyle okunmalıdır. Bir yorumlayıcı, sık kullanılan bazı değiştirilemez nesneleri önceden kurup her istendiğinde aynısını verebilir; buna önbelleklenmiş nesne (interning) denir. Hangi değerlerin bu kapsama girdiği dilin tanımında yazmaz. Başka bir yorumlayıcıda — hatta aynı yorumlayıcının başka bir koşumunda — bu üç satırın üçünün de True ya da üçünün de False çıkması dilin tanımını bozmaz. Tablo bir davranışı açıklar, bir kural kurmaz.

Dördüncü satır ayrı bir sınıftandır ve güvencesi vardır: bir liste yazımı her değerlendirildiğinde yeni bir nesne kurar. Önbellekleme burada söz konusu bile değildir, çünkü nesne değiştirilebilirdir — paylaşılsaydı birinde yapılan değişiklik ötekinde görünürdü.

Buradan tek bir pratik kural çıkar ve istisnası yoktur: eşitlik == ile sorulur, is yalnız kimlik sorusu içindir. İki değerin aynı olup olmadığını is ile sınayan bir kod, küçük değerlerde çalışıp büyüklerde sessizce yanlış yanıt verir — hata iletisi vermeyen, en zor bulunan kusur sınıfıdır.

Son iki satır kuralın meşru kullanımını gösteriyor. None dil tanımı gereği tek bir nesnedir; nereden gelirse gelsin aynı nesnedir. Doğruluk değerleri için de aynısı geçerlidir. Bu yüzden bir değerin boş olup olmadığı == None ile değil, is None ile sorulur: burada gerçekten kimlik sorulmaktadır ve yanıtı dil güvence eder.

Özet

  • Tip nesnenin özelliğidir, adın değil; tek bir ad 7 ayrı tipe ve bir işleve hiçbir bildirim olmadan bağlanabilir.
  • Dinamik tipleme tipin yokluğu değil, tip denetiminin yürütme aşamasına ertelenmesidir; karşılıksız bir işlem TypeError ile düşer.
  • += yazımı dört tipin 3’ünde adı yeni bir nesneye bağlar, 1’inde nesneyi yerinde değiştirir; ayrımı yapan şey tipin değiştirilebilir olup olmamasıdır.
  • Değiştirilemez bir nesnede yerinde işlem tanımlanamaz; bu yüzden ikinci bir ad eski değeri görmeye devam eder.
  • Eşit iki değerin aynı nesne çıkması gerçekleştirime bağlıdır ve programın doğruluğu buna dayandırılamaz; liste yazımının her seferinde yeni nesne kurması ise dil tanımının güvencesidir.
  • Eşitlik == ile sorulur; is yalnız kimlik içindir — None sınaması bu yüzden is None ile yazılır.

Sonraki Adım

Bu derste x += ek yazımının tipe göre iki ayrı iş yaptığı ölçüldü ama yaptığı işin adı konmadı. Sonraki ders o adı koyar ve kursun ikinci iddiasını öder: n + m ile n += m aynı işlecin iki yazımı değil, iki ayrı özel yöntemin çağrısıdır. Aritmetik, karşılaştırma, mantıksal ve üyelik işleçlerinin her biri hangi yöntemi çağırıyor, üyelik sınaması aradığı yöntemi bulamadığında nereye düşüyor ve bir işleç hiçbir karşılık bulamadığında 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