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,
  • property ile 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 > 0 ve stock >= 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.

Nesne durumunun yalnızca kurallı davranışlar üzerinden değiştirilerek invariantın korunmasını gösteren diyagram.

Kapsülleme ve invariant
ImportantKapsülleme = erişimi yasaklamak değildir

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 = 100

Python 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.

Warning__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 ve setter kullanımının bir niteliğe erişim ve değişiklik için kontrollü bir arayüz sağladığını gösteren diyagram.

property erişim kapısı
@property
def balance(self):
    return self._balance

Bu 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 = 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

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value <= 0:
            raise ValueError("Fiyat pozitif olmalı")
        self._price = value

Bu, doğrulamayı iki yerde kopyalamaktan daha güvenlidir. Kural değişirse tek noktayı güncellersiniz.

ImportantTek doğrulama noktası

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 += amount

Kontrol değişiklikten sonra yapılmıştır. Hata üretildiğinde self._balance zaten negatif olmuş olabilir.

Daha güvenli sıra:

  1. Tüm ön koşulları kontrol et,
  2. 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

Ö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.balance
  • account.withdraw(100)
  • rectangle.area
  • order.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

  1. Invariant nedir?
  2. Neden doğrulama yalnızca kullanıcı arayüzünde yapılmamalıdır?
  3. _balance Python’da gerçekten erişilemez midir?
  4. __balance ne yapar; neden gerçek private değildir?
  5. @price.setter neden price adını kullanır?
  6. __init__ içinde self.price = price kullanmanın avantajı nedir?
  7. 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.
  • _name bir erişim yasağı değil, iç kullanım niyetidir; __name ise 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.
Back to top