Neden önemli?
Ilgaz Bank'ta bir hesabı temsil etmek için şimdiye kadar sözlük kullandın: {"no": "TR01", "sahip": "Ayşe", "bakiye": 1200}. Para çekme fonksiyonunu ayrı yazdın, bakiye kontrolünü başka bir yere koydun; üç ay sonra "bakiye" anahtarını birisi "balance" yazdı ve raporlar sessizce boş geldi. Sınıf (class), veriyi ve o veriye ait davranışı tek bir çatı altında toplar: hesap kendi bakiyesini bilir, kendi para çekme kuralını uygular, kendini okunur biçimde yazdırır. Nesne yönelimli programlama (OOP) büyük projelerin, kütüphanelerin ve iş mantığının yazılma biçimidir; bu derste temeli kuruyoruz.
class, __init__ ve self
class bir kalıp tanımlar; o kalıptan üretilen her nesneye örnek (instance) denir. __init__ örnek oluşturulurken otomatik çalışan kurucu metottur. self, metodun üzerinde çalıştığı örneğin kendisidir; Python onu ilk parametre olarak otomatik geçirir.
class Hesap:
def __init__(self, no, sahip, bakiye=0.0):
self.no = no # örnek niteliği (instance attribute)
self.sahip = sahip
self.bakiye = bakiye
def yatir(self, tutar):
self.bakiye += tutar
def cek(self, tutar):
if tutar > self.bakiye:
return False
self.bakiye -= tutar
return True
h = Hesap("TR01", "Ayşe Kaya", 1200.0)
h.yatir(300)
print(h.cek(2000))
print(h.cek(500))
print(h.sahip, h.bakiye)
# h.cek(500) aslında Hesap.cek(h, 500) demektir
print(Hesap.cek(h, 100), h.bakiye)
self der; farklı yazmak kodu okuyan herkesi yavaşlatır.__repr__ ve __str__: nesneyi yazdırmak
Yukarıdaki h'yi doğrudan yazdırırsan <__main__.Hesap object at 0x...> görürsün; işe yaramaz. __repr__ geliştiriciye yönelik, mümkünse nesneyi yeniden oluşturacak biçimde bir dize döndürür; __str__ son kullanıcıya yönelik güzel gösterimdir. Yalnızca birini yazacaksan __repr__ yaz: __str__ tanımlı değilse Python ona düşer.
class Hesap:
def __init__(self, no, sahip, bakiye=0.0):
self.no, self.sahip, self.bakiye = no, sahip, bakiye
def __repr__(self):
return f"Hesap(no={self.no!r}, sahip={self.sahip!r}, bakiye={self.bakiye})"
def __str__(self):
return f"{self.no} - {self.sahip}: {self.bakiye:,.2f} TL"
h = Hesap("TR01", "Ayşe Kaya", 1200.0)
print(h) # __str__
print(repr(h)) # __repr__
print([h]) # liste içinde __repr__ kullanılır
{x!r}, repr(x) çağırır: dizeler tırnaklı görünür, böylece "TR01" ile TR01 karışmaz.Sınıf niteliği ile örnek niteliği
__init__ içinde self.x = ... ile tanımlanan nitelik her örneğe aittir. Sınıf gövdesine doğrudan yazılan nitelik ise tüm örneklerce paylaşılır: sabitler, sayaçlar, varsayılan ayarlar için kullanılır.
class Hesap:
faiz_orani = 0.30 # sınıf niteliği: tüm hesaplar için ortak
adet = 0 # kaç hesap açıldı
def __init__(self, no, bakiye=0.0):
self.no = no # örnek niteliği
self.bakiye = bakiye
Hesap.adet += 1 # sınıf niteliğini güncelle
def yillik_faiz(self):
return round(self.bakiye * self.faiz_orani, 2)
a = Hesap("TR01", 1000)
b = Hesap("TR02", 5000)
print(Hesap.adet)
print(a.yillik_faiz(), b.yillik_faiz())
Hesap.faiz_orani = 0.35 # tüm örnekler etkilenir
print(a.yillik_faiz(), b.yillik_faiz())
a.faiz_orani = 0.10 # yalnızca a için örnek niteliği oluşur, sınıfı gölgeler
print(a.yillik_faiz(), b.yillik_faiz())
islemler = [] satırını sınıf gövdesine yazarsan tüm hesaplar aynı listeyi paylaşır; birine eklenen işlem hepsinde görünür. Liste, sözlük gibi değiştirilebilir nitelikleri her zaman __init__ içinde self.islemler = [] olarak tanımla.property: metodu nitelik gibi okutmak
Bazen bir değer saklanmaz, hesaplanır (kullanılabilir bakiye = bakiye + kredili mevduat). Bazen de doğrudan atanmasını engellemek, kural koymak istersin. @property bir metodu parantezsiz okunan nitelik yapar; @ad.setter ise atamada çalışacak doğrulamayı ekler.
class Hesap:
def __init__(self, no, bakiye=0.0, limit=0.0):
self.no = no
self._bakiye = bakiye # alt çizgi: "dışarıdan doğrudan dokunma"
self.limit = limit
@property
def bakiye(self):
return self._bakiye
@bakiye.setter
def bakiye(self, deger):
if deger < -self.limit:
raise ValueError(f"bakiye {-self.limit} altına inemez")
self._bakiye = deger
@property
def kullanilabilir(self): # salt okunur, hesaplanan nitelik
return self._bakiye + self.limit
h = Hesap("TR01", 500, limit=1000)
print(h.bakiye, h.kullanilabilir)
h.bakiye = -800 # setter çalışır, limit içinde
print(h.bakiye, h.kullanilabilir)
try:
h.bakiye = -2000
except ValueError as e:
print("Reddedildi:", e)
try:
h.kullanilabilir = 1
except AttributeError as e:
print("Salt okunur:", e)
| Yapı | Nasıl erişilir | Ne zaman |
|---|---|---|
Örnek niteliği self.x | h.x | Her nesnenin kendi verisi |
| Sınıf niteliği | Hesap.x ya da h.x | Tüm örneklerde ortak sabit/sayaç |
| Metot | h.cek(100) | Nesnenin davranışı |
@property | h.kullanilabilir | Hesaplanan ya da korunan değer |
@classmethod | Hesap.csv_den(...) | Alternatif kurucu, sınıf düzeyi iş |
@staticmethod | Hesap.iban_gecerli(...) | Sınıfla ilgili ama ne self ne cls kullanan yardımcı |
classmethod ve staticmethod
@classmethod ilk parametre olarak örneği değil sınıfı (cls) alır; en yaygın kullanımı farklı girdiden nesne üreten alternatif kuruculardır. @staticmethod ne örnek ne sınıf alır; sadece sınıfın ad alanında yaşayan sıradan bir fonksiyondur.
class Hesap:
def __init__(self, no, sahip, bakiye=0.0):
self.no, self.sahip, self.bakiye = no, sahip, bakiye
def __repr__(self):
return f"Hesap({self.no!r}, {self.sahip!r}, {self.bakiye})"
@classmethod
def csv_den(cls, satir):
no, sahip, bakiye = satir.split(",")
return cls(no.strip(), sahip.strip(), float(bakiye))
@staticmethod
def no_gecerli(no):
return no.startswith("TR") and no[2:].isdigit()
print(Hesap.csv_den("TR07, Mehmet Demir, 2500.50"))
print(Hesap.no_gecerli("TR07"), Hesap.no_gecerli("07TR"))
hesaplar = [Hesap.csv_den(s) for s in ["TR01,Ayşe,100", "TR02,Can,250"]]
print(sum(h.bakiye for h in hesaplar))
Bir araya getirelim: işlem geçmişli Hesap
from datetime import date
class Hesap:
para_birimi = "TL"
def __init__(self, no, sahip, bakiye=0.0):
self.no = no
self.sahip = sahip
self._bakiye = bakiye
self.islemler = [] # her örneğe ait, __init__ içinde
@property
def bakiye(self):
return self._bakiye
def _kaydet(self, tur, tutar):
self.islemler.append((date.today().isoformat(), tur, tutar))
def yatir(self, tutar):
if tutar <= 0:
raise ValueError("tutar pozitif olmalı")
self._bakiye += tutar
self._kaydet("yatırma", tutar)
def cek(self, tutar):
if tutar > self._bakiye:
raise ValueError("yetersiz bakiye")
self._bakiye -= tutar
self._kaydet("çekme", tutar)
def __repr__(self):
return f"Hesap({self.no!r}, bakiye={self._bakiye} {self.para_birimi})"
h = Hesap("TR01", "Ayşe Kaya")
h.yatir(1000)
h.cek(250)
try:
h.cek(5000)
except ValueError as e:
print("Hata:", e)
print(h)
print(len(h.islemler), "işlem;", [t for _, t, _ in h.islemler])
Derinleş: nesnelerle gerçek tasarım
Buraya kadar tek bir sınıf yazmayı öğrendin. Gerçek projelerde ise sınıflar birbirini içerir, fonksiyonlara geçirilir, kopyalanır, sıralanır ve "geçersiz durumda hiç oluşmasın" diye korunur. Bu bölümde PCAP'ın ve iş görüşmelerinin sevdiği beş konuyu derinleştiriyoruz: nesne kimliği ve takma adlar (aliasing), kopyalama, kompozisyon, değişmezler (invariant) ve metot zincirleme. Dunder metotların tamamını (karşılaştırma, __getitem__, __iter__) "İleri OOP" dersinde, property ve getattr ayrıntılarını "Kapsülleme" dersinde göreceksin.
1. Değişken nesneyi değil, nesneye giden oku tutar
h2 = h1 yeni bir hesap açmaz; aynı nesneye ikinci bir ad (takma ad, alias) verir. Fonksiyona nesne geçirdiğinde de kopyası değil kendisi gider; fonksiyon içinde h.bakiye = 0 yazarsan dışarıdaki hesap da sıfırlanır. == "değer olarak eşit mi?" diye sorar; is ise "aynı nesne mi?" diye. __eq__ yazmadığın sınıflarda ikisi aynı sonucu verir, çünkü varsayılan eşitlik kimlik karşılaştırmasıdır.
class Hesap:
def __init__(self, no, bakiye):
self.no = no
self.bakiye = bakiye
def faiz_isle(h, oran): # parametre aynı nesneyi gösterir
h.bakiye = round(h.bakiye * (1 + oran), 2)
h1 = Hesap("TR01", 1000)
h2 = h1 # takma ad: yeni nesne YOK
faiz_isle(h2, 0.05)
print(h1.bakiye) # h1 de değişti
h3 = Hesap("TR01", 1050.0) # aynı veriler, AYRI nesne
print(h1 == h3, h1 is h3, h1 is h2)
print(id(h1) == id(h2))
faiz_isle, guncelle). Değiştirmemesi gerekiyorsa yeni nesne döndürsün. Ekip içindeki hataların önemli kısmı "bu fonksiyon benim nesnemi değiştirmiş" sürprizinden çıkar.2. Kopyalamak: copy.copy ve copy.deepcopy
Bir nesnenin bağımsız bir kopyası gerektiğinde copy modülünü kullanırsın. Sığ kopya (shallow copy, copy.copy) yeni bir nesne yaratır ama içindeki liste ve sözlükler hâlâ ortaktır. Derin kopya (deep copy, copy.deepcopy) iç içe her şeyi de kopyalar. Anadolu Market'te bir kampanyayı şablondan çoğaltırken derin kopya şarttır; yoksa yeni kampanyaya eklediğin kural eskisine de eklenir.
import copy
class Kampanya:
def __init__(self, ad, kurallar):
self.ad = ad
self.kurallar = kurallar # liste: değiştirilebilir
sablon = Kampanya("Yaz", ["min_tutar=500"])
sig = copy.copy(sablon)
derin = copy.deepcopy(sablon)
sig.ad = "Yaz-2" # düz nitelik: bağımsız
sig.kurallar.append("kargo_bedava") # liste ORTAK
derin.kurallar.append("yuzde=10") # liste ayrı
print(sablon.ad, sablon.kurallar)
print(derin.kurallar)
print(sig.kurallar is sablon.kurallar, derin.kurallar is sablon.kurallar)
| İşlem | Yeni nesne? | İçteki liste/sözlük | Ne zaman |
|---|---|---|---|
b = a | Hayır | Ortak | Aynı nesneye ikinci ad |
copy.copy(a) | Evet | Ortak | İçinde yalnız değişmez değerler varsa |
copy.deepcopy(a) | Evet | Kopyalanır | Şablondan bağımsız nesne üretmek |
Kendi kopyala() metodun | Evet | Sen seçersin | Kimlik (no, id) sıfırlanacaksa |
3. Kompozisyon: "sahiptir" ilişkisi
Bir sipariş birden çok kalem içerir; bir kalem bir sipariş değildir. Bu "sahiptir" (has-a) ilişkisine kompozisyon (composition) denir ve gerçek kodda kalıtımdan (inheritance) çok daha sık kullanılır. Kurallar: her sınıf kendi işini bilsin (Kalem kendi tutarını hesaplar), dış sınıf iç nesnelere iş devretsin (delegation).
class Kalem:
def __init__(self, urun, fiyat, adet=1):
self.urun, self.fiyat, self.adet = urun, fiyat, adet
def tutar(self):
return self.fiyat * self.adet
class Siparis:
def __init__(self, no):
self.no = no
self.kalemler = [] # Siparis, Kalem nesnelerine SAHİP
def ekle(self, kalem):
self.kalemler.append(kalem)
return self # zincirleme için
def toplam(self):
return sum(k.tutar() for k in self.kalemler) # işi kaleme devret
def en_pahali(self):
return max(self.kalemler, key=lambda k: k.tutar())
s = Siparis("AM-501")
s.ekle(Kalem("Kulaklık", 750)).ekle(Kalem("Kablo", 90, 3))
print(s.toplam(), s.en_pahali().urun)
4. Değişmezler: geçersiz nesne hiç doğmasın
Değişmez (invariant), nesnenin ömrü boyunca hep doğru kalması gereken kuraldır: "bakiye eksiye inmez", "randevu saati mesai içindedir", "maaş pozitiftir". Bu kuralı __init__ içinde ve durumu değiştiren her metotta denetlersin; ihlalde ValueError fırlatırsın. Böylece hata kaynağına en yakın yerde patlar (fail fast), üç modül ötede sessizce bozuk rapor üretmez.
class Personel:
def __init__(self, ad, maas):
if not ad.strip():
raise ValueError("ad boş olamaz")
if maas <= 0:
raise ValueError(f"maaş pozitif olmalı: {maas}")
self.ad = ad.strip()
self.maas = maas
def zam_yap(self, yuzde):
if not 0 < yuzde <= 50:
raise ValueError("zam oranı 0-50 arasında olmalı")
self.maas = round(self.maas * (1 + yuzde / 100), 2)
return self.maas
p = Personel(" Elif Şahin ", 40000)
print(p.ad, p.zam_yap(12.5))
for ad, maas in [("", 30000), ("Can", -5)]:
try:
Personel(ad, maas)
except ValueError as e:
print("Reddedildi:", e)
5. Nesne listelerini sıralamak ve süzmek
Gerçekte tek nesneyle değil, yüzlercesiyle çalışırsın. sorted, max, min ve comprehension'lar nesnelerle de çalışır; key parametresine bir nitelik okuyan fonksiyon verirsin. operator.attrgetter("bakiye"), lambda h: h.bakiye ile aynı işi yapar ve birden çok niteliği aynı anda alabilir.
from operator import attrgetter
class Hesap:
def __init__(self, no, sube, bakiye):
self.no, self.sube, self.bakiye = no, sube, bakiye
def __repr__(self):
return f"{self.no}({self.bakiye})"
hesaplar = [Hesap("TR03", "Ankara", 900), Hesap("TR01", "İzmir", 2500),
Hesap("TR02", "Ankara", 2500)]
print(sorted(hesaplar, key=lambda h: (-h.bakiye, h.no)))
print(sorted(hesaplar, key=attrgetter("sube", "no")))
print([h.no for h in hesaplar if h.sube == "Ankara"])
print(max(hesaplar, key=attrgetter("bakiye")))
max listede ilk karşılaştığını döndürür. Eşitlikte belirli bir kural istiyorsan anahtara ikinci bir öğe ekle.6. Metot zincirleme ve None tuzağı
Durumu değiştiren metot return self derse s.ekle(a).ekle(b) gibi akıcı (fluent) bir arayüz kurarsın; pandas ve SQLAlchemy'deki .filter().order_by() yazımı buna dayanır. Ama bir metotta return unutulursa None döner ve zincirin ikinci halkası AttributeError: 'NoneType' object has no attribute ... verir. Bu hata mesajını gördüğünde ilk bakacağın yer zincirdeki bir önceki metottur. Python'un kendi list.append ve list.sort'u bilinçli olarak None döndürür: "yerinde değiştiren metot yeni değer döndürmez" sözleşmesi. Kendi sınıfında hangisini seçtiğini tutarlı uygula.
Alt çizgi sözleşmesi
Python'da gerçek "private" yoktur. _bakiye tek alt çizgi "iç kullanım, dışarıdan dokunma" uyarısıdır; __bakiye çift alt çizgi ise ad karıştırma (name mangling) ile _Hesap__bakiye olur ve alt sınıflarla çakışmayı önler. Günlük kodda tek alt çizgi yeterlidir.
__eq__ ve __hash__
İki Hesap nesnesi varsayılan olarak yalnızca aynı nesneyse eşittir. "Aynı hesap numarası = aynı hesap" istiyorsan __eq__ yazarsın; o zaman küme ve sözlük anahtarı olarak kullanmak için __hash__ da tanımlaman gerekir. Bir sonraki derslerde dataclass bunları senin yerine üretecek.
__slots__ ile bellek
Milyonlarca küçük nesne yaratacaksan __slots__ = ("no", "bakiye") satırı her örnekteki __dict__ sözlüğünü kaldırıp belleği ciddi azaltır; karşılığında örneğe sonradan yeni nitelik ekleyemezsin. "Bellek ve iç yapı" dersinde ölçeceksin.
Ne zaman sınıf, ne zaman sözlük?
Veri sadece taşınıyorsa ve davranışı yoksa sözlük ya da dataclass yeter. Verinin kuralları (bakiye eksiye inemez), davranışı (çek, yatır) ve tutarlılığı (işlem kaydı) varsa sınıf yaz. Fonksiyonlarına hep aynı sözlüğü ilk parametre olarak geçiriyorsan bu bir sınıf olmak istiyor demektir.