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))
__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ım | Anlamı | Dışarıdan erişim |
|---|---|---|
bakiye | Genel (public) arayüz | Serbest |
_bakiye | Korunan (protected): "iç ayrıntı, dokunma" sözleşmesi | Teknik olarak serbest; from m import * almaz |
__bakiye | Gizli (private): ad karıştırma (name mangling) uygulanır | _Hesap__bakiye adıyla erişilir |
__init__ | Dunder (özel metot): iki yanda çift alt çizgi | Karış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:
| Fonksiyon | Eşdeğeri | Not |
|---|---|---|
getattr(n, "ad") | n.ad | Üçüncü argüman varsayılan: getattr(n, "ad", None) hata vermez |
setattr(n, "ad", d) | n.ad = d | Property setter'ı da çalışır |
hasattr(n, "ad") | getattr dener, AttributeError olursa False | Property içindeki AttributeError da False'a döner |
delattr(n, "ad") | del n.ad | Deleter 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
getattr(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.
setattr ile alan doldurma, __dict__ ile serileştirme, property ile hesaplanan alanlar."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.