Python’da nesne zihinsel modeli: kimlik, değer, durum ve referans
Bu haftadan itibaren nesne yönelimli programlamaya (object-oriented programming, OOP) geçiyoruz. Python’da programın kullandığı her değer, yani her sayı, metin, liste ve sözlük, bir nesnedir (object). Kendi nesne türlerimizi, yani sınıfları (class), gelecek hafta class deyimiyle tanımlayacağız. Bu hafta değişken adlarının nesnelere nasıl bağlandığını ve aynı nesneye iki farklı addan erişince ne olduğunu göreceğiz. 3. haftada listelerle yaptığımız deneyleri bu kez bir kayıtla yineleyeceğiz. Örnekler bir kırtasiyenin stok programından. Her ürünün bir adı ve depoda kaç tane kaldığını gösteren bir stok sayısı var. Kasada satılan kalem depo ekranında da eksilmeli, yani iki ekran aynı ürün kaydını kullanmalı.
Bu sayfadaki kod hücreleri kendiliğinden çalışmaz. Her hücrede önce çıktıyı tahmin edin, sonra Run Code düğmesine basın. Kodu değiştirip yeniden çalıştırabilir, Start Over ile ilk hâline döndürebilirsiniz.
Bu bölümün kapsamı
- Değişken adının bir nesneye bağlanması
- Nesnenin kimlik, tür ve değer özellikleri
- Aynı nesneye iki referans olduğunda değişikliğin iki addan da görülmesi
isile==ayrımı- Küçük tam sayı ve string önbelleklemesinden dil kuralı çıkarılamayacağı
- Nesnenin durumu ve durumun hangi yollarla değiştiği
- Yerinde değiştirme ile yeniden bağlama ayrımı
id()değerinin bellek adresiyle ilişkisi- Çöp toplama (garbage collection) ayrıntıları
- CPython’ın hangi nesneleri yeniden kullandığının ayrıntıları
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Ad bir nesneye bağlanır
C’de ürün kaydını struct Product { char name[20]; int stock; }; diye tanımlayıp struct Product pencil = {"Kalem", 10}; yazdığınızda pencil bellekte iki alanı olan bir kutuydu. pencil.stock = 8; o kutunun içindeki alanı değiştirirdi. Python’da aynı kaydı 3. haftada gördüğünüz sözlükle tutabilirsiniz:
product = {"name": "Kalem", "stock": 10}Bu satır önce bir sözlük nesnesi oluşturur, sonra product adını o nesneye bağlar. Kutu benzetmesi tek bir adla çalışırken işe yarar, ama iki ad aynı nesneyi gösterdiği anda yetmez. Bu derste şu modeli kullanacağız:
Değişken adı bir nesneye bağlanır.
Model sayılar için de geçerli. 3. haftada gördüğünüz id() bir nesnenin kimlik numarasını döndürüyordu. Hücrede score adına iki kez değer atanıyor ve her atamadan sonra id(score) yazdırılıyor. C’deki kutu modeli doğru olsaydı iki sayı aynı çıkardı. Python’da aynı mı çıkar?
score = 90 satırı 80 nesnesinin içini 90 yapmaz. score adını, değeri 90 olan başka bir nesneye bağlar. Buna yeniden bağlama denir.
İki farklı sayı. Sayılar her çalıştırmada değişebilir. Önemli olan, ikisinin farklı olması: score adı önce bir nesneyi, sonra başka bir nesneyi gösterdi.
Her nesnenin kimliği, türü ve değeri var
Python’ın resmî belgelerindeki veri modeline (data model) göre her nesnenin bir kimliği (identity), bir türü (type) ve bir değeri (value) vardır. Kimlik nesneyi ötekilerden ayırır ve nesne var olduğu sürece değişmez. Tür, nesneyle hangi işlemleri yapabileceğinizi ve değerinin değişip değişemeyeceğini belirler. Değer nesnenin içeriğidir.
Hücre kalem kaydının üç özelliğini yazdırıyor. Hücreyi iki kez çalıştırın. Üç satırdan hangisi değişebilir?
type(product) türü, id(product) kimliği, product ise değeri gösterir. Her çalıştırmada yeni bir sözlük oluştuğu için kimlik değişebilir. Tür ve değer aynı kalır.
id() değerini bellek adresi diye ezberlemeyin
Python kodunu çalıştıran programa Python uygulaması (implementation) denir. Bilgisayarınıza kurduğunuz Python, CPython adlı uygulamadır. Bu sayfadaki hücreleri çalıştıran Pyodide de tarayıcıda çalışan bir CPython’dır. PyPy ve MicroPython başka uygulamalardır. CPython’da id() nesnenin bellek adresini verir, ama dil bunu garanti etmez. Biz id() değerini yalnız iki adın aynı nesneyi gösterip göstermediğini görmek için kullanacağız. Bu iş için is daha okunaklıdır. Kimlik numarası yalnız aynı anda var olan nesneleri ayırır: artık kullanılmayan bir nesnenin numarası sonradan başka bir nesneye verilebilir.
İki ad, tek kayıt
C’de bir struct değişkenini başka birine verdiğinizde alanlar kopyalanır: struct Product selected = pencil; satırından sonra iki ayrı kayıt olur. Kasa ekranıyla depo ekranının aynı kaydı kullanması için işaretçi gerekirdi: struct Product *selected = &pencil;.
3. haftada Python’da = işaretinin kopya almadığını, var olan nesneye bir ad daha bağladığını gördünüz. Buna aynı nesneye referans verme (aliasing) dedik. Sözlük de listeyle aynı davranır. Hücrede kasa ekranı selected adıyla iki kalem satıyor. print(product) ne yazar?
selected = product yeni bir sözlük oluşturmaz. product ve selected aynı sözlüğün iki adıdır, bu yüzden selected ile yapılan değişikliği product ile bakınca da görürüz. C’de işaretçinin yaptığı işi Python’da düz bir atama yapar.
Benzerlik paylaşımda biter. C’de *selected = eraser; yazmak, selected’ın gösterdiği pencil kaydının üzerine eraser kaydının alanlarını kopyalar. Python’da selected = {"name": "Silgi", "stock": 4} yazmak ise kaydı değiştirmez, yalnız selected adını yeni bir sözlüğe bağlar. product eski sözlüğü göstermeye devam eder.
Gelecek hafta account, customer, order gibi adlar kendi yazdığınız sınıflardan oluşturulan nesneleri gösterecek. other = account satırı da hesabın kopyasını almaz, aynı hesaba bir ad daha bağlar. Hesap bir addan değiştirilirse öteki ad da yeni durumu görür.
Aynı kayıt mı, eşit kayıt mı?
C’de iki işaretçinin aynı kaydı gösterip göstermediğini işaretçileri == ile karşılaştırarak, iki kaydın aynı bilgileri taşıyıp taşımadığını ise alanları tek tek karşılaştırarak anlardınız. Python’da bu iki soru için iki ayrı işleç var. 3. haftada gördüğünüz gibi is iki adın aynı nesneyi gösterip göstermediğini, == iki değerin eşit olup olmadığını sorar.
Hücrede second, first sözlüğünün 3. haftada gördüğümüz .copy() ile alınmış kopyası. third adına ise düz atamayla first veriliyor. Üç satır ne yazar?
İçeriği eşit iki nesne iki ayrı kayıt olabilir. İki adın aynı kaydı gösterip göstermediğini is söyler.
first == second→True: iki sözlüğün anahtarları ve değerleri eşit.first is second→False:.copy()yeni bir sözlük oluşturdu.first is third→True:third = firstaynı sözlüğe bir ad daha bağladı.
Küçük sayılarda is neden bazen True verir?
Hücrede aynı değerdeki sayılar iki kez karşılaştırılıyor: önce 5 ile 5, sonra metinden dönüştürülen iki 1000. Üç satır ne yazar?
CPython küçük tam sayılar ve bazı stringler için her seferinde yeni nesne oluşturmaz, var olanı yeniden kullanır. Buna önbellekleme (caching) denir, stringlerde interning adı da kullanılır. Uygulama bunu hız ve bellek kazanmak için yapar. “Aynı değerdeki sayılar aynı nesnedir” diye bir dil kuralı yoktur: CPython, PyPy ya da başka bir uygulama hangi nesneleri yeniden kullanacağına kendisi karar verir.
Hücrede 1000’i metinden dönüştürmemizin de bir nedeni var. x = 1000 ve y = 1000 yazarsanız CPython aynı hücredeki aynı sabiti bir kez oluşturabilir ve x is y True verir. Bu da uygulamanın tercihidir. Dilin koyduğu kural şu: değiştirilemez türlerde Python aynı değer için var olan bir nesneyi yeniden kullanabilir, değiştirilebilir türlerde kullanamaz. Bu yüzden iki ayrı [] her zaman iki ayrı listedir.
== ile karşılaştırın
Bir is deneyinde True görmeniz, başka bir sayıda ya da başka bir uygulamada da True göreceğiniz anlamına gelmez. Değer sorusunu == ile sorun. is en sık value is None biçiminde, bir değerin None olup olmadığını denetlemek için kullanılır. Programda tek bir None nesnesi olduğu için bu soru kimlikle sorulur.
Fonksiyon kaydın kopyasını değil kendisini alır
C’de bir struct’ı fonksiyona verdiğinizde fonksiyon onun kopyasını alırdı. Satışın çağıran koddaki kayda yansıması için işaretçi verirdiniz: sell(&pencil, 2). 4. haftada Python’da parametre adının, çağıranın verdiği nesneye bağlandığını gördünüz.
Hücredeki sell fonksiyonu sözlüğü işaretçi olmadan alıyor. Fonksiyonun içindeki ad product, dışarıdaki ad pencil. Son satır ne yazar?
Çağrıda product parametresi pencil adının gösterdiği sözlüğe bağlanır, Python sözlüğü kopyalamaz. Fonksiyonun sözlükte yaptığı değişikliği çağıran kod da görür. return için de durum aynı: fonksiyon bir sözlüğü döndürdüğünde çağırana kopyasını değil kendisini verir.
Parametreye yeni değer atamak çağıranın adını değiştirmez
C’de void increase(int n) fonksiyonu çağıranın değişkenini değiştiremezdi. C’de işaretçi de kopyalanarak gider: sell(&pencil, 2) çağrısında fonksiyon adresin bir kopyasını alır. Bu adresle asıl kaydı değiştirebilir, ama parametredeki işaretçiye başka bir adres atamak çağıranı etkilemez.
4. haftadaki add_one deneyini hatırlayın. Aşağıdaki fonksiyon da parametresine yeni bir değer atıyor ve içeride 11 yazdırıyor. Son satır ne yazar?
number + 1 değeri 11 olan bir int nesnesi verir. Atama yalnız fonksiyonun yerel number adını bu nesneye bağlar. value adı hâlâ 10 nesnesini gösterir.
İki fonksiyon farklı iş yaptığı için farklı davrandı. sell sözlüğü yerinde değiştirdi: aynı nesnenin içeriği değişti. increase ise adı yeniden bağladı: yerel ad başka bir nesneyi göstermeye başladı. sell içinde product = {"name": "Silgi", "stock": 0} yazsaydık bu da yalnız yerel adı yeniden bağlardı ve pencil değişmezdi.
Tür ise nesneyle hangi işin yapılabileceğini belirler. 3. haftadan hatırlayın: list, dict ve set değiştirilebilir (mutable), int, float, bool, str ve tuple değiştirilemez (immutable) türlerdir. int nesnesinin içini değiştiren bir işlem olmadığı için int parametreyle yalnız yeniden bağlama yapılabilir. Listeyi gösteren bir adı ise hem yerinde değiştirebilir hem yeniden bağlayabilirsiniz. 4. haftada gördüğünüz gibi aynı += yazımı listeyi yerinde uzatır, int’te adı yeniden bağlar.
Nesnenin durumu
OOP’de bir nesnenin o anki verisine onun durumu (state) deriz. Değiştirilebilir bir nesnenin durumu program çalışırken değişebilir.
{"name": "Kalem", "stock": 8} sözlüğünde stock değeri durumun bir parçasıdır. Stok 8’den 7’ye inerse nesnenin durumu değişir, kimliği ve türü aynı kalır. Stok 0 olunca satış yapılamaz: durum, nesneyle neyin yapılabileceğini belirler.
Değer (value) Python veri modelinin her nesne için kullandığı terimdir: [70, 80] listesinin değeri içindeki iki sayıdır. Durum (state) ise OOP’de nesneyi tasarlarken kullandığımız terimdir: nesnenin o anki verisi. Bir banka hesabının bakiyesi onun durumudur, bakiye yetmezse para çekilemez. Değiştirilebilir bir nesnenin değeri değiştiğinde çoğu zaman “nesnenin durumu değişti” deriz. İki terim birbirine yakındır, ama ikisini birbirinin yerine kullanmayacağız.
OOP’de sık sık şu soruyu soracağız:
Bu durumun hangi yollarla değişmesine izin veriyoruz?
Stok örneğinde: stock değerini yalnız satış yapan fonksiyon mu değiştirebilmeli, yoksa programın her yerinden product["stock"] = -100 yazılabilmeli mi?
Davranış neden verinin yanında olmalı?
Az önceki sell fonksiyonuyla, stokta iki kalem varken beş kalem satalım. Son satır ne yazar?
Program hata vermeden çalışır. “Stok eksiye düşemez” kuralını denetleyen tek bir satır bile yok.
5. haftada gördüğümüz raise ile fonksiyona bir denetim ekleyelim. Hücre ne yazar? Satış reddedilirse stok değişir mi?
Denetim, stoğu değiştiren satırdan önce çalışır. Satış reddedilince stok olduğu gibi kalır.
Bu küçük örnekte kural sell fonksiyonunun içinde duruyor. Ama program büyüdükçe product sözlüğünde hangi anahtarların bulunması gerektiğini ve sözlüğü hangi fonksiyonların değiştirdiğini izlemek zorlaşır. Başka bir dosyadaki bir fonksiyon product["stock"] değerini hiç denetlemeden değiştirebilir. C’de bir struct ile onu değiştiren fonksiyonlar da böyle ayrı ayrı dururdu.
Nesnenin verisi onun durumudur. Bu veriyi okuyan ve değiştiren fonksiyonlar da nesnenin davranışıdır. Gelecek hafta göreceğimiz sınıf (class), durumu ve davranışı tek bir ad altında, aynı yapıda toplar.
C ile Python yan yana
Bu haftaki işlerin C’deki ve Python’daki karşılıkları:
| İş | C’de ya da önceki haftalarda | Bu haftaki Python yolu | Dikkat |
|---|---|---|---|
| Ürünün adını ve stoğunu tek kayıtta tutmak | struct Product pencil = {"Kalem", 10}; |
product = {"name": "Kalem", "stock": 10}: product adı bir sözlük nesnesine bağlanır |
Ada yeni değer atamak nesneyi değiştirmez, adı başka bir nesneye bağlar |
| Kasa ve depo ekranının aynı kaydı kullanması | İşaretçi: struct Product *selected = &pencil; |
selected = product |
= hiçbir türde kopya almaz. Ayrı bir kayıt için .copy() gerekir. .copy() sığ kopyadır: içteki liste ya da sözlük iki kopyada ortak kalır |
| İki kaydın aynı kayıt mı, eşit kayıt mı olduğunu anlamak | İşaretçileri == ile, alanları tek tek karşılaştırmak |
is kimliği, == değeri sorar |
Sayıları ve metinleri is ile karşılaştırmayın |
| Çağıranın kaydındaki stoğu azaltan satış fonksiyonu | Fonksiyona işaretçi vermek: sell(&pencil, 2) |
sell(product, 2): parametre aynı sözlüğe bağlanır |
Parametreye yeni değer atamak çağıranın adını değiştirmez |
| Stoğun eksiye düşmesini önlemek | sell içinde if ile denetlemek |
sell içinde raise ValueError |
Kodun başka bir yeri yine product["stock"] = -100 yazabilir |
Python’da selected = product satırı kasaya ve depoya aynı kaydı verir. C’de aynı satır iki ayrı kayıt oluştururdu, çünkü struct ataması kopyalar. Python’da = paylaşır. Satış fonksiyonu için C’de işaretçi gerekirdi. Python’da gerekmez, çünkü parametre zaten verilen nesneye bağlanır. Eksi stok kuralı iki dilde de yarım kalıyor: sell stoğu denetliyor, ama kayıt programın her yerinden değiştirilebilir. Gelecek hafta veriyi ve onu değiştiren fonksiyonları bir sınıfta toplayacağız. 8. haftada da kuralı sınıfın içinde koruyacağız.
Alıştırma: Yerinde mi, yeniden bağlama mı?
Hücre dört satır yazdırıyor. Önce dört satırı tahmin edin, sonra çalıştırın.
Hangi satır listeyi yerinde değiştirdi, hangisi bir adı yeniden bağladı? Hangi adlardan bakınca aynı listeyi görüyorsunuz?
Alıştırma: Referans diyagramı
first = {"balance": 100}
second = first
third = {"balance": 100}Üç değişken adını ve kaç ayrı sözlük nesnesi olduğunu gösteren bir diyagram çizin. Ardından second["balance"] += 50 satırından sonra hangi adlarla bakınca 150 görüleceğini açıklayın.
İki sözlük nesnesi var. first ve second aynı sözlüğü, third ise içeriği eşit ikinci sözlüğü gösterir. second["balance"] += 50 ortak sözlüğü yerinde değiştirir: first ve second ile bakınca 150, third ile bakınca 100 görülür.
Alıştırma: Geçersiz durumu engelle
Boşluğa, bakiye çekilmek istenen tutara yetmiyorsa ValueError yükselten iki satır yazın. Denetim bakiyeyi azaltan satırdan önce gelmeli: işlem reddedilirse bakiye değişmemeli. Bakiyenin tamamı çekilebilmeli. Run Code sonucu denetler.
if amount > account["balance"]:
raise ValueError("Yetersiz bakiye")Sıra sizde: Prosedürel hesap
account = {
"owner": "Ada",
"balance": 500,
}Veriyi bir sözlükte, işlemleri ayrı fonksiyonlarda tutan bu yaklaşıma prosedürel yaklaşım denir. C’de bir struct ve onu işleyen ayrı fonksiyonlarla çalışmak da aynı yaklaşımdır. Para yatırma, para çekme ve bakiye sorgulama fonksiyonlarını yazın. Yatırılan ve çekilen tutar pozitif olmalı, çekilen tutar bakiyeyi aşmamalı. Bu kurallar bozulursa ValueError yükseltin. Bakiye sorgulama fonksiyonu bakiyeyi döndürsün, sözlüğü değiştirmesin. Başarısız bir işlemden sonra sözlüğün değişmediğini doğrulayın.
Çalışır kod görevi: Üç durumu deneyin: normal para çekme, yetersiz bakiye ve negatif tutar. Bakiyeyi en az üç assert ile denetleyin. 5. haftadaki gibi assert satırı, koşul yanlışsa AssertionError yükseltir: assert account["balance"] == 400. Reddedilen bir işlemi try içinde çağırın, yoksa program assert satırına gelmeden durur:
try:
withdraw(account, 1000)
except ValueError:
pass
assert account["balance"] == 400Sonunda şu soruyu cevaplayın:
Bu üç fonksiyonu ve hesabın verisini tek bir yapıda toplarsak ne kazanırız?
Bu sorunun cevabı bizi gelecek hafta class BankAccount tasarımına götürecek.
Tek sayfa özet
- Python’da her değer bir nesnedir. Değişken adı nesne değildir, bir nesneye bağlanır.
- Ada yeni değer atamak nesnenin içini değiştirmez, adı başka bir nesneye bağlar. Buna yeniden bağlama denir.
product["stock"] = 8gibi bir atama ise sözlüğün içini değiştirir. - Python veri modelinde her nesnenin kimliği, türü ve değeri vardır.
b = akopya almaz. İki ad aynı nesneyi gösterir ve bir addan yapılan değişikliği öteki de görür. C’de işaretçinin yaptığı işi Python’da düz atama yapar.==eşitlik,iskimlik sorusudur. Eşit içerikli iki nesne iki ayrı nesne olabilir.- Küçük tam sayı ve string önbelleklemesi uygulamanın tercihidir. Değer karşılaştırmasını
==ile yapın,is’iNonedenetimi gibi kimlik soruları için kullanın. - Fonksiyon çağrılınca parametre adı verilen nesneye bağlanır. Fonksiyon nesneyi yerinde değiştirirse çağıran da görür, parametreyi yeniden bağlarsa çağıranın adı değişmez. Değiştirilemez bir nesneyi (
int,str) gösteren parametreyle yalnız yeniden bağlama yapılabilir. - OOP’de nesnenin o anki verisine durum deriz. Nesnenin verisini okuyan ve değiştiren fonksiyonlar onun davranışıdır.
- Sözlükteki veriyi programın her yeri değiştirebilir. Veriyi ve onu kurallara uyarak değiştiren fonksiyonları tek bir yapıda, sınıfta toplamamızın bir nedeni bu.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Değişken adının bir nesneye bağlandığını, kutu olmadığını açıklar.
- Bir nesnenin kimlik, tür ve değer özelliklerini ayırt eder.
- Aynı nesneye iki referans olduğunda bir addan yapılan değişikliğin ötekinden görüldüğünü gösterir.
isile==sorularını ayırt eder veisişlecini yalnızca kimlik sorusu için kullanır.- Küçük tam sayı ve string önbelleklemesinden dil kuralı çıkarılamayacağını açıklar.
- Nesnenin durumunu tanımlar ve programda durumu değiştiren satırları bulur.
- Bir nesneyi yerinde değiştirmek ile bir adı başka bir nesneye yeniden bağlamak arasındaki farkı açıklar.