Ana içeriğe geç
Kesit Python
Orta · Nesne yönelimli programlama

Kapsülleme ve iç gözlem: property, __dict__ ve getattr

Örnek ve sınıf değişkenini, paylaşım tuzağını, _korunan ve __gizli adları (name mangling), property ile doğrulamayı, salt okunur alanları ve getattr/setattr/vars ile çalışma anında nesne incelemeyi öğrenirsin.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli?

Ilgaz Bank'ın mobil uygulamasında bir geliştirici, hata ayıklarken şu satırı yazıp unutuyor: hesap.bakiye = -25000. Kod çalışıyor, test geçiyor, ama müşterinin hesabı hiçbir işlem kaydı olmadan eksiye düşüyor. Sınıflar dersinde nesnenin veriyi ve davranışı bir arada tuttuğunu gördün. Bu derste o verinin nasıl korunacağını (kapsülleme, encapsulation) ve bir nesnenin içine çalışma anında nasıl bakılacağını (iç gözlem, introspection) öğreneceksin. İkisi de PCAP sınavının 4. bölümünün çekirdeğidir ve gerçek projelerde her gün kullanılır: doğrulayan setter'lar, salt okunur alanlar, nesneyi dinamik olarak JSON'a çeviren yardımcılar, eklenti sistemleri.

Kapsüllemenin özü şudur: nesnenin iç durumunu yalnızca kendi metotları değiştirsin, dışarıya kontrollü bir kapı açılsın. Python bunu derleyici zoruyla değil, sözleşme ve araçlarla yapar: alt çizgi adlandırması, name mangling ve property.

Örnek değişkeni ve sınıf değişkeni: arama sırası

Sınıflar dersinde ikisinin farkını gördün; şimdi işin mekaniğine bakalım. nesne.ad yazdığında Python önce nesnenin sözlüğüne, bulamazsa sınıfın sözlüğüne, sonra üst sınıflara bakar. Okurken bu zincir izlenir; ama atama her zaman nesnenin kendi sözlüğüne yazar. Bu yüzden k1.oran = 35 sınıf değişkenini değiştirmez, onu gölgeleyen (shadowing) yeni bir örnek değişkeni yaratır.

class Kur:
    oran = 32.5            # sınıf değişkeni: tüm nesneler paylaşır

k1 = Kur()
k2 = Kur()
k1.oran = 33.0             # k1'e ÖZEL yeni değişken; sınıfa dokunmaz
Kur.oran = 34.0            # sınıfı değiştir: gölgelenmeyen herkes görür
print(k1.oran, k2.oran, Kur.oran)
print(k1.__dict__, k2.__dict__)
del k1.oran                # gölgeyi kaldır
print(k1.oran)             # artık sınıfınkini görür

Asıl tehlike değiştirilebilir (mutable) bir sınıf değişkenidir. Liste ya da sözlük sınıf gövdesinde tanımlanıp self.liste.append(...) ile değiştirilirse atama olmadığı için gölge oluşmaz; herkes aynı listeye yazar.

class Musteri:
    siparisler = []                    # TUZAK: tek liste, herkes paylaşır
    def __init__(self, ad):
        self.ad = ad
    def siparis_ver(self, no):
        self.siparisler.append(no)

a = Musteri("Ayşe")
b = Musteri("Burak")
a.siparis_ver("AM-101")
print(b.siparisler)                     # Burak'ın siparişi yok ama görünüyor!

class MusteriDuzgun:
    def __init__(self, ad):
        self.ad = ad
        self.siparisler = []           # her nesneye kendi listesi
c = MusteriDuzgun("Can")
print(c.siparisler, "siparisler" in vars(c))
KuralSınıf değişkeni olarak yalnızca değişmez (immutable) değerler tut: sabitler, oranlar, sayaçlar. Liste, sözlük, küme gibi kaplar her zaman __init__ içinde self. ile oluşturulur.

__dict__ ve vars(): nesnenin içi

Her nesnenin niteliklerini tutan bir sözlüğü vardır: nesne.__dict__. vars(nesne) aynı sözlüğü döndürür ve daha okunurdur. Sınıfın da bir __dict__'i vardır; metotlar, sınıf değişkenleri ve property'ler oradadır. Sınıfınki salt okunur bir mappingproxy'dir, doğrudan yazamazsın.

class Urun:
    kdv = 0.20
    def __init__(self, ad, fiyat):
        self.ad = ad
        self.fiyat = fiyat
    def kdvli(self):
        return round(self.fiyat * (1 + self.kdv), 2)

u = Urun("Kulaklık", 1000)
print(vars(u))                                  # yalnız örnek verisi
print([k for k in vars(Urun) if not k.startswith("__")])
print(type(vars(Urun)).__name__)
u.__dict__["stok"] = 7                          # sözlüğe yazmak = nitelik eklemek
print(u.stok, u.kdvli())

vars() özellikle hata ayıklarken ve nesneyi sözlüğe/JSON'a çevirirken işe yarar. Ama dikkat: property'ler ve sınıf değişkenleri nesnenin sözlüğünde görünmez.

Tek alt çizgi, çift alt çizgi ve name mangling

Python'da private anahtar sözcüğü yoktur. Bunun yerine iki adlandırma kuralı vardır:

YazımAnlamıDışarıdan erişim
bakiyeGenel (public) arayüzSerbest
_bakiyeKorunan (protected): "iç ayrıntı, dokunma" sözleşmesiTeknik olarak serbest; from m import * almaz
__bakiyeGizli (private): ad karıştırma (name mangling) uygulanır_Hesap__bakiye adıyla erişilir
__init__Dunder (özel metot): iki yanda çift alt çizgiKarıştırma YOK

Sınıf gövdesinde __ad biçimindeki (sonunda çift alt çizgi olmayan) her ad, derleme sırasında _SinifAdi__ad olarak yeniden yazılır. Amaç gizlemek değil, alt sınıflarla ad çakışmasını önlemektir: alt sınıf da __bakiye tanımlarsa o _AltSinif__bakiye olur, üst sınıfınkini ezmez.

class Hesap:
    def __init__(self, sahip, bakiye):
        self.sahip = sahip
        self._limit = 5000          # korunan: sözleşme
        self.__bakiye = bakiye      # gizli: ad karıştırılır

h = Hesap("Ayşe", 1200)
print(sorted(vars(h)))
try:
    print(h.__bakiye)
except AttributeError as e:
    print("Hata:", e)
print(h._Hesap__bakiye)              # mümkün ama YAPMA
h.__bakiye = 0                       # sınıf dışında karıştırma olmaz: YENİ nitelik
print(sorted(vars(h)))

Son satır PCAP'te sevilen bir tuzaktır: sınıf dışında yazılan h.__bakiye = 0 karıştırılmaz; nesneye __bakiye adında ikinci bir nitelik ekler, gerçek bakiye yerinde durur.

property: kontrollü kapı

property, bir metodu nitelik gibi okutur ve atamayı kendi kodundan geçirir. Getter (okuyucu), setter (yazıcı) ve deleter (silici) olmak üzere üç parçası vardır. Böylece dış arayüz urun.fiyat = 250 gibi sade kalır ama içeride doğrulama çalışır.

class Urun:
    def __init__(self, ad, fiyat):
        self.ad = ad
        self.fiyat = fiyat          # setter'dan geçer, doğrulanır

    @property
    def fiyat(self):
        return self._fiyat

    @fiyat.setter
    def fiyat(self, deger):
        if not isinstance(deger, (int, float)) or deger < 0:
            raise ValueError(f"Geçersiz fiyat: {deger!r}")
        self._fiyat = round(float(deger), 2)

    @fiyat.deleter
    def fiyat(self):
        print("Fiyat kaldırıldı")
        del self._fiyat

u = Urun("Termos", 349.999)
print(u.fiyat)
try:
    u.fiyat = -10
except ValueError as e:
    print(e)
print(vars(u))
del u.fiyat
print(hasattr(u, "fiyat"))

İki ayrıntıya dikkat: değer _fiyat adında saklanır (property ile aynı adı kullanırsan setter kendini sonsuza kadar çağırır, RecursionError). Ve kurucuda self.fiyat = fiyat yazmak bilinçli bir seçimdir: doğrulama nesne ilk oluşturulurken de çalışır.

Salt okunur nitelik

Yalnız getter tanımlarsan nitelik salt okunur olur; atama AttributeError verir. Bakiye, müşteri numarası, oluşturma tarihi gibi değerler için idealdir. Bakiye değişecekse bunu yatir/cek gibi kurallı metotlar yapar.

class Hesap:
    def __init__(self, no, bakiye=0.0):
        self._no = no
        self.__bakiye = float(bakiye)

    @property
    def no(self):
        return self._no

    @property
    def bakiye(self):
        return self.__bakiye

    def yatir(self, tutar):
        if tutar <= 0:
            raise ValueError("Tutar pozitif olmalı")
        self.__bakiye += tutar

h = Hesap("AB-001", 500)
h.yatir(250)
print(h.no, h.bakiye)
try:
    h.bakiye = 1_000_000
except AttributeError as e:
    print("Engellendi:", e)

getattr, setattr, hasattr, delattr: adı çalışma anında belli olan nitelikler

Niteliğin adı bir değişkende (ör. formdan ya da CSV başlığından gelen bir metinde) duruyorsa nokta sözdizimi işe yaramaz. Dört yerleşik fonksiyon bu işi yapar:

FonksiyonEşdeğeriNot
getattr(n, "ad")n.adÜçüncü argüman varsayılan: getattr(n, "ad", None) hata vermez
setattr(n, "ad", d)n.ad = dProperty setter'ı da çalışır
hasattr(n, "ad")getattr dener, AttributeError olursa FalseProperty içindeki AttributeError da False'a döner
delattr(n, "ad")del n.adDeleter varsa onu çağırır
class Calisan:
    def __init__(self, ad, departman, maas):
        self.ad, self.departman, self.maas = ad, departman, maas

c = Calisan("Elif", "İK", 42000)
guncelleme = {"departman": "Finans", "maas": 45000, "unvan": "Uzman"}
for alan, deger in guncelleme.items():
    if hasattr(c, alan):
        setattr(c, alan, deger)
    else:
        print("Bilinmeyen alan atlandı:", alan)
print(vars(c))
print(getattr(c, "unvan", "belirtilmemiş"))
metot_adi = "upper"
print(getattr(c.ad, metot_adi)())     # metotlar da nitelik
Varsayılanı unutmagetattr(n, ad) nitelik yoksa AttributeError fırlatır. Dışarıdan gelen adlarla çalışırken ya varsayılan ver ya da önce hasattr ile sor.

İç gözlem nitelikleri: __name__, __module__, __bases__, __class__

Sınıflar da nesnedir ve kendileri hakkında bilgi taşır. PCAP bunları doğrudan sorar:

class Hesap:
    pass
class VadeliHesap(Hesap):
    pass

v = VadeliHesap()
print(VadeliHesap.__name__)                     # sınıfın adı
print(VadeliHesap.__module__)                   # tanımlandığı modül
print(VadeliHesap.__bases__)                    # doğrudan üst sınıflar (demet)
print(v.__class__.__name__, type(v) is VadeliHesap)
print(isinstance(v, Hesap), type(v) is Hesap)   # isinstance kalıtımı gözetir
print(issubclass(VadeliHesap, Hesap), issubclass(Hesap, object))
print(Hesap.__bases__)

type(n) nesnenin tam sınıfını verir; isinstance ise alt sınıfları da kabul eder. "Bu bir Hesap mı?" sorusunda neredeyse her zaman isinstance doğrudur. __bases__ yalnız doğrudan üst sınıfları içerir; tüm zinciri görmek için __mro__ kullanılır (çoklu kalıtım (inheritance) dersinde).

classmethod ile alternatif kurucu

Bir sınıfı farklı kaynaklardan (CSV satırı, sözlük, API yanıtı) kurmak istediğinde __init__'i şişirmek yerine @classmethod ile adlandırılmış kurucular yazarsın. İlk parametre cls'dir; cls(...) çağırdığın için alt sınıflarda da doğru türü üretir.

class Hesap:
    _sayac = 0                                   # değişmez sayaç: sınıf değişkeni uygun

    def __init__(self, sahip, bakiye=0.0):
        Hesap._sayac += 1
        self.no = f"AB-{Hesap._sayac:04d}"
        self.sahip = sahip
        self.bakiye = float(bakiye)

    @classmethod
    def satirdan(cls, satir):
        sahip, bakiye = satir.split(";")
        return cls(sahip.strip(), float(bakiye))

    @classmethod
    def sozlukten(cls, d):
        return cls(d["sahip"], d.get("bakiye", 0))

    def __repr__(self):
        return f"{type(self).__name__}({self.no}, {self.sahip}, {self.bakiye:.2f})"

print(Hesap.satirdan("Ayşe Kaya; 1500.5"))
print(Hesap.sozlukten({"sahip": "Burak Demir"}))
print(Hesap._sayac)

Sayaçta Hesap._sayac += 1 yazdık, self._sayac += 1 değil. İkincisi okumayı sınıftan yapar ama sonucu nesneye yazar; sayaç hiç artmamış gibi görünür. Gölgeleme kuralının pratik sonucu budur.

Gerçek işteBankacılık ve sağlık yazılımlarında denetim (audit) ekipleri "bakiye/teşhis alanı nerede değiştirilebiliyor?" diye sorar. Cevabın "yalnız şu iki metotta" olması kod incelemesini (code review) dakikalara indirir. Salt okunur property ve tek giriş noktalı metotlar bu cevabı mümkün kılar. Ayrıca pydantic, SQLAlchemy, Django ORM gibi kütüphaneler içeride tam olarak bu dersteki araçları kullanır: setattr ile alan doldurma, __dict__ ile serileştirme, property ile hesaplanan alanlar.
Uzmanlaş

"Hepimiz yetişkiniz" felsefesi

Python topluluğunun sözü: "We are all consenting adults." Dil seni _Hesap__bakiye'ye erişmekten alıkoymaz; alt çizgi bir uyarı levhasıdır, kilit değil. Gerçek güvenlik (yetkilendirme, şifreleme) uygulama katmanında sağlanır. Kapsülleme ise hata önleme ve bakım kolaylığı içindir. Bu yüzden çoğu proje __gizli yerine tek alt çizgi _korunan kullanır; çift alt çizgiyi yalnızca kalıtımda ad çakışması gerçek bir risk olduğunda seçer.

Önce düz nitelik, sonra property

Java alışkanlığıyla her alana get_x/set_x yazma. Python'da alanı düz nitelik olarak başlat; ileride doğrulama gerekirse aynı adla property'ye çevir. Çağıran kodun tek satırı bile değişmez. Bu, property'nin varlık sebebidir.

__getattr__ ve __setattr__

__getattr__(self, ad) yalnız normal arama başarısız olduğunda çağrılır; eksik alanlara varsayılan vermek ya da başka bir nesneye yönlendirmek (delegation) için kullanılır. __setattr__ ise her atamada çalışır; içinde self.x = ... yazarsan sonsuz döngüye girersin, super().__setattr__(ad, deger) kullanmalısın. Bu derste bir meydan okuma görevinde göreceksin. Tekrarlanan doğrulama mantığını birçok sınıfta kullanmak için descriptor'lar ve __slots__ İleri OOP dersinde.

PCAP için hızlı kontrol listesi

  • hasattr(Sinif, "x") sınıf değişkenleri için True, yalnız __init__'te atanan örnek değişkenleri için False'tur.
  • __name__ sınıfta vardır, sıradan bir nesnede yoktur: n.__class__.__name__ yazılır.
  • __bases__ demettir; object'ten türeyen sınıf için (<class 'object'>,).
  • Name mangling yalnız sınıf gövdesi içinde yazılan adlara uygulanır.
Alıştırmalara geç (18)