Bu hafta nesnelerin anlamsız değerlere düşmesini önlemeyi öğreneceğiz. Üç kavram kullanacağız: değişmez koşul (invariant), kapsülleme (encapsulation) ve property. Üçünü de aşağıda birer örnekle tanımlayacağız.
Bu bölümün kapsamı
NoteSınavda sorulur
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 private belirtecinin olmaması
property ve setter; üç 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
NoteSınav dışı (bilgi amaçlı)
Çift alt çizgi ad dönüştürmesinin (name mangling) nasıl çalıştığı
__set_name__, descriptor protokolü
dataclass ile doğrulama
__getattr__ / __setattr__ ile erişim yakalama
Bu başlıklar konunun devamıdır; ileride karşınıza çıkar ama bu derste ezberlemeniz beklenmiyor.
Nesne geçersiz bir duruma düşebilir mi?
Geçen hafta yazdığımız BankAccount sınıfında account.balance = -500 satırını hiçbir şey durdurmaz. Bakiyesi eksiye düşen bir hesaba ya da fiyatı sıfırın altında bir ürüne izin veren program çalışır, ama doğru çalışmaz. Bu bölümde bu tür kuralları sınıfın koduna yazacağız. Kurala uymayan bir değer geldiğinde program hata verecek.
Değişmez koşul (invariant) nedir?
Değişmez koşul (invariant), nesne kullanılırken her zaman doğru olması gereken kuraldır. Niteliklerin değeri değişir, kural değişmez: bir hesabın bakiyesi 100 de olabilir 20 de, ama eksi olamaz.
Bir banka hesabı için:
balance >= 0
Bir ürün için:
price > 0 ve stock >= 0
Bir ders kaydı için:
0 <= score <= 100
Bu kurallar nesnenin ne olduğunu da söyler: puanı 130 olan bir ders kaydı artık anlamlı bir kayıt değildir.
Geçersiz başlangıcı engellemek
__init__ kuralı daha nesne oluşturulurken denetler. Negatif bakiyeli bir hesap hiç oluşmaz; nesne ilk andan itibaren geçerlidir.
Bakiyeyi yalnız metotlar değiştirsin
Bakiyeyi değiştiren iki metot var, deposit ve withdraw. İkisi de bakiyeye dokunmadan önce tutarı denetliyor. Dışarıdaki kod bakiyeyi account.balance ile okur. @property ile işaretlenen balance metodu parantezsiz çağrılır ve _balance değerini döndürür; property ayrıntısını aşağıda göreceğiz.
_balance adının başındaki tek alt çizgi, Python programcıları arasındaki bir adlandırma geleneğidir. Anlamı şudur: “Bu nitelik sınıfın iç ayrıntısıdır, dışarıdan dokunmayın.” Python bu niteliğe erişimi teknik olarak engellemez.
Kapsülleme ve invariant
ImportantKapsüllemede amaç kuralı korumaktır
Bir nesnenin dışarıdaki koda açtığı metot ve niteliklere o nesnenin arayüzü denir. Kapsüllemede amaç, niteliklerin yalnızca arayüzdeki kurallı metotlarla değişmesidir. Bazı dillerde bir nitelik private diye işaretlenir ve ona sınıf dışından erişim tamamen engellenir. Python’da kapsülleme çoğu zaman böyle bir engelle değil, sorumluluk ve arayüz disipliniyle korunur: programcılar alt çizgili adlara dokunmaz, niteliği sınıfın metotlarıyla değiştirir.
Tek alt çizgi ve çift alt çizgi
Python’da _balance yalnızca bir adlandırma geleneğidir (convention). Okuyana “bu ad sınıfın ya da modülün iç ayrıntısıdır” der. __balance gibi iki alt çizgiyle başlayan bir ad ise 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 Account:def__init__(self):self.__balance =100
Python bu adı kabaca _Account__balance biçimine dönüştürür. Bunun amacı niteliği gizlemek ya da korumak değildir. 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ı azaltır.
Warning__x gerçek anlamda private değildir
Dönüştürülmüş ada account._Account__balance yazarak yine erişebilirsiniz. Yani çift alt çizgili bir niteliğe “kimse erişemez” demek yanlıştır. Bu derste sınıfın iç verisi için çoğunlukla tek alt çizgi (_balance) kullanacağız. __x biçiminde bir ad gördüğünüzde ad dönüştürmenin çalıştığını bilmeniz yeterlidir.
property ne zaman anlamlı?
Bazen bir niteliğin account.balance diye okunmasını, ama yalnız kurala uyan değerlerle değişmesini isteriz. Python’da bunun aracı propertydir: dışarıdan nitelik gibi görünen, ama okununca ya da atanınca bir metot çalıştıran bir ad.
property erişim kapısı
@propertydef balance(self):returnself._balance
@property ile işaretlenen metot, değer okunurken çalışır; buna getter denir. account.balance yazınca bu metot çalışır ve _balance değerini döndürür. Değer atanırken çalışan metoda ise setter denir. Burada setter tanımlamadığımız için account.balance = -100 gibi bir atama yapılamaz.
Değerin değişebilmesi, ama yalnız kurala uyarak değişmesi gerekiyorsa setter yazılır:
Dışarıdan kullanılan ad price ise getter metodunun adı, @price.setter dekoratöründeki ad ve setter metodunun adı aynı olmalıdır. Setter metoduna farklı bir ad vermek bu konuda en sık yapılan hatalardandır.
__init__ neden setter’ı kullanıyor?
Product.__init__ içinde doğrudan:
self._price = price
yazmak yerine:
self.price = price
yazdık. Böylece başlangıçtaki fiyat ile daha sonraki fiyat atamaları aynı doğrulama kuralından geçer.
class Product:def__init__(self, price):self.price = price # setter çalışır@propertydef price(self):returnself._price@price.setterdef price(self, value):if value <=0:raiseValueError("Fiyat pozitif olmalı")self._price = value
Bu, doğrulamayı iki yere kopyalamaktan daha güvenlidir: kural değişirse yalnız setter’ı güncellersiniz.
ImportantTek doğrulama noktası
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 ise __init__ de mümkün olduğunda ilk değeri o setter’la atasın.
Fail fast
Fail fast (erken hata ver) ilkesi şunu söyler: geçersiz bir değer ortaya çıktığında sorunu mümkün olduğunca erken bildirmek genellikle daha güvenlidir. Örneğin
Product("Klavye", -100)
çağrısının daha nesne oluşturulurken hata vermesi, negatif fiyatın programın içinde dolaşıp çok daha sonra yanlış bir rapora yol açmasından iyidir.
Bakiye önce azaltılıyor, kontrol sonra yapılıyor. Kontrol hata verdiğinde self._balance çoktan eksiye düşmüş oluyor ve öyle kalıyor; para other hesabına da geçmiyor.
Bir işlemin yapılabilmesi için önceden doğru olması gereken koşula ön koşul denir. Burada ön koşul, bakiyenin tutara yetmesidir. Daha güvenli sıra şudur:
Tüm ön koşulları kontrol et,
Sonra durumu değiştir.
Bu ilkeyi dönem boyunca kullanacağız.
Alıştırma: Invariant belirle
Aşağıdaki sınıflar için en az iki invariant yazın:
Student
Product
Reservation
Tarihlerin sırası, puan aralığı, fiyatın pozitif ya da stoğun negatif olmaması gibi kurallar düşünebilirsiniz.
Alıştırma: Güvenli withdraw
if amount <=0:raiseValueError("Tutar pozitif olmalı")if amount >self._balance:raiseValueError("Yetersiz bakiye")
Alıştırma: Property mi metot mu?
Aşağıdakileri tartışın:
account.balance
account.withdraw(100)
rectangle.area
order.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 ürün
Product sınıfı yazın:
name: boş olamaz,
price: 0’dan büyük olmalı,
stock: negatif olamaz.
Metotlar:
add_stock(amount)
sell(amount)
Fiyatı getter ve setter’ı olan bir property ile yazın. __init__ içinde self.price = price yazarak aynı doğrulamayı yeniden kullanın. Stok yetmezse sell hata vermeli ve stoğu değiştirmemelidir.
Çalışır kod görevi: En az şu beş durumu çalıştırın: geçerli bir ürün, geçersiz başlangıç fiyatı, sonradan atanan geçersiz fiyat, başarılı satış ve yetersiz stok. Hata veren işlemlerden sonra niteliklerin değişmediğini assert ile doğrulayın.
Sınav provası
Önce kendi cevabınızı seçin; sonra cevap anahtarında her şıkkın neden doğru veya yanlış olduğunu okuyun. Çeldiriciler uydurma değil, bu konuda gerçekten yapılan hatalardır. Sınav maddeleri de bu mantıkla yazılır.
Madde 1. Değişmez koşul (invariant) nedir?
A. Sınıfın bütün örneklerinde ortak olan sınıf niteliğidir
B. Nesne kullanılırken her zaman doğru olması gereken kuraldır
C. Metotların kaç parametre alacağını belirleyen sözleşmedir
D. Nesne üretildikten sonra hiç değişmeyen nitelik değeridir
E. Programın hiç hata vermeyeceğini garanti eden kontroldür
A. Kontrol değişiklikten sonra yapıldığı için bakiye eksiye düşmüş kalır
B.raise bir metot içinde kullanılamaz
C.amount negatif olabileceği için tür denetimi gerekir
D. Sorun yoktur; istisna yükselince değişiklik kendiliğinden geri alınır
E.other nesnesinin özel niteliğine erişilemez
Madde 11. Durumu birden çok adımda değiştiren bir metotta hangi sıra izlenmelidir?
A. Önce durum değiştirilir, sonuç yanlışsa geri alınır
B. Kontrol hiç yapılmaz; çağıran doğru değer göndermekle yükümlüdür
C. Kontroller try/except içine alınır ve hata yutulur
D. Önce bütün ön koşullar sınanır, sonra durum değiştirilir
E. Her adımdan sonra ayrı ayrı kontrol yapılır
TipCevaplar ve gerekçeler
Madde 1. Doğru: B.balance >= 0 ya da 0 <= score <= 100 gibi kurallar nesne kullanıldığı sürece doğru kalmalıdır. Niteliklerin değeri değişir, kural değişmez.
A yanlış: Sınıf niteliği paylaşılan veridir; kural değildir.
C yanlış: Metodun kaç parametre aldığını imzası belirler; bu ayrı bir konudur.
D yanlış: Hiç değişmeyen değer sabittir; invariant’ta ise değer değişebilir, değişmeyen kuraldır.
E yanlış: Kuralı korumanın yolu çoğu zaman hata yükseltmektir; amaç geçersiz değeri engellemektir.
Madde 2. Doğru: E.raise çalıştığı anda __init__ yarıda kesilir ve account adına hiçbir şey bağlanmaz. Nesne ilk andan itibaren geçerli olmalıdır.
A yanlış: Kod değeri kendiliğinden düzeltmez; hata yükseltir.
B yanlış:if kontrolü raise ile nesnenin oluşmasını engeller.
C yanlış:__init__ içinde hata yükseltmek olağan ve önerilen bir kalıptır.
D yanlış: Yazdırılan, yakalanan istisnanın mesajıdır.
Madde 3. Doğru: A.Product("Klavye", -100) çağrısının hemen hata vermesi, negatif fiyatın sistemde dolaşmasından iyidir. Hata ortaya çıktığı yere yakın bildirilirse nedenini bulmak kolaylaşır; geç fark edilen yanlış değer ise ilerideki raporları bozar.
B yanlış: Değeri kullanıldığı yerde denetlemek de akla yatkın görünebilir. Ama ilke tam da bu ertelemeye karşıdır.
C yanlış: “Fast” sözcüğü hızı çağrıştırdığı için bu anlam akla gelebilir. Ama tam tersi savunulur: doğrulama erkene alınır, kaldırılmaz.
D yanlış: Hatalarla çabuk başa çıkmanın yolu hepsini yakalamak gibi görünebilir. Ama her hatayı yakalamak sorunu gizler; bu ilkeye aykırıdır.
E yanlış: “Fail fast” adı, programın hata anında hemen çökmesi gerektiği izlenimini verir. Ama istisna yakalanıp işlenebilir, program sonlanmak zorunda değildir. İlke yalnız sorunun erken bildirilmesini ister.
Madde 4. Doğru: C. Python erişimi engellemez. Kuralı, niteliği değiştiren metotlar ve alt çizgi geleneğine uyan programcılar korur.
A yanlış: Python’da böyle bir engel yoktur; _balance adına da __balance adına da dışarıdan ulaşılabilir.
B yanlış: Çoğu nesnenin nitelikleri değişmek zorundadır; denetlenen, nasıl değiştikleridir.
D yanlış: Kopyalama ayrı bir konudur.
E yanlış: Kapsülleme çoğu zaman metot ekler.
Madde 5. Doğru: B. Tek alt çizgi erişimi engellemez: _balance bir adlandırma geleneğidir, Python bunu zorlamaz. Kontrol yalnız nesne oluşturulurken bir kez çalıştı. Dışarıdan yapılan doğrudan atama bu kontrolden geçmez. Kuralın her değişiklikte denetlenmesi için değişikliği bir metotla ya da property ile yapmak gerekir.
A yanlış: Kural sınıfın içinde yazıldığı için niteliği her zaman koruduğu düşünülebilir. Ama __init__ yalnız nesne oluşturulurken bir kez çalışır. Sonraki atamalar onu yeniden çalıştırmaz.
C yanlış: Alt çizgi “dokunmayın” uyarısı gibi okunduğu için erişimi engellediği sanılabilir. Ama Python’da dışarıdan erişimi engelleyen bir private belirteci yoktur; alt çizgi yalnız niyeti gösterir.
D yanlış: Alt çizgili ad korunuyormuş gibi göründüğü için atamanın etkisiz kaldığı sanılabilir. Ama atama başarılı olur ve değer gerçekten -50 olur.
E yanlış: Geleneğe aykırı bir kullanımın en azından uyarı vereceği beklenebilir. Ama Python alt çizgili bir ada yapılan atama için uyarı vermez. Geleneğe uymak programcıya kalır.
Madde 6. Doğru: C. Alt çizgi yalnız niyeti gösterir; Python erişimi teknik olarak engellemez. Bu geleneğe uyulmasını Python zorlamaz; kodu yazan ekip kendi arasında uyar.
A yanlış: Alt çizgi başka dillerdeki private sözcüğünün karşılığıymış gibi anlatıldığında bu yanılgı doğar. Python’da böyle bir erişim belirteci yoktur; dışarıdan okuma da yazma da yapılabilir.
B yanlış: Alt çizgili ad çoğunlukla bir property arkasında durduğu için bu bağ kurulabilir. property ile birlikte sık kullanılır, ama alt çizgi tek başına bunu söylemez.
D yanlış: Tek ve çift alt çizgi kolayca karıştırılır. Ad dönüştürmesi çift alt çizgide olur.
E yanlış: Alt çizgi sınıfa ait bir işaret gibi görünebilir. Ama niteliğin nesneye ait olduğunu self. ile atanması gösterir.
Madde 7. Doğru: D.property sayesinde normal görünen atama bir metot çağrısına dönüşür ve doğrulamadan geçer.
A yanlış: Python price adına yapılan atamayı setter’a gönderir; nesnede aynı adlı yeni bir nitelik oluşmaz, gölgeleme olmaz.
B yanlış: Setter tanımlı olduğu için atama yapılabilir.
C yanlış: Atama setter’ı çağırır ve hata yükselir.
E yanlış: Setter bunu engeller.
Madde 8. Doğru: E. Üçü de dışarıdan kullanılan price adını taşır. Setter’a farklı ad vermek en sık yapılan hatalardandır.
A yanlış: Dosya adının bir ilgisi yoktur.
B yanlış: İç nitelik genellikle _price olur; parametre adı serbesttir.
C yanlış: Sınıf adının property adıyla ilgisi yoktur.
D yanlış: Setter farklı adla yazılırsa property’ye bağlanmaz ve atama çalışmaz.
Madde 9. Doğru: A. Başlangıç değeri de setter’daki doğrulamadan geçer ve kural tek yerde kalır. Aynı kuralı iki yerde yazarsanız zamanla ikisi birbirini tutmayabilir. Kural tek yerdeyse değiştiğinde yalnız orayı güncellersiniz.
B yanlış:self.price daha sade göründüğü için daha hızlı sanılabilir. Tersine, araya bir metot çağrısı girer; gerekçe hız değildir.
C yanlış: Satır bir property kullandığı için onu tanımlatan şey sanılabilir. Ama property zaten tanımlıdır; bu satır onu kullanır.
D yanlış: Doğrulama nesneyi koruduğu için onu kilitlediği düşünülebilir. Ama setter niteliği kurala uyarak değiştirmek için vardır; nesneyi değiştirilemez yapmaz.
E yanlış: Alt çizgili ad satırdan kalktığı için gizlenmiş gibi görünür. Ama ad yine erişilebilir; kazanç, doğrulamanın tek yerde kalmasıdır.
Madde 10. Doğru: A. Hata yükseldiğinde self._balance zaten negatife düşmüştür; nesne yarım güncellenmiş, geçersiz bir durumda kalır. Doğru sıra önce bütün ön koşulları sınamak, sonra durumu değiştirmektir.
B yanlış:raise örnekleri çoğunlukla fonksiyonlarda görüldüğü için metotta kullanılamayacağı sanılabilir. Ama kullanılabilir; sorun raise satırının değişiklikten sonra gelmesidir.
C yanlış: Negatif amount gerçekten ayrı bir eksik olduğu için cazip gelir. Ama bu bir tür denetimi değil, değer denetimidir; metodu güvensiz yapan da işlemlerin sırasıdır.
D yanlış: İstisna işlemi iptal ediyormuş gibi göründüğü için yapılan değişikliğin de geri alınacağı sanılabilir. Ama Python değişikliği kendiliğinden geri almaz; bakiye eksi kalır.
E yanlış: Alt çizgili bir niteliğe başka bir nesne üzerinden erişmek kurala aykırı görünebilir. Ama tek alt çizgi teknik bir engel değildir; aynı sınıf içinde bu erişim olağandır.
Madde 11. Doğru: D. Böylece hata yükseldiğinde nesne hiç değişmemiş olur ve geçerli durumunu korur.
A yanlış: Geri alma kodu kolayca eksik kalır ve nesne yarım güncellenmiş kalabilir.
B yanlış: İnvariant’ı korumak nesnenin kendi sorumluluğudur.
C yanlış: Hata gizlenir ve geçersiz durum kimse fark etmeden kalıcı olur.
E yanlış: İlk adım geçip ikincisi düşerse nesne yine yarım kalır.
Tek sayfa özet
Değişmez koşul (invariant), nesne için her zaman doğru olması gereken kuraldır; balance >= 0 gibi.
Kapsülleme, niteliklerin yalnız sınıfın kurallı metotlarıyla değişmesidir.
Kural hem __init__ içinde hem niteliği değiştiren her metotta denetlenmelidir.
_name bir erişim yasağı koymaz, yalnız “bu ad içeride kullanılır” der. Kuralı Python değil, sınıfın arayüzü korur.
property, product.price = 900 gibi sade bir atamanın arkasına kontrol koyar.
Getter, @price.setter ve setter aynı adı taşır. __init__ ilk değeri setter’la atarsa doğrulama tek yerde kalır.
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.
property ve 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.