# Python'da sınıflar ve OOP: __init__, self, metotlar, kalıtım ve dataclass

URL: https://skillaria.top/tr/guides/python/python-siniflar-ve-oop

Python'da sınıf, alanları ve metotları tanımlayan bir şablondur; nesneler (örnekler) sınıf çağrılarak oluşturulur. __init__ alanları doldurur, self ise üzerinde çalışılan nesneyi gösterir. Kalıtım üst sınıfın davranışını alıp bazı metotları değiştirmeye izin verir; @dataclass ise ağırlıklı olarak veri tutan sınıflar için __init__, __repr__ ve __eq__ metotlarını otomatik üretir.

Python'da sınıf, bir nesnenin hangi verileri tuttuğunu ve bu verilerle neler yapılabileceğini tanımlayan bir şablondur. Nesne (örnek), bu şablondan üretilmiş somut bir kopyadır. Python'daki nesne yönelimli programlama üç şeye dayanır: metotlu sınıflar, kalıtım ve `__init__` ya da `__repr__` gibi özel metotlar. Hepsini tek bir örnekte, harcama takibi üzerinden adım adım inceleyelim.

## Sözlük varken neden sınıf kullanılır

Verileri bir sözlükte de tutabilirsiniz: `{"title": "Kahve", "amount": 250}`. Sorun şu ki sözlük hangi anahtarların doğru olduğunu bilmez. `expense.get("amout", 0)` gibi bir yazım hatası hata vermez; sıfır döner ve rapor sessizce yanlış hesaplar.

Sınıf nesnesinin alanları önceden tanımlıdır. Var olmayan bir özniteliğe erişmek hemen `AttributeError` verir ve modern Python doğru ismi de önerir. Üstelik sınıf verileri ve onlarla yapılan işlemleri tek bir yerde toplar: harcama, kendi KDV'li tutarını hesaplayabilir; rapor, toplamı kendisi bulur.

## Python'da sınıf nasıl oluşturulur

Sınıf `class` anahtar sözcüğüyle tanımlanır; PEP 8 gereği adlar CamelCase biçiminde yazılır. En basit sınıf örneği:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("Kahve", 250)
taxi = Expense("Taksi", 300)
print(coffee.title, coffee.amount)   # Kahve 250
```

`Expense("Kahve", 250)` çağrısı yeni bir örnek oluşturur. `coffee` ve `taxi` aynı türden iki farklı nesnedir ve her birinin kendi alan değerleri vardır. Türünü `isinstance(coffee, Expense)` ile kontrol edebilirsiniz; sonuç `True` olur.

### __init__: sınıfın başlatılması

`__init__` sıklıkla sınıfın kurucusu (constructor) olarak anılır, ama daha doğru adı başlatıcıdır. Nesneyi asıl oluşturan `__new__` metodudur; `__init__` ise hemen ardından çağrılır ve gelen argümanları alanlara dağıtır. Bu metottan bir değer döndürmeniz gerekmez (aslında `None` döndürmek zorundadır).

`__init__` içinde girdileri doğrulamak, programda hatalı nesnelerin oluşmasını önler:

```
class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("Tutar negatif olamaz")
        self.title = title
        self.amount = amount
```

### self nedir

`self`, metodun üzerinde çalıştığı nesnenin kendisine verilen referanstır. `coffee.with_tax()` yazdığınızda Python aslında `Expense.with_tax(coffee)` çağrısını yapar ve nesneyi ilk argüman olarak geçirir. Bu yüzden metot tanımında `self` ilk parametredir, çağrıda ise yazılmaz.

`self` bir anahtar sözcük değil, bir gelenektir: parametreyi istediğiniz adla adlandırabilirsiniz, ama kimse öyle yapmaz. Yeni başlayanların sık yaptığı hata, metot tanımında `self`'i unutmaktır. Bu durumda çağrı şu hatayla çöker: `TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given`.

## Metotlar ve özel metotlar

Metot, sınıfın içinde tanımlanan bir fonksiyondur. `self` üzerinden nesnenin alanlarına erişir:

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("Kahve", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('Kahve', 250)
```

`__repr__` olmasaydı çıktı `<__main__.Expense object at 0x7f...>` gibi olurdu; hata ayıklama için işe yaramazdı. İki alt çizgiyle başlayıp biten metotlara özel (dunder) metotlar denir ve Python onları kendisi çağırır. `__str__` kullanıcıya dönük güzel çıktıyı, `__eq__` `==` karşılaştırmasını, `__len__` ise `len(obj)` çağrısını yönetir.

### @classmethod ve @staticmethod

Sıradan bir metot örnekle çalışır. `@classmethod` süsleyicisiyle tanımlanan metot ilk argüman olarak sınıfın kendisini (`cls`) alır; bu da onu alternatif bir kurucu yapmak için idealdir. `@staticmethod` ise ne nesneyi ne de sınıfı alır: sınıfla mantıksal olarak ilişkili sıradan bir fonksiyondur.

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("Öğle yemeği;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False
```

### Yıkıcı metot: __del__

`__del__` metodu, nesne çöp toplayıcı tarafından silinmeden önce çağrılır. Kaynakları serbest bırakmak için buna güvenmeyin: çağrının ne zaman yapılacağı garanti değildir. Dosyalar ve bağlantılar için `with` bağlam yöneticisini ya da açık bir `close()` metodunu kullanın.

## Sınıf özniteliği ve örnek özniteliği

`self.ad = ...` ile tanımlanan alanlar belirli bir nesneye aittir. Sınıf gövdesinde doğrudan tanımlanan değişkenler ise sınıf öznitelikleridir ve tüm örnekler tarafından paylaşılır. Sabitler genellikle bu şekilde tutulur.

```
class Expense:
    CURRENCY = "TRY"          # sınıf özniteliği, sabit

    def __init__(self, title, amount):
        self.title = title    # örnek öznitelikleri
        self.amount = amount

a = Expense("Kahve", 250)
b = Expense("Çay", 150)
print(a.CURRENCY, Expense.CURRENCY)   # TRY TRY
```

Tehlikeli bir tuzak, değiştirilebilir sınıf özniteliğidir. Gövdede `rows = []` tanımlayıp `self.rows.append(...)` ile doldurursanız, liste tüm nesneler arasında ortak olur. Değiştirilebilir alanları her zaman `__init__` içinde oluşturun.

Bir nesnenin alanlarını `vars(obj)` ya da `obj.__dict__` ile görebilirsiniz; adı bir metin olarak verilen bir özniteliği ise `getattr(obj, "amount")` ile okursunuz.

## Özellikler: @property

Özellik (property), dışarıdan parantez olmadan öznitelik gibi okunan bir metottur. Hesaplanan değerler için idealdir: rapor toplamını ayrıca saklamanıza ve güncel tutmanıza gerek kalmaz.

```
class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("Kahve", 250))
r.add(Expense("Taksi", 300))
print(r.total)   # 550
```

Yazmayı denetlemek istiyorsanız özelliğe `@total.setter` ile bir ayarlayıcı eklersiniz. Ayarlayıcı yoksa `r.total = 0` ataması `AttributeError` verir.

Kapsülleme konusunda: Python'da katı anlamda özel alan yoktur. Tek alt çizgi (`_onbellek`) "dışarıdan dokunmayın" anlamına gelen bir anlaşmadır; çift alt çizgi (`__gizli`) ise ad bozma (name mangling) uygular, böylece alt sınıfın aynı adlı alanı yanlışlıkla ezmesi engellenir.

## Python'da kalıtım

Kalıtım, bir üst sınıftan türeyen alt sınıf oluşturmanızı sağlar: alt sınıf üst sınıfın tüm alanlarını ve metotlarını alır, istediklerini de yeniden tanımlar. Üst sınıf parantez içinde yazılır.

```
class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # üst sınıfın __init__ metodunu çağır
        self.month = month

    def add(self, expense):       # metodu yeniden tanımlama
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # üst sınıfın davranışını koru

m = MonthlyReport("ekim")
m.add(Expense("Kahve", 250))   # [ekim] + Expense('Kahve', 250)
print(m.total)                 # 250 — özellik miras alındı
```

`super()`, üst sınıfın metotlarını çağırmak için bir aracı nesne döndürür. Alt sınıfın `__init__` metodunda `super().__init__()` çağrısını unutmak klasik bir hatadır: `rows` alanı oluşmaz ve ilk `add` çağrısı `AttributeError` ile düşer.

Akrabalığı `issubclass(MonthlyReport, Report)` ile sorgulayabilirsiniz. Python 3'teki tüm sınıflar örtük olarak `object`'ten türer; temel `__repr__`, `__eq__` gibi metotlar buradan gelir. Python çoklu kalıtımı da destekler (`class C(A, B)`); metotların arama sırası `C.__mro__` ile görülür.

### Python'da soyut sınıf

Soyut sınıf bir arayüz tanımlar: alt sınıfların hangi metotları uygulamak zorunda olduğunu belirler. Bu sınıftan doğrudan nesne üretilemez. Bunun için `abc` modülü kullanılır:

```
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...
```

Tüm soyut metotları uygulamayan bir alt sınıf da soyut kalır ve nesne oluşturulurken aynı hatayı verir.

## Kalıtım yerine kompozisyon

Kalıtımı öğrenince insanın aklına katmanlı hiyerarşiler gelir: temel harcama, parasal harcama, TL harcaması... ve bu şekilde beş seviye. Python'da bu tarz derin hiyerarşiler nadiren yazılır. Kalıtım "bir" ilişkisini (is-a), kompozisyon ise "içerir" ilişkisini (has-a) anlatır: rapor bir liste değildir, bir `rows` alanında harcama listesi tutar.

Pratik kural: bir nesnenin yeteneğini yalnızca kullanmak istiyorsanız, onu bir alana koyun; kalıtım almayın. Veri ağırlıklı yapılar için çoğu zaman dataclass yeterlidir, mantık ise sıradan fonksiyonlarda tutulabilir.

## Python dataclass: şablon kodu olmayan sınıf

Esas olarak veri tutan sınıflar için `__init__`, `__repr__` ve `__eq__` metotlarını elle yazmak yorucudur. `dataclasses` modülündeki `@dataclass` süsleyicisi bunları alan tanımlarındaki tür ipuçlarından üretir:

```
from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Tutar negatif olamaz")

e = Expense("Kahve", 250)
print(e)                           # Expense(title='Kahve', amount=250, tags=[])
print(e == Expense("Kahve", 250))  # True
```

Daha önce `__init__` içinde yazdığınız doğrulamayı `__post_init__`'e taşıyabilirsiniz: bu metot üretilen başlatıcıdan hemen sonra çağrılır. Değiştirilebilir varsayılan değerler yalnızca `field(default_factory=list)` ile verilebilir; `tags: list = []` yazarsanız dataclass bunu reddeder ve `ValueError` verir.

### frozen ve slots

`frozen=True` parametresi nesneyi değiştirilemez yapar; `slots=True` (Python 3.10'dan itibaren) ise `__slots__` üretir ve bellekten tasarruf sağlar. Dondurulmuş bir nesnenin alanını değiştirmeye çalışmak `FrozenInstanceError` verir. Yeni bir değerle nesne elde etmek için kopyasını oluşturursunuz:

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("Kahve", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='Kahve', amount=250) Expense(title='Kahve', amount=300)
```

Python 3.13'ten itibaren aynı işi genel amaçlı `copy.replace()` fonksiyonu da yapar. Dondurulmuş dataclass nesneleri kümelerde kullanılabilir ve sözlük anahtarı olabilir; çünkü `__hash__` metoduna sahiptirler.

## Sınıflarda yenilikler: Self, @override, __slots__

Sınıfları tür açısından tanımlamak için iki faydalı araç var. `typing.Self` (Python 3.11, PEP 673), "aynı sınıftan bir nesne dönecek" anlamına gelir ve kalıtılan metotlarda da çalışır. `@override` süsleyicisi (Python 3.12, PEP 698) ise bir metodun üst sınıftaki bir metodu yeniden tanımladığını işaretler: adı yanlış yazarsanız mypy ya da pyright, böyle bir üst metot olmadığını bildirir.

```
from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # zincir halinde çağrılabilir

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)
```

Python 3.15 dar kapsamlı bir kısıtlamayı kaldırdı: `tuple`'dan türeyen alt sınıflarda boş olmayan `__slots__` tanımlanabiliyor; ayrıca `__dict__` ve `__weakref__` her sınıfın `__slots__` listesine yazılabiliyor. Python 3.14'te tuple alt sınıfında böyle bir tanım `TypeError: nonempty __slots__ not supported for subtype of 'tuple'` hatasına yol açıyordu.

## Sınıflarda sık yapılan hatalar

| Hata | Belirti | Çözüm |
| --- | --- | --- |

| Metotta `self` unutulmuş | `TypeError: ... takes 0 positional arguments but 1 was given` | Metodun ilk parametresi her zaman `self` |

| `__init__` içinde `self.` olmadan alan | Değer `__init__` bittiğinde kaybolur | `self.amount = amount` yazın |

| Sınıf özniteliği olarak liste | Veriler nesneler arasında karışır | Listeleri `__init__` içinde ya da `default_factory` ile oluşturun |

| `super().__init__()` yok | Üst sınıfın alanlarında `AttributeError` | Üst sınıfın başlatıcısını çağırın |

| Özelliği parantezle çağırmak | `TypeError: 'int' object is not callable` | Özelliği parantezsiz okuyun: `r.total` |

## Özet

Python'da OOP'nin başlangıcı basittir: sınıf alanları ve metotları tanımlar, `__init__` alanları doldurur, `self` üzerinde çalışılan nesneyi gösterir. Kalıtım ve `super()` davranışı genişletmeye, soyut sınıflar arayüz tanımlamaya, `@property` ise hesaplanan alanlar oluşturmaya yarar. Veri ağırlıklı sınıflar için `@dataclass` kullanın; derin hiyerarşiler yerine kompozisyonu tercih edin. Sınıflar ve dataclass'lar kursta harcama takipçisi örneğiyle uygulamalı olarak işlenir.

## FAQ

**Sınıf ile nesne arasındaki fark nedir?**

Sınıf türün tanımıdır: hangi alan ve metotlara sahip olduğunu söyler. Nesne ya da örnek, Expense("Kahve", 250) gibi sınıf çağrılarak oluşturulur ve kendi alan değerlerini tutar.

**Sınıfta mutlaka __init__ yazmam gerekir mi?**

Hayır. Tanımlanmazsa object'ten miras alınan, argüman kabul etmeyen bir başlatıcı kullanılır. Dataclass ise bunu dekoratörle kendisi üretir.

**__str__ ile __repr__ arasındaki fark nedir?**

__repr__ geliştirici ve hata ayıklayıcı için kesin bir gösterimdir; __str__ ise print ve f-metinlerde kullanıcıya gösterilen çıktıdır. __str__ yoksa Python __repr__ kullanır.

**Dataclass ne zaman, sıradan sınıf ne zaman seçilmeli?**

Sınıf ağırlıklı olarak veri tutuyor ve hazır __init__, __repr__, karşılaştırma isteniyorsa dataclass uygundur. Başlatma mantığı karmaşıksa ya da davranış veriden daha önemliyse sıradan sınıf daha iyidir.

**Python'da özel (private) alanlar var mı?**

Katı anlamda yoktur. Başta tek alt çizgi bir "dışarıdan kullanılmasın" anlaşmasıdır; çift alt çizgi ise ad bozma yaparak alt sınıfın aynı adlı alanı yanlışlıkla ezmesini engeller.


---
Source: Skillaria.Top (https://skillaria.top)
