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

Sınıflar: veri ve davranışı bir arada tutmak

class, __init__, self, nitelik ve metotlarla kendi veri tipini tanımlar; __repr__, sınıf niteliği ve property ile Ilgaz Bank Hesap sınıfını kurarsın.

Uygulamada aç, 21 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'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 bir anahtar kelime değilİlk parametreye istediğin adı verebilirsin ama tüm Python dünyası 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
!r ne yapıyor?f-string içinde {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())
Değiştirilebilir sınıf niteliği tuzağı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şilirNe zaman
Örnek niteliği self.xh.xHer nesnenin kendi verisi
Sınıf niteliğiHesap.x ya da h.xTüm örneklerde ortak sabit/sayaç
Metoth.cek(100)Nesnenin davranışı
@propertyh.kullanilabilirHesaplanan ya da korunan değer
@classmethodHesap.csv_den(...)Alternatif kurucu, sınıf düzeyi iş
@staticmethodHesap.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ş

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))
Sessiz yan etkiBir fonksiyon kendisine verilen nesneyi değiştiriyorsa bunu adında ya da belgesinde söyle (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)
İşlemYeni nesne?İçteki liste/sözlükNe zaman
b = aHayırOrtakAynı nesneye ikinci ad
copy.copy(a)EvetOrtakİçinde yalnız değişmez değerler varsa
copy.deepcopy(a)EvetKopyalanırŞablondan bağımsız nesne üretmek
Kendi kopyala() metodunEvetSen seçersinKimlik (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)
Tasarım kısayoluGereksinim metnindeki isimler sınıf adayıdır (sipariş, kalem, müşteri), fiiller metot adayıdır (ekle, iptal et, öde), sıfatlar nitelik ya da property adayıdır (toplam, geçerli, gecikmiş). "X bir Y midir?" sorusuna evet diyorsan kalıtım, "X'in bir Y'si var mı?" sorusuna evet diyorsan kompozisyon düşün.

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")))
Neden max TR01 verdi?İki hesap eşit en büyük bakiyedeyse 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.

Gerçek işteKod incelemelerinde (code review) en sık yorumlar şunlardır: "Bu fonksiyon parametreyi değiştiriyor, adı bunu söylemiyor", "Bu nesne geçersiz değerle oluşturulabiliyor, kurucuda doğrula", "Varsayılan argüman olarak liste verilmiş". Bir sınıf yazdıktan sonra kendine üç soru sor: Geçersiz durumda oluşturulabilir mi? Dışarıdan verilen liste/sözlüğü kopyalamadan mı sakladım? Hangi metotlar nesneyi değiştiriyor ve bu adlarından anlaşılıyor mu?
Uzmanlaş

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.

Alıştırmalara geç (21)