Temeller

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

Güncellendi: · Skillaria.Top

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

HataBelirtiÇözüm
Metotta self unutulmuşTypeError: ... takes 0 positional arguments but 1 was givenMetodun ilk parametresi her zaman self
__init__ içinde self. olmadan alanDeğer __init__ bittiğinde kaybolurself.amount = amount yazın
Sınıf özniteliği olarak listeVeriler nesneler arasında karışırListeleri __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ğırmakTypeError: '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.

Sık sorulan sorular

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.

Bu konuyu videoyla öğrenin

Bu konu “Python 3.15: от новичка до профи” kursunda uygulamalı olarak ayrıntılı işleniyor.

Kursa git