← Kursa Dön
📄 Text · 15 min

Kalıtım ve super()

Diyelim ki bir oyun yazıyorsun. Savaşçı, okçu ve büyücü sınıfların var. Her birinin adı, canı, seviyesi var. Her biri hareket edebilir, hasar alabilir. Ama her birinin kendine özel yetenekleri de var. Bu ortak özellikleri her sınıfta tekrar tekrar mı yazacaksın?

Tabii ki hayır. İşte kalıtım (inheritance) tam da bu sorunu çözer.


Kalıtım Nedir?

Kalıtım, bir sınıfın başka bir sınıftan özellik ve davranışları devralması mekanizmasıdır. Miras alan sınıf, miras veren sınıfın tüm attribute ve metodlarını otomatik olarak kullanabilir.

Aile Ağacı Analojisi 🌳

Kalıtımı bir aile ağacı gibi düşün. Sen anne-babandan bazı özellikleri miras alırsın — göz rengi, boy, saç tipi. Ama kendi özelliklerini de geliştirirsin. Anne-babanın özelliklerini "kopyalamıyorsun", onlardan devralıyorsun ve üzerine kendi özelliklerini ekliyorsun.

Programlamada da öyle:

  • Parent (ebeveyn) sınıf: Temel özellikleri tanımlar. Buna base class veya superclass da denir.

  • Child (çocuk) sınıf: Parent'tan miras alır, yeni özellikler ekler veya mevcut olanları değiştirir. Buna derived class veya subclass da denir.

# Parent class
class Character:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp
        self.level = 1

    def take_damage(self, amount):
        self.hp -= amount
        if self.hp <= 0:
            return f"{self.name} yenildi!"
        return f"{self.name} {amount} hasar aldı. HP: {self.hp}"

    def level_up(self):
        self.level += 1
        return f"{self.name} seviye atladı! Seviye: {self.level}"

# Child classes
class Warrior(Character):
    def slash(self):
        return f"{self.name} kılıçla saldırıyor!"

class Archer(Character):
    def shoot(self):
        return f"{self.name} ok atıyor!"

class Mage(Character):
    def cast_spell(self):
        return f"{self.name} büyü yapıyor!"
# Kullanım
w = Warrior("Arthas", 150)
a = Archer("Legolas", 100)
m = Mage("Gandalf", 80)

# Parent'ın metodları çalışır
print(w.take_damage(30))  # Arthas 30 hasar aldı. HP: 120
print(a.level_up())       # Legolas seviye atladı! Seviye: 2

# Child'a özel metodlar
print(w.slash())           # Arthas kılıçla saldırıyor!
print(m.cast_spell())     # Gandalf büyü yapıyor!

Warrior, Archer ve Mage sınıfları Character'dan miras alıyor. name, hp, take_damage(), level_up() gibi ortak şeyleri tekrar yazmadık — sadece kendilerine özel metodları ekledik.


class Child(Parent): Söz Dizimi

Kalıtım söz dizimi çok basit — sınıf adından sonra parantez içinde parent sınıfı yazarsın:

class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

    def speak(self):
        return f"{self.name}: {self.sound}!"

class Dog(Animal):        # Animal'dan miras alıyor
    def fetch(self):
        return f"{self.name} topu getirdi!"

class Cat(Animal):        # Animal'dan miras alıyor
    def purr(self):
        return f"{self.name} mırıldanıyor..."

dog = Dog("Karabaş", "Hav hav")
cat = Cat("Boncuk", "Miyav")

print(dog.speak())   # Karabaş: Hav hav!
print(dog.fetch())   # Karabaş topu getirdi!
print(cat.speak())   # Boncuk: Miyav!
print(cat.purr())    # Boncuk mırıldanıyor...

Parantez yazmazsan sınıf object'ten miras alır (Python 3'te her sınıf zaten object'ten miras alır):

class Foo:         # Aslında class Foo(object) ile aynı
    pass

print(Foo.__bases__)  # (<class 'object'>,)

Kalıtım Terminolojisi

Farklı kaynaklarda farklı isimler görebilirsin, hepsi aynı anlama gelir:

Miras verenMiras alan
Parent classChild class
Base classDerived class
SuperclassSubclass
Üst sınıfAlt sınıf

super() Kullanımı

super(), parent sınıfın metodlarına erişmeni sağlar. En yaygın kullanımı __init__ içinde parent'ın constructor'ını çağırmaktır.

class Vehicle:
    def __init__(self, brand, year):
        self.brand = brand
        self.year = year
        self.is_running = False

    def start(self):
        self.is_running = True
        return f"{self.brand} çalıştırıldı."

    def info(self):
        return f"{self.year} {self.brand}"

class ElectricCar(Vehicle):
    def __init__(self, brand, year, battery_capacity):
        super().__init__(brand, year)  # Parent'ın __init__'ini çağır
        self.battery_capacity = battery_capacity
        self.battery_level = 100

    def charge(self):
        self.battery_level = 100
        return f"{self.brand} şarj edildi."

    def info(self):  # Override
        base = super().info()  # Parent'ın info()'sunu çağır
        return f"{base} (Elektrikli, {self.battery_capacity}kWh)"

tesla = ElectricCar("Tesla", 2024, 75)
print(tesla.start())   # Tesla çalıştırıldı. — parent metodu
print(tesla.charge())  # Tesla şarj edildi. — child metodu
print(tesla.info())    # 2024 Tesla (Elektrikli, 75kWh) — override

super() Olmadan Ne Olur?

class Parent:
    def __init__(self, x):
        self.x = x
        print(f"Parent init: x={x}")

class Child(Parent):
    def __init__(self, x, y):
        # super().__init__(x) çağırmadık!
        self.y = y
        print(f"Child init: y={y}")

c = Child(1, 2)
# Child init: y=2

# print(c.x)  # AttributeError! Parent'ın __init__'i çağrılmadı!
print(c.y)    # 2

⚠️ Dikkat: Child sınıfta __init__ tanımlıyorsan, super().__init__() çağırmayı unutma! Yoksa parent'ın attribute'ları oluşturulmaz ve AttributeError alırsın. Bu, yeni başlayanların en sık yaptığı hatalardan biri.

super() Farklı Metodlarda

super() sadece __init__'te değil, herhangi bir override metodda parent'ın versiyonunu çağırmak için kullanılabilir:

class FileHandler:
    def save(self, data):
        print(f"Dosya kaydediliyor: {len(data)} byte")
        return True

class LoggingFileHandler(FileHandler):
    def save(self, data):
        print(f"[LOG] save() çağrıldı, veri boyutu: {len(data)}")
        result = super().save(data)  # Parent'ın save'ini çağır
        print(f"[LOG] save() tamamlandı, sonuç: {result}")
        return result

handler = LoggingFileHandler()
handler.save("merhaba dünya")
# [LOG] save() çağrıldı, veri boyutu: 13
# Dosya kaydediliyor: 13 byte
# [LOG] save() tamamlandı, sonuç: True

Method Overriding

Child sınıf, parent'ın bir metodunu aynı isimle yeniden tanımlayarak davranışını değiştirebilir. Buna method overriding denir.

class Shape:
    def area(self):
        return 0

    def describe(self):
        return f"Bu bir {self.__class__.__name__}. Alanı: {self.area():.2f}"

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):  # Override!
        import math
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):  # Override!
        return self.width * self.height

c = Circle(5)
r = Rectangle(4, 6)

print(c.describe())  # Bu bir Circle. Alanı: 78.54
print(r.describe())  # Bu bir Rectangle. Alanı: 24.00

# describe() parent'ta tanımlı ama area() çağırdığında
# child'ın override edilmiş versiyonunu çalıştırıyor!

Bu çok güçlü bir mekanizma. describe() parent'ta tanımlı ama area() çağırdığında child'ın override'ını kullanıyor. Bu, polimorfizmin temelidir (bir sonraki derste detaylıca göreceğiz).

Tamamen Yeni vs Genişletme

Override ederken iki yaklaşım var:

Yaklaşım 1: Tamamen Yeni — Parent'ın metodunu kullanmadan sıfırdan yazarsın.

class Logger:
    def log(self, message):
        print(f"[LOG] {message}")

class TimestampLogger(Logger):
    def log(self, message):
        # Tamamen yeni — parent'ın log'unu KULLANMIYORUZ
        from datetime import datetime
        now = datetime.now().strftime("%H:%M:%S")
        print(f"[{now}] {message}")

Yaklaşım 2: Genişletme — Parent'ın metodunu çağırıp üzerine eklersin.

class VerboseLogger(Logger):
    def log(self, message):
        # Genişletme — parent'ın log'unu ÇAĞIRIYORUZ + ekstra iş
        super().log(message)  # Parent'ın log'unu çağır
        print(f"[DETAIL] Message length: {len(message)}")

vl = VerboseLogger()
vl.log("Sistem başladı")
# [LOG] Sistem başladı
# [DETAIL] Message length: 14

__init__ Override

__init__ override etmek en yaygın senaryodur:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

class Manager(Employee):
    def __init__(self, name, salary, department, team_size):
        super().__init__(name, salary)  # Parent'ın init'i
        self.department = department     # Yeni attribute
        self.team_size = team_size       # Yeni attribute

    def __str__(self):
        return (f"{self.name} — {self.department} Müdürü "
                f"(Takım: {self.team_size} kişi)")

class Developer(Employee):
    def __init__(self, name, salary, languages):
        super().__init__(name, salary)
        self.languages = languages

    def __str__(self):
        langs = ", ".join(self.languages)
        return f"{self.name} — Developer ({langs})"

m = Manager("Ahmet", 50000, "IT", 12)
d = Developer("Zeynep", 40000, ["Python", "JavaScript", "Go"])

print(m)  # Ahmet — IT Müdürü (Takım: 12 kişi)
print(d)  # Zeynep — Developer (Python, JavaScript, Go)

isinstance() ve issubclass()

Bu iki fonksiyon, kalıtım ilişkilerini kontrol etmek için kullanılır:

isinstance(): Nesne-Sınıf İlişkisi

class Animal:
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

class GoldenRetriever(Dog):
    pass

rex = GoldenRetriever()

print(isinstance(rex, GoldenRetriever))  # True
print(isinstance(rex, Dog))              # True — Dog'dan miras aldı
print(isinstance(rex, Animal))           # True — Animal'dan da miras aldı
print(isinstance(rex, Cat))              # False

# Tuple ile birden fazla tip kontrolü
print(isinstance(rex, (Dog, Cat)))       # True — en az biri eşleşirse

issubclass(): Sınıf-Sınıf İlişkisi

print(issubclass(GoldenRetriever, Dog))     # True
print(issubclass(GoldenRetriever, Animal))  # True
print(issubclass(Dog, Cat))                 # False
print(issubclass(Animal, Animal))           # True — kendisi de subclass!

type() vs isinstance()

rex = GoldenRetriever()

# type() TAM eşleşme arar
print(type(rex) == GoldenRetriever)  # True
print(type(rex) == Dog)              # False — tam eşleşme yok!
print(type(rex) == Animal)           # False

# isinstance() kalıtım zincirini kontrol eder
print(isinstance(rex, GoldenRetriever))  # True
print(isinstance(rex, Dog))              # True
print(isinstance(rex, Animal))           # True

💡 İpucu: Tip kontrolü yapacaksan neredeyse her zaman isinstance() kullan. type() kalıtımı görmezden gelir ve kodunu kırılgan yapar. Tek istisna: tam olarak "bu sınıftan mı, alt sınıf değil mi?" diye kontrol etmek istediğin durum.


Kalıtım vs Composition: "is-a" vs "has-a"

Kalıtım her derde deva değil. Bazen composition (bileşim) daha iyi bir seçim.

"is-a" İlişkisi → Kalıtım

"X bir Y'dir" diyebiliyorsan kalıtım mantıklı:

  • Köpek bir hayvandır → class Dog(Animal)

  • Elektrikli araba bir araçtır → class ElectricCar(Vehicle)

  • Öğrenci bir kişidir → class Student(Person)

"has-a" İlişkisi → Composition

"X'in bir Y'si vardır" diyorsan composition kullan:

  • Araba bir motora sahiptir → self.engine = Engine()

  • Üniversite öğrencilere sahiptir → self.students = [Student(...)]

  • Bilgisayar bir CPU'ya sahiptir → self.cpu = CPU()

# ❌ YANLIŞ — Kalıtım ile
class Engine:
    def __init__(self, hp):
        self.hp = hp
    def start(self):
        return f"{self.hp}HP motor çalıştı!"

class Car(Engine):  # Araba bir motor MU? Hayır!
    pass

# ✅ DOĞRU — Composition ile
class Engine:
    def __init__(self, hp):
        self.hp = hp
    def start(self):
        return f"{self.hp}HP motor çalıştı!"

class Car:
    def __init__(self, brand, engine_hp):
        self.brand = brand
        self.engine = Engine(engine_hp)  # Arabanın bir motoru VAR

    def start(self):
        engine_msg = self.engine.start()
        return f"{self.brand}: {engine_msg}"

car = Car("Toyota", 150)
print(car.start())  # Toyota: 150HP motor çalıştı!

Karışık Kullanım — Gerçek Dünya

Gerçek dünyada genellikle ikisi bir arada kullanılır:

class Battery:
    def __init__(self, capacity):
        self.capacity = capacity
        self.level = 100

    def charge(self):
        self.level = 100
        return "Şarj tamamlandı."

    def drain(self, amount):
        self.level = max(0, self.level - amount)

class GPS:
    def __init__(self):
        self.location = (41.0082, 28.9784)  # İstanbul

    def get_location(self):
        return f"Konum: {self.location}"

class Vehicle:
    def __init__(self, brand, year):
        self.brand = brand
        self.year = year

    def start(self):
        return f"{self.brand} çalıştı."

    def __str__(self):
        return f"{self.year} {self.brand}"

class ElectricCar(Vehicle):  # Kalıtım: ElectricCar IS-A Vehicle
    def __init__(self, brand, year, battery_cap):
        super().__init__(brand, year)
        self.battery = Battery(battery_cap)  # Composition: HAS-A Battery
        self.gps = GPS()                      # Composition: HAS-A GPS

    def drive(self, km):
        drain = km * 0.2  # km başına %0.2 şarj
        self.battery.drain(drain)
        return f"{km}km gidildi. Batarya: %{self.battery.level:.0f}"

    def charge(self):
        return self.battery.charge()

tesla = ElectricCar("Tesla", 2024, 75)
print(tesla.start())              # Tesla çalıştı.
print(tesla.drive(50))            # 50km gidildi. Batarya: %90
print(tesla.gps.get_location())   # Konum: (41.0082, 28.9784)
print(tesla.charge())             # Şarj tamamlandı.

Kalıtım vs Composition Karar Tablosu

KriterKalıtımComposition
İlişki tipi"is-a" (X bir Y'dir)"has-a" (X'in Y'si var)
BağımlılıkSıkı (tight coupling)Gevşek (loose coupling)
EsneklikAz (parent değişirse child kırılabilir)Çok (bileşen değiştirilebilir)
Kod tekrarıAz (miras ile paylaşım)Delegation gerektirir
Runtime değişiklikZor (sınıf sabit)Kolay (bileşen değiştirilebilir)

💡 İpucu: "Kalıtım mı, composition mı?" diye şüpheye düştüğünde composition tercih et. Kalıtım güçlü bir araçtır ama aşırı kullanımı kodunu kırılgan yapar. "Favor composition over inheritance" yazılım dünyasının en bilinen prensiplerinden biridir.


Protected (_) ve Private (__) Kalıtımda Davranışı

Erişim belirleyicileri kalıtımda nasıl davranır?

_protected: Alt Sınıftan Erişilebilir

Tek alt çizgi convention'u "bu iç kullanım için" der ama alt sınıflar erişebilir. Aslında buna "protected" denmesinin nedeni budur — dışarıdan değil ama alt sınıftan erişilebilir:

class Animal:
    def __init__(self, name):
        self._name = name        # "protected" — alt sınıf erişebilir
        self._energy = 100

    def _rest(self):
        """İç metod — alt sınıflar kullanabilir."""
        self._energy = min(100, self._energy + 20)

class Dog(Animal):
    def play(self):
        self._energy -= 30      # Alt sınıftan erişim OK
        if self._energy < 20:
            self._rest()        # Protected metoda erişim OK
        return f"{self._name} oynadı! Enerji: {self._energy}"

    def status(self):
        return f"{self._name}: %{self._energy} enerji"

d = Dog("Rex")
print(d.play())    # Rex oynadı! Enerji: 70
print(d.play())    # Rex oynadı! Enerji: 40
print(d.play())    # Rex oynadı! Enerji: 30 (10 olacaktı ama _rest çağrıldı)
print(d.status())  # Rex: %30 enerji

__private: Name Mangling Etkisi

Çift alt çizgi ile name mangling uygulanır. Alt sınıfta aynı isimle erişmeye çalışırsan farklı bir attribute oluşur:

class Animal:
    def __init__(self, name):
        self.__id = id(self)     # Name mangling → _Animal__id

    def get_id(self):
        return self.__id         # Animal scope'unda → _Animal__id

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.__id = f"DOG-{id(self)}"  # _Dog__id — FARKLI attribute!

    def show_ids(self):
        # self.__id → _Dog__id
        print(f"Dog id: {self.__id}")
        # Parent'ın __id'sine erişmek zor
        print(f"Animal id: {self._Animal__id}")

d = Dog("Rex", "Golden")
d.show_ids()
# Dog id: DOG-140234567890
# Animal id: 140234567890

print(d.__dict__)
# {'_Animal__id': 140234567890, '_Dog__id': 'DOG-140234567890'}

Kalıtımda Erişim Özeti

ErişimParent metodChild metodDışarıdan
self.public
self._protected⚠️ (yapabilirsin ama yapma)
self.__private❌ (farklı mangled name)

⚠️ Dikkat: Çift alt çizgiyi (__) kalıtımda kasıtlı olarak kullan — sadece alt sınıfın bu attribute'u yanlışlıkla ezmesini engellemek istediğin durumlarda. Genel kullanımda tek alt çizgi (_) yeterlidir.


Kalıtım Zinciri

Kalıtım birden fazla seviye derinliğinde olabilir:

class LivingThing:
    def breathe(self):
        return "Nefes alıyor..."

class Animal(LivingThing):
    def move(self):
        return "Hareket ediyor..."

class Dog(Animal):
    def bark(self):
        return "Hav hav!"

class GoldenRetriever(Dog):
    def fetch(self):
        return "Topu getirdi!"

gr = GoldenRetriever()

# Tüm zincirdeki metodlar erişilebilir
print(gr.fetch())     # GoldenRetriever
print(gr.bark())      # Dog
print(gr.move())      # Animal
print(gr.breathe())   # LivingThing

# MRO (Method Resolution Order)
print(GoldenRetriever.__mro__)
# (GoldenRetriever, Dog, Animal, LivingThing, object)

Python, bir metod arandığında MRO sırasını takip eder: önce kendi sınıfında arar, bulamazsa parent'a, sonra grandparent'a… ta ki object'e kadar.


Gerçek Dünya Örneği: Bildirim Sistemi

from datetime import datetime

class Notification:
    """Temel bildirim sınıfı."""

    _total_sent = 0

    def __init__(self, recipient, message):
        self.recipient = recipient
        self.message = message
        self.created_at = datetime.now()
        self.sent = False
        self.sent_at = None

    def send(self):
        """Alt sınıflar bu metodu override eder."""
        raise NotImplementedError("Alt sınıf send() metodunu implement etmeli!")

    def _mark_sent(self):
        """Gönderildi olarak işaretle."""
        self.sent = True
        self.sent_at = datetime.now()
        Notification._total_sent += 1

    @classmethod
    def total_sent(cls):
        return cls._total_sent

    def __str__(self):
        status = "✅" if self.sent else "⏳"
        return f"{status} [{self.__class__.__name__}] → {self.recipient}: {self.message}"

    def __repr__(self):
        return f"{self.__class__.__name__}('{self.recipient}', '{self.message}')"

class EmailNotification(Notification):
    def __init__(self, recipient, message, subject):
        super().__init__(recipient, message)
        self.subject = subject

    def send(self):
        print(f"📧 Email → {self.recipient} | Konu: {self.subject}")
        self._mark_sent()

class SMSNotification(Notification):
    MAX_LENGTH = 160

    def __init__(self, recipient, message):
        if len(message) > self.MAX_LENGTH:
            message = message[:self.MAX_LENGTH - 3] + "..."
        super().__init__(recipient, message)

    def send(self):
        print(f"📱 SMS → {self.recipient} | {self.message}")
        self._mark_sent()

class PushNotification(Notification):
    def __init__(self, recipient, message, title, icon="🔔"):
        super().__init__(recipient, message)
        self.title = title
        self.icon = icon

    def send(self):
        print(f"{self.icon} Push → {self.recipient} | {self.title}: {self.message}")
        self._mark_sent()

class SlackNotification(Notification):
    def __init__(self, channel, message, mention=None):
        super().__init__(channel, message)
        self.mention = mention

    def send(self):
        mention_str = f"@{self.mention} " if self.mention else ""
        print(f"💬 Slack → #{self.recipient} | {mention_str}{self.message}")
        self._mark_sent()
# Kullanım
notifications = [
    EmailNotification("ali@mail.com", "Hoş geldiniz!", "Kayıt"),
    SMSNotification("+905551234567", "Doğrulama kodunuz: 4523"),
    PushNotification("user_42", "Siparişiniz kargoya verildi", "Kargo"),
    SlackNotification("genel", "Deploy tamamlandı ✅", "devops"),
]

for notif in notifications:
    notif.send()
    print(f"  {notif}")
    print()

print(f"Toplam gönderilen: {Notification.total_sent()}")

Bu örnekte:

  • Notification temel sınıf olarak ortak davranışı tanımlıyor.

  • Her alt sınıf send() metodunu override ediyor.

  • _mark_sent() protected metod olarak alt sınıflardan çağrılabiliyor.

  • _total_sent class attribute ile toplam sayı takip ediliyor.


Yaygın Hatalar

1. super().__init__() Unutmak

class Base:
    def __init__(self):
        self.base_attr = "important!"

class Child(Base):
    def __init__(self):
        # super().__init__() YOK!
        self.child_attr = "also important"

c = Child()
# print(c.base_attr)  # AttributeError!

2. Gereksiz Kalıtım (God Class)

# ❌ Her şeyi tek parent'a yığma
class Everything:
    def save_to_db(self): ...
    def send_email(self): ...
    def generate_pdf(self): ...
    def parse_json(self): ...

class User(Everything):  # "User HER ŞEYDİR" — hayır, değil!
    pass

3. Çok Derin Kalıtım Zinciri

# ❌ 5+ seviye = bakımı zor, anlaşılması imkansız
class A: pass
class B(A): pass
class C(B): pass
class D(C): pass
class E(D): pass  # Burada bir metod nereden geliyor? Bulmaya çalış!

Özet

  • Kalıtım, bir sınıfın başka bir sınıftan özellik ve davranışları devralmasıdır — aile ağacındaki miras gibi.

  • class Child(Parent): söz dizimi ile kalıtım tanımlanır. Child, Parent'ın tüm attribute ve metodlarına erişir.

  • `super()` ile parent sınıfın metodlarına erişiriz. __init__'te super().__init__() çağırmayı unutma!

  • Method overriding ile child sınıf parent'ın davranışını tamamen değiştirebilir veya super() ile genişletebilir.

  • `isinstance()` kalıtım zincirini kontrol eder, type() sadece tam eşleşme arar. Genel olarak isinstance() tercih et.

  • Kalıtım vs Composition: "is-a" ilişkisi varsa kalıtım, "has-a" varsa composition kullan. Şüphede composition tercih et.

  • _protected attribute'lar alt sınıftan erişilebilir; __private attribute'lar name mangling ile karışır.