Kapsülleme, property ve geçerli durum
Bir sınıf yazmak, nitelikleri dış dünyaya açıp birkaç metot eklemekten ibaret değildir. Nesnenin her zaman anlamlı bir durumda kalması gerekir. Bu hafta kapsülleme (encapsulation) fikrini, kontrollü durum değişimini ve değişmez koşul (invariant) kavramını kullanacağız.
Bu hafta neleri yapabilmelisiniz?
- Kapsüllemenin amacını “veriyi gizlemek”ten daha geniş biçimde açıklayabilmeli,
- Bir nesnenin invariant’ını belirleyebilmeli,
- Başlangıç durumunu
__init__içinde doğrulayabilmeli, - Durum değişimlerini metotlarla sınırlandırabilmeli,
propertyile kontrollü okuma/yazma arayüzü kurabilmeli,__init__ile setter arasında doğrulama tekrarını önleyebilmeli,- Tek alt çizgi ile çift alt çizginin (
__name) anlamını ayırabilmeli, - Geçersiz girişte nesneyi yarım değiştirilmiş durumda bırakmamalısınız.
Değişmez koşul (invariant) nedir?
Değişmez koşul (invariant), nesnenin geçerli kabul edildiği her anda doğru olması gereken kuraldır.
Bir banka hesabı için:
balance >= 0
Bir ürün için:
price > 0vestock >= 0
Bir ders kaydı için:
0 <= score <= 100
Bu kurallar yalnızca veri doğrulama ayrıntısı değil, nesnenin ne anlama geldiğinin bir parçasıdır.
Geçersiz başlangıcı engellemek
Nesne ilk andan itibaren geçerli olmalıdır.
Kontrollü durum değişimi
_balance başındaki tek alt çizgi “bu niteliği sınıfın iç detayı olarak kabul et” biçiminde bir programcı sözleşmesidir. Python bunu teknik olarak tamamen erişilemez yapmaz.
Amaç, nesnenin durumunun hangi kurallarla değişeceğini tek bir güvenilir arayüzde toplamaktır. Python’da kapsülleme çoğu zaman katı erişim engelinden çok sorumluluk ve arayüz disiplini ile sağlanır.
Tek alt çizgi ve çift alt çizgi aynı şey değildir
Python’da _balance bir konvansiyondur: “bu ad sınıf/modülün iç ayrıntısıdır” mesajı verir. Buna karşılık __balance biçimindeki çift alt çizgi, sınıf gövdesinde name mangling denen ad dönüştürmesini tetikler:
class Account:
def __init__(self):
self.__balance = 100Python bu adı kabaca _Account__balance biçimine dönüştürür. Amaç gerçek bir güvenlik/private mekanizması kurmak değildir; özellikle alt sınıflarda aynı adlı niteliklerin kazara çakışmasını azaltmaktır.
__x “asıl private” değildir
account._Account__balance gibi dönüştürülmüş ada teknik olarak erişilebilir. Bu nedenle çift alt çizgiyi “kimse erişemez” diye öğretmek yanlıştır. Bu derste normal iç durum için çoğunlukla tek alt çizgi (_balance) kullanacağız; __x sözdizimini gördüğünüzde name mangling olduğunu tanımanız yeterlidir.
property ne zaman anlamlı?
Bir niteliği kullanıcıya account.balance biçiminde okunabilir sunmak ama değişimini kontrol altında tutmak isteyebiliriz.
@property
def balance(self):
return self._balanceBu durumda account.balance okunabilir; setter tanımlamadığımız için normal account.balance = -100 ataması yapılamaz.
Bazı durumlarda setter anlamlıdır:
Setter sözdiziminde üç ad aynı olmalıdır
Yukarıdaki örnekte özellikle şuna dikkat edin:
@property
def price(self):
...
@price.setter
def price(self, value):
...Public özelliğin adı price ise getter metodu, @price.setter dekoratöründeki ad ve setter metodunun adı aynı olmalıdır. Öğrencilerin en sık yaptığı hatalardan biri setter metoduna farklı ad vermektir.
__init__ neden setter’ı kullanıyor?
Product.__init__ içinde doğrudan:
self._price = priceyazmak yerine:
self.price = priceyazdı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
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value <= 0:
raise ValueError("Fiyat pozitif olmalı")
self._price = valueBu, doğrulamayı iki yerde kopyalamaktan daha güvenlidir. Kural değişirse tek noktayı güncellersiniz.
Aynı invariant’ı __init__ içinde bir kez, setter içinde bir kez daha yazmak kolayca tutarsızlığa yol açar. Setter gerçekten nesnenin public güncelleme kapısıysa, __init__ de mümkün olduğunda o kapıyı kullanarak başlangıç durumunu doğrulasın.
Fail fast
Geçersiz durum ortaya çıktığında sorunu mümkün olduğunca erken bildirmek genellikle daha güvenlidir:
Product("Klavye", -100)oluşturulurken hata vermesi, negatif fiyatın sistemde dolaşıp çok daha sonra yanlış rapor üretmesinden iyidir.
Yarım güncelleme tehlikesi
def transfer_to(self, other, amount):
self._balance -= amount
if self._balance < 0:
raise ValueError("Yetersiz bakiye")
other._balance += amountKontrol değişiklikten sonra yapılmıştır. Hata üretildiğinde self._balance zaten negatif olmuş olabilir.
Daha güvenli sıra:
- 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:
StudentProductReservation
Örneğin tarihlerin sırası, puan aralığı, pozitif fiyat, negatif olmayan stok gibi kurallar düşünün.
Alıştırma — Güvenli withdraw
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ğıdakileri tartışın:
account.balanceaccount.withdraw(100)rectangle.areaorder.cancel()
Hangileri “durumdan hesaplanan bir özellik” gibi, hangileri “eylem/komut” gibi okunuyor? Python’da her şeyi property yapmak da her şeyi get_...() metoduna dönüştürmek de gerekli değildir. Arayüzün niyetini açık kılın.
Küçük üretim görevi — 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 için getter + setter property kullanın ve __init__ içinde self.price = price üzerinden aynı doğrulamayı yeniden kullanın. sell yetersiz stokta hata vermeli ve stok değerini değiştirmemelidir.
Çalışır kod teslimi: Geçerli ürün, geçersiz başlangıç fiyatı, sonradan geçersiz fiyat ataması, başarılı satış ve yetersiz stok olmak üzere en az beş senaryoyu çalıştırın. Başarısız işlemlerden sonra nesne durumunun değişmediğini assert ile doğrulayın.
Kendinizi kontrol edin
- Invariant nedir?
- Neden doğrulama yalnızca kullanıcı arayüzünde yapılmamalıdır?
_balancePython’da gerçekten erişilemez midir?__balancene yapar; neden gerçek private değildir?@price.setternedenpriceadını kullanır?__init__içindeself.price = pricekullanmanın avantajı nedir?- Neden önce doğrulayıp sonra durumu değiştirmek daha güvenlidir?
Bu haftadan akılda kalması gerekenler
- Kapsülleme, nesnenin geçerli durumunu koruyan bir sorumluluk sınırıdır.
- Invariant, nesnenin her geçerli durumda sağlaması gereken kuraldır.
- Başlangıç ve durum geçişleri doğrulanmalıdır.
_namebir erişim yasağı değil, iç kullanım niyetidir;__nameise name mangling uygular, güvenlik sağlamaz.property, arayüzü sade tutarken kontrol ekleyebilir.- Getter/setter adları tutarlı olmalı;
__init__setter’ı kullanarak doğrulamayı tek noktada tutabilir.