Kapsülleme, property ve geçerli durum
Geçen hafta BankAccount sınıfını yazdınız, ama account.balance = -500 gibi bir satırı hiçbir şey durdurmuyordu. Program yine çalışır, ama eksi bakiyeli bir hesapla yanlış sonuç verir. Bu hafta nesnenin kurallarını sınıfın içine yazacağız: kurala uymayan bir değer geldiğinde program hata verecek. Üç kavram kullanacağız: değişmez koşul (invariant), kapsülleme (encapsulation) ve property. Üçünü de ilk geçtikleri yerde birer örnekle tanımlayacağız.
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ı
- Bir sınıf için değişmez koşul (invariant) yazma
- Geçersiz başlangıç durumunu
__init__içinde engelleme - Kapsüllemenin amacı: erişimi yasaklamak değil, niteliklerin yalnız sınıfın metotlarıyla değişmesi
- Tek alt çizgi geleneği ve Python’da dışarıdan erişimi engelleyen bir
privatebelirtecinin olmaması propertyve setter yazma, üç adın aynı olması- Doğrulamayı tek yerde tutmak için
__init__içinde setter’ı kullanmak - Önce ön koşulları denetleyip sonra niteliği değiştirmek
- Çift alt çizgi ad dönüştürmesinin (name mangling) nasıl çalıştığı
__set_name__ve descriptor protokolüdataclassile doğrulama__getattr__ve__setattr__ile erişim yakalama
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Ürünün beş kuralını C’de nasıl korurdunuz?
3. haftadaki envanter görevi bir soruyla bitiyordu: “Stok sıfırın altına düşmesin” kuralını nerede koruyoruz? 6. haftada kuralı sell fonksiyonunun içine yazdınız, ama sözlüğü programın her yeri yine değiştirebiliyordu. Bu hafta o soruyu cevaplıyoruz.
Bir kırtasiyenin stok programında her ürünün adı, fiyatı ve stok sayısı var. Program şu beş kuralı korumalı:
- Fiyatı sıfır ya da eksi olan, stoğu eksi olan bir ürün hiç kaydedilmemeli.
- Stok yalnız satışla ve mal girişiyle değişmeli. Stokta olandan fazlası satılamamalı.
- Programın öteki parçaları stoğu doğrudan değiştirememeli.
- Fiyat sonradan değiştirildiğinde de aynı kural denetlenmeli.
- Satış hata verirse ürün satıştan önceki hâlinde kalmalı.
C’de ürünü bir struct ile tutardınız. Bu kuralların her birini C’de nerede ve nasıl korurdunuz? Okumaya devam etmeden önce her kural için bir satır yazın. Geçen haftaki BankAccount sınıfıyla da düşünebilirsiniz. “Bunu C’de yapamam” da bir cevap.
Bölümde bu beş kuralı Python’da Product sınıfının içine yazacağız. Bölümün sonunda listenize döneceğiz.
Geçersiz ürünü hiç oluşturmamak
Fiyatın sıfırdan büyük olması ve stoğun eksiye düşmemesi, ürün kullanıldığı sürece her an doğru kalması gereken kurallardır. Böyle bir kurala değişmez koşul (invariant) denir. Niteliklerin değeri değişir, kural değişmez: stok 12 de olabilir 0 da, ama eksi olamaz. Bir banka hesabında balance >= 0, bir ders kaydında 0 <= score <= 100 de birer değişmez koşuldur.
Kuralı ilk denetleyecek yer __init__. C’de ürünü oluşturan bir fonksiyon, kurala uymayan bir değer gelince hata kodu döndürürdü. __init__ değer döndürmez, hatayı raise ile bildirir. Hücrede stoğu -5 olan bir kalem oluşturuluyor. Hücre ne yazar? pen adı bir nesneye bağlanır mı?
raise çalışınca __init__ yarıda kalır ve nesne hiç oluşmaz. Oluşan her ürün ilk andan itibaren kurala uyar.
Hatayı erken vermek: fail fast
Fail fast (erken hata ver) ilkesi, geçersiz bir değeri ortaya çıktığı anda bildirmeyi önerir. Stoğu -5 olan kalemin daha oluşturulurken hata vermesi, eksi stoğun programda dolaşıp haftalar sonra yanlış bir sipariş raporuna yol açmasından iyidir. Hata, değerin ortaya çıktığı yere yakın verilirse nedeni de kolay bulunur.
Stoğu yalnız metotlar değiştirsin
Stok iki yoldan değişir: mal girişi ve satış. Hücrede ikisi için birer metot var, add_stock ve sell. Stoğu okumak için de stock() metodu var. Kalemin stoğu 5, önce 3 tane ekleniyor, sonra 10 tane satılmak isteniyor. Hücre ne yazar?
İki metot da stoğa dokunmadan önce miktarı denetler, stoğu ancak kural tutuyorsa değiştirir. Kural artık sınıfın içinde, stoğu değiştiren iki metotta yazılı.
Niteliğin adı da değişti: stock yerine _stock. Bu alt çizginin ne işe yaradığına sıradaki başlıkta bakacağız.
Tek alt çizgi: “dışarıdan dokunmayın”
C’de bir struct’ın alanlarını, o struct’ı gören her kod değiştirebilirdi. Bazı dillerde bir nitelik private diye işaretlenir ve ona sınıfın dışından erişim tamamen engellenir. Python’da böyle bir belirteç yok. _stock adının başındaki tek alt çizgi bir adlandırma geleneğidir (convention). Python programcıları bu işareti şöyle okur: “Bu nitelik sınıfın iç ayrıntısıdır, dışarıdan dokunmayın.”
Python bu geleneği zorlar mı? Hücrede kalem 5 stokla oluşturuluyor, sonra dışarıdaki kod _stock niteliğine -5 atıyor. Bu atama hata verir mi? Son satır ne yazar?
__init__ içindeki kontrol yalnız nesne oluşturulurken bir kez çalışır. Sonraki atamayı denetleyen bir kod yok ve Python alt çizgili bir ada dışarıdan atama yapılmasını engellemez.
Bir nesnenin dışarıdaki koda açtığı metot ve niteliklere o nesnenin arayüzü denir. Kapsülleme, niteliklerin yalnız arayüzdeki metotlarla, kurala uyarak değişmesidir. Python bunu bir erişim engeliyle sağlamaz. Kuralı programcılar korur: alt çizgili adlara dışarıdan dokunmaz, niteliği sınıfın metotlarıyla değiştirirler.
Çift alt çizgi ve ad dönüştürme
__stock gibi iki alt çizgiyle başlayan bir ad, sınıf gövdesinde Python’ın o adı değiştirmesine yol açar. Buna ad dönüştürme (name mangling) denir:
class Product:
def __init__(self):
self.__stock = 5Python bu adı _Product__stock biçimine dönüştürür. Bu dönüşüm niteliği gizlemek ya da korumak için yapılmaz. Bir sınıftan yeni bir sınıf türetildiğinde (buna kalıtım denir, 10. bölümde göreceğiz) iki sınıftaki aynı adlı niteliklerin yanlışlıkla çakışmasını önler.
__x gerçek anlamda private değildir
Dönüştürülmüş ada pen._Product__stock yazarak yine erişebilirsiniz. Çift alt çizgili bir nitelik için “kimse erişemez” demek bu yüzden yanlıştır. Bu derste sınıfın iç verisi için tek alt çizgi (_stock) kullanacağız. __x biçiminde bir ad gördüğünüzde ad dönüştürmenin çalıştığını bilmeniz yeter.
Fiyatı kurala uyarak değiştirmek: property
Stoğu pen.stock() diye, parantezle okuyoruz. Fiyat ise düz bir nitelik: pen.price = -12 satırını hiçbir şey durdurmaz. Fiyatın pen.price diye okunmasını, ama yalnız kurala uyan değerlerle değişmesini istiyoruz. Python’da bunun aracı property’dir: dışarıdan nitelik gibi görünen, ama okununca ya da atanınca bir metot çalıştıran ad.
Önce okuma tarafına bakalım. 7. haftada @classmethod için gördüğünüz gibi, @property de bir dekoratördür. Bir metodun üstüne yazılınca o metot parantezsiz çağrılır:
@property
def stock(self):
return self._stock@property ile işaretlenen metot değer okunurken çalışır. Buna getter denir. Artık pen.stock() yerine pen.stock yazarız. Değer atanırken çalışan metoda ise setter denir. Hücredeki sınıfta stock için setter yok. print(pen.stock) ne yazar? Son satırda ne olur?
Setter’ı olmayan bir property’ye değer atanamaz. Stok için istediğimiz de bu: stok yalnız add_stock ve sell yoluyla değişsin.
Fiyat ise değişebilmeli, ama yalnız kurala uyarak. Bunun için setter yazılır. Hücredeki pen.price = 12 ve pen.price = -12 satırları setter’ı çalıştırır. İlk print() ne yazar? Son satır çalışır mı?
pen.price = -12 sıradan bir atama gibi görünür, ama Python bu atamayı setter’a gönderir. Setter kurala uymayan değeri reddeder ve _price eski değerinde kalır.
Setter söz diziminde üç ad aynı olmalıdır
Yukarıdaki örnekte şu üç satıra dikkat edin:
@property
def price(self):
...
@price.setter
def price(self, value):
...Dışarıdan kullanılan ad price ise getter metodunun adı, @price.setter dekoratöründeki ad ve setter metodunun adı da price olmalıdır. Setter metoduna farklı bir ad vermek bu konuda en sık yapılan hatalardandır.
__init__ neden setter’ı kullanıyor?
Son hücredeki __init__, fiyatı self._price = price diye değil self.price = price diye atıyor. __init__ içinde hiç if yok. Product("Klavye", -100) hata verir mi?
__init__ içindeki self.price = price ataması da setter’dan geçer. Böylece başlangıçtaki fiyat da sonraki fiyat atamaları da aynı kuraldan geçer. C’de ürünü oluşturan fonksiyonun, fiyatı denetleyen set_price() fonksiyonunu çağırması gibi düşünebilirsiniz.
Aynı kuralı __init__ içinde bir kez, setter içinde bir kez daha yazarsanız ikisi kolayca birbirini tutmaz hâle gelir: birini güncelleyip ötekini unutursunuz. Niteliği dışarıdan değiştirmenin yolu setter’sa __init__ de ilk değeri o setter’la atasın. Kural değişirse yalnız setter’ı güncellersiniz.
Yarım güncelleme tehlikesi
Satış metodunu bu kez başka bir sırayla yazalım: stok önce azaltılıyor, kontrol sonra yapılıyor. Kalemin stoğu 3, 5 tane satılmak isteniyor ve satış hata veriyor. Son satır kaç yazar?
Kontrol hata verdiğinde _stock çoktan eksiye düşmüştür ve öyle kalır. raise o ana kadar yapılan değişikliği geri almaz. Ürün yarım güncellenmiş, kurala uymayan bir durumda kalır.
Bir işlemin yapılabilmesi için önceden doğru olması gereken koşula ön koşul denir. Burada ön koşul, stoğun satılan miktara yetmesidir. Güvenli sıra şudur:
- Bütün ön koşulları denetle.
- Sonra durumu değiştir.
“Stoğu yalnız metotlar değiştirsin” başlığındaki sell bu sırayı izliyordu. İki nesneyi birlikte değiştiren işlemlerde sıra daha da önemlidir. Bir hesaptan ötekine para aktaran metot kontrolü arada yaparsa, hata anında para bir hesaptan çıkmış, öteki hesaba girmemiş olur. Bu sırayı dönem boyunca kullanacağız.
Soruya dönelim: beş kural Python’da
Bölümün başında ürünün beş kuralını C’de nasıl koruyacağınızı sormuştuk. Python’daki karşılıkları şöyle:
| Kural | C’de ya da 7. haftada | Bu haftaki Python yolu | Dikkat |
|---|---|---|---|
| Geçersiz ürün kaydedilmesin | Ürünü oluşturan fonksiyon değerleri denetler, hata kodu döndürür | __init__ içinde raise ValueError(...) |
__init__ değer döndürmez, hatayı raise ile bildirir |
| Stok yalnız satışla ve mal girişiyle değişsin | sell() ve add_stock() fonksiyonları if ile denetler |
sell() ve add_stock() metotları, _stock niteliği |
Stoğu okumak için setter’ı olmayan bir stock property’si yeter |
| Öteki kod stoğu doğrudan değiştiremesin | Yapılamaz: struct alanını her kod değiştirebilir |
Tek alt çizgi: _stock |
Python da engellemez, pen._stock = -5 çalışır |
| Fiyat sonradan değişince de denetlensin | set_price() fonksiyonu yazılır, herkesin onu çağırdığına güvenilir |
@property ve @price.setter, __init__ içinde self.price = price |
Getter, @price.setter ve setter aynı adı taşır |
| Satış hata verirse ürün eski hâlinde kalsın | Önce if ile denetlenir, sonra alan değiştirilir |
Aynı sıra: önce ön koşullar, sonra atama | raise yapılmış değişikliği geri almaz |
Listenizi tabloyla karşılaştırın. 3. kural için “C’de yapamam” diye yazdıysanız Python’da da durum pek farklı değil: alt çizgi bir engel koymaz, programcılara “dokunmayın” der. Kuralı korumanın yolu, niteliği yalnız sınıfın metotlarıyla değiştirmektir. 4. kural için ayrı bir set_price() fonksiyonu düşündüyseniz Python’daki setter aynı işi yapar, üstelik dışarıdaki kod sıradan bir atama yazar.
3. haftadaki soruya da artık cevap verebiliriz. Stok bir sözlükte dururken “stok sıfırın altına düşmesin” kuralı, sözlüğü değiştiren her satırda ayrı ayrı korunmak zorundaydı. Şimdi kural Product sınıfında, __init__ ve sell içinde yazılı. Stoğu değiştirmek isteyen kod bu metotları çağırır ve kural her seferinde denetlenir.
Alıştırma: Invariant belirle
Aşağıdaki sınıfların her biri için en az iki invariant yazın:
StudentBankAccountReservation
Puan aralığı, bakiyenin eksiye düşmemesi ya da tarihlerin sırası gibi kurallar düşünebilirsiniz.
Alıştırma: Güvenli withdraw
7. haftada bakiyenin eksiye düşmemesi kuralını withdraw metoduna yazacağımızı söylemiştik. Boşluğa iki kontrol yazın: tutar pozitif olmalı ve bakiye tutara yetmeli. Kural bozulursa metot ValueError yükseltsin. İki kontrol de bakiyeyi azaltan satırdan önce gelmeli.
if amount <= 0:
raise ValueError("Tutar pozitif olmalı")
if amount > self._balance:
raise ValueError("Yetersiz bakiye")Alıştırma: Property mi metot mu?
Aşağıdaki dört yazımı karşılaştırın:
account.balanceaccount.withdraw(100)rectangle.areaorder.cancel()
Hangileri nesnenin verisinden hesaplanan bir değer gibi, hangileri nesneye verilen bir komut gibi okunuyor? Python’da her şeyi property yapmanız gerekmez, her değeri bir get_...() metoduyla okumanız da gerekmez. Arayüzü, kodu okuyan kişi neyin değer, neyin eylem olduğunu hemen anlayacak biçimde kurun.
Sıra sizde: Güvenli kitap
7. haftada yazdığınız Book sınıfı aynı kitabın iki kez ödünç alınmasını engellemiyordu. Şimdi engelleyin. Book sınıfı şu kuralları korusun:
titleboş olamaz.is_borroweddışarıdan okunabilir, ama yalnızborrow()vereturn_book()metotlarıyla değişir.- Ödünçteki kitap yeniden ödünç alınamaz.
- Ödünçte olmayan kitap iade edilemez.
title niteliğini getter ve setter’ı olan bir property ile yazın. __init__ içinde self.title = title yazarak aynı doğrulamayı yeniden kullanın. is_borrowed için yalnız getter yazın. Kural bozulursa metot ValueError yükseltmeli ve kitabın durumunu değiştirmemeli.
Çalışır kod görevi: En az şu beş durumu çalıştırın: geçerli bir kitap, boş başlıkla oluşturulan kitap, sonradan atanan boş başlık, aynı kitabı iki kez ödünç alma ve ödünçte olmayan kitabı iade etme. Hata veren işlemlerden sonra niteliklerin değişmediğini assert ile doğrulayın.
Tek sayfa özet
- Değişmez koşul (invariant), nesne kullanıldığı sürece her an doğru kalması gereken kuraldır:
stock >= 0gibi. - Kuralı ilk
__init__denetler. Kurala uymayan değer gelirseraisenesnenin oluşmasını engeller. - Kapsülleme, niteliklerin yalnız sınıfın metotlarıyla, kurala uyarak değişmesidir.
_stockgibi tek alt çizgili bir ad “bu ad içeride kullanılır” der, ama erişimi engellemez. Python’daprivatebelirteci yoktur.property,pen.price = 12gibi sade bir atamanın arkasına denetim koyar. Setter’ı olmayan bir property’ye değer atanamaz.- Getter,
@price.setterve setter aynı adı taşır.__init__ilk değeri setter’la atarsa doğrulama tek yerde kalır. - Önce bütün ön koşulları denetleyin, sonra durumu değiştirin.
raiseyapılmış değişikliği geri almaz.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Bir sınıf için değişmez koşul (invariant) yazar.
- Geçersiz başlangıç durumunu
__init__içinde engeller. - Kapsüllemede amacın erişimi yasaklamak değil, niteliklerin yalnız sınıfın metotlarıyla değişmesi olduğunu açıklar.
- Tek alt çizginin bir adlandırma geleneği olduğunu ve Python’ın dışarıdan erişimi engellemediğini gösterir.
propertyve setter tanımlar; üç adın aynı olması gerektiğini bilir.__init__içinde setter’ı kullanarak doğrulamayı tek yerde tutar.- Önce ön koşulları sınayıp sonra durumu değiştirerek yarım güncellemeyi önler.