Nesne Tabanlı Programlama 2

6 - Özel Metotlar ve Operatör Aşırı Yükleme

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2026

Motivasyon: Bu Konu Neden Var?

Geçen hafta polimorfizmde şunu görmüştük:

  • Aynı mesaj, farklı nesnelerde farklı davranabilir.
  • İstemci kodu, nesnenin iç ayrıntısını bilmek zorunda değildir.

Bu hafta bu fikri Python’ın kendisine taşıyoruz:

Python da bir istemcidir.

print(obj), len(obj), obj[0], obj1 + obj2 gibi ifadelerle sınıfınızdan belli davranışlar bekler.

Bu Dersin Öğrenme Hedefleri

Bu dersin sonunda şunları ayırt edebiliyor olmanızı hedefliyoruz:

  • Özel metotların neden doğrudan değil, dil tarafından dolaylı çağrıldığını
  • __str__ ile __repr__ arasındaki farkı
  • __len__, __getitem__, __contains__, __iter__ gibi metotların hangi davranışları açtığını
  • Operatör aşırı yüklemede neden her operatöre anlam yüklememek gerektiğini
  • NotImplemented döndürmenin ne zaman doğru tercih olduğunu

Önceki Haftalarla Bağlantı

Bu konu önceki iki haftanın doğal devamıdır:

  1. Kapsülleme ve soyutlamada, sınıfın dışarıya sunduğu arayüzü konuştuk.
  2. Polimorfizmde, aynı çağrının farklı nesnelerde çalışmasını gördük.
  3. Bu hafta ise Python’ın yerleşik işlemlerine uyumlu bir arayüz tasarlıyoruz.

Kısaca:

Özel metotlar, Python ile aranızdaki sözleşmedir.

Önce Sezgi: Bu Derste Ne Göreceğiz?

Bu noktada teknik ayrıntıyı değil, önce temel fikri görmek yeterli:

  • Python bazı nesneleri ekranda yazdırabilir.
  • Bazı nesnelerin uzunluğunu hesaplayabilir.
  • Bazı nesneleri dolaşabilir ya da toplayabilir.

Bu davranışları bir sınıfa siz de kazandırabilirsiniz.

Yani bu dersin ilk sorusu şudur:

“Kendi sınıfımı Python’ın yerleşik davranışlarıyla nasıl uyumlu hale getiririm?”

Hangi İfade, Hangi Metodu Tetikler?

Yazdığımız ifade Python’ın beklediği davranış
print(obj) __str__
repr(obj) __repr__
len(obj) __len__
obj[key] __getitem__
obj[key] = value __setitem__
del obj[key] __delitem__
x in obj __contains__
for x in obj __iter__
obj1 + obj2 __add__

Bugün ağırlıklı olarak bu çekirdek davranışlara odaklanacağız.

__str__ ve __repr__

  • __str__: son kullanıcıya gösterilecek daha okunabilir temsil
  • __repr__: geliştirici için daha açık, daha kesin temsil

Pratik kural:

  • str(obj) → “ekranda nasıl göreyim?”
  • repr(obj) → “bu nesnenin içeriğini nasıl daha net anlayayım?”

__repr__, mümkünse nesneyi yeniden üretmeye yakın bir gösterim verir; ama bu her zaman zorunlu değildir.

Örnek: __str__ ve __repr__

class Ders:
    def __init__(self, kod, ad, kredi):
        self.kod = kod
        self.ad = ad
        self.kredi = kredi

    def __str__(self):
        return f"{self.kod} - {self.ad} ({self.kredi} kredi)"

    def __repr__(self):
        return f"Ders('{self.kod}', '{self.ad}', {self.kredi})"


ders = Ders("NTP201", "Nesne Tabanlı Programlama 2", 4)
print(ders)
print(repr(ders))

Bu Örnekte Aslında Ne Oldu?

Burada henüz tüm özel metotları bilmenize gerek yok.

Önce şu gözlemi yapın:

  • print(ders) dediğimizde Python nesneyi ekrana nasıl uygun biçimde yazacağını arar.
  • repr(ders) dediğimizde daha teknik bir temsil ister.
  • Biz de bu iki davranışı sınıf içinde tanımlamış olduk.

Yani özel metotların mantığı şudur:

Python bir davranış bekler, siz o davranışı sınıfınıza eklersiniz.

__len__: Nesnenin “Kaç Tane?” Sorusuna Cevabı

__len__, nesnenin büyüklüğünü veya eleman sayısını döndürmek için kullanılır.

Önemli nokta:

  • Dönüş değeri tam sayı olmalıdır.
  • Negatif bir değer döndürmek doğru değildir.
  • Daha çok koleksiyon benzeri nesnelerde anlamlıdır.

Örnek: Ödev Kutusu

class OdevKutusu:
    def __init__(self):
        self.odevler = []

    def ekle(self, odev_adi):
        self.odevler.append(odev_adi)

    def __len__(self):
        return len(self.odevler)


kutu = OdevKutusu()
kutu.ekle("Hafta 1")
kutu.ekle("Hafta 2")
print(len(kutu))

__getitem__, __setitem__, __delitem__

Bu metotlar nesneye dizi ya da sözlük benzeri erişim kazandırır:

Bu üç metot aynı ailenin parçalarıdır; ancak çoğu durumda ilk anlamamız gereken davranış okuma davranışıdır.

  • obj[key]
  • obj[key] = value
  • del obj[key]

Ama burada kritik soru şudur:

Nesneniz gerçekten indekslenebilir ya da anahtarla erişilebilir bir yapı mı?

Değilse sırf gösteriş için bu metotları eklemek iyi tasarım değildir.

Önemli Teknik Nokta: Hata Davranışı

Sözlük benzeri bir yapı tasarlıyorsanız, bulunamayan anahtarda çoğu zaman KeyError beklenir.

Bu yüzden her durumda sessizce None ya da “öğrenci yok” döndürmek risklidir; çünkü Python’ın standart davranışı hakkında yanlış bir beklenti oluşturabilir.

İki yaklaşım vardır:

  • Yerleşik türlere benzemek istiyorsanız, uygun hatayı üretin.
  • Bilinçli olarak farklı davranacaksanız, bunu açıkça tasarlayın.

Örnek: Not Defteri

class NotDefteri:
    def __init__(self):
        self.notlar = {}

    def __getitem__(self, hafta_no):
        return self.notlar[hafta_no]

    def __setitem__(self, hafta_no, icerik):
        self.notlar[hafta_no] = icerik

    def __delitem__(self, hafta_no):
        del self.notlar[hafta_no]


defter = NotDefteri()
defter[1] = "Sınıflar"
defter[2] = "Kalıtım"

print(defter[1])
del defter[2]

Kısa Kontrol Sorusu

defter[99] ifadesi çalıştığında ne olmalı?

  1. Her zaman None
  2. Her zaman boş metin
  3. Eğer yapı sözlük gibi davranıyorsa çoğu zaman KeyError

Genel olarak beklenen cevap çoğu durumda şudur:

“Nesnenin hangi davranışı taklit ettiğine bağlıdır; sözlük gibi ise KeyError beklenir.”

Operatör Aşırı Yükleme Nedir?

Operatör aşırı yükleme, sınıfınıza +, -, ==, < gibi operatörlerle anlamlı bir davranış kazandırır.

Buradaki ana ilke şudur:

Bir operatörü sadece gerçekten doğal ve sezgisel ise aşırı yükleyin.

Örneğin:

  • İki vektörü toplamak mantıklıdır.
  • İki para miktarını toplamak mantıklıdır.
  • İki öğrenciyi + ile toplamak çoğu durumda anlamsızdır.

Temel Operatörler

Operatör Özel metot Beklenen anlam
+ __add__ toplama / birleştirme
- __sub__ çıkarma
* __mul__ çarpma / tekrar
== __eq__ eşitlik
< __lt__ sıralama karşılaştırması

Aynı mantıkla başka operatörler de tanımlanabilir; ancak bugün çekirdekte en sık karşılaşılan davranışları öne çıkarıyoruz.

Örnek: Vektör Toplama

class Vektor:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vektor({self.x}, {self.y})"

    def __add__(self, other):
        if not isinstance(other, Vektor):
            return NotImplemented
        return Vektor(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        if not isinstance(other, Vektor):
            return NotImplemented
        return Vektor(self.x - other.x, self.y - other.y)

Kullanım

v1 = Vektor(2, 3)
v2 = Vektor(4, 5)

print(v1 + v2)
print(v1 - v2)

Neden NotImplemented?

Bu detay önemlidir.

Şöyle yazmak teknik olarak zayıftır:

return Vektor(self.x + other.x, self.y + other.y)

Çünkü other beklediğiniz tipte değilse hata kontrolsüz biçimde patlar.

Daha doğru yaklaşım:

  • Uygun olmayan türde NotImplemented döndürmek
  • Python’ın diğer operandın ilgili metodunu denemesine izin vermek
  • Gerekirse sonunda anlamlı bir TypeError oluşmasını sağlamak

Bu, öğrencinin “çalışan örnek” ile “sağlam tasarım” arasındaki farkı görmesi için önemlidir.

Örnek: Para Sınıfı

class Para:
    def __init__(self, miktar):
        self.miktar = miktar

    def __repr__(self):
        return f"Para({self.miktar})"

    def __add__(self, other):
        if isinstance(other, Para):
            return Para(self.miktar + other.miktar)
        return NotImplemented

    def __mul__(self, katsayi):
        if not isinstance(katsayi, (int, float)):
            return NotImplemented
        return Para(self.miktar * katsayi)

    def __str__(self):
        return f"{self.miktar:.2f} TL"

Karşılaştırma Operatörleri

Karşılaştırma metotları, nesnelerin sıralanması ve eşitlik kontrolü için kullanılır.

Ama yine aynı ilke geçerlidir:

Neye göre eşitlik kurduğunuzu açık seçik belirlemelisiniz.

Soru:

  • İki öğrenci numarası aynıysa mı eşit?
  • Adı aynıysa mı eşit?
  • Ortalaması aynıysa mı eşit?

Yanlış seçilen eşitlik tanımı, ileride ciddi mantık hataları üretir.

Örnek: Öğrenci Sıralama

class Ogrenci:
    def __init__(self, no, ad, ortalama):
        self.no = no
        self.ad = ad
        self.ortalama = ortalama

    def __repr__(self):
        return f"Ogrenci({self.no}, '{self.ad}', {self.ortalama})"

    def __eq__(self, other):
        if not isinstance(other, Ogrenci):
            return NotImplemented
        return self.no == other.no

    def __lt__(self, other):
        if not isinstance(other, Ogrenci):
            return NotImplemented
        return self.ortalama < other.ortalama

Bu örnekte:

  • eşitlik öğrenci numarasına göre,
  • sıralama ise ortalamaya göre tanımlanmıştır.

Bu kararın bilinçli verilmesi gerekir, eşitlik ve sıralama ölçütleri farklı olduğunda tasarım kararının sonuçları dikkatle düşünülmelidir.

__contains__: in Operatörü

Bir nesnenin içinde eleman var mı sorusuna cevap vermek için kullanılır.

class Kurs:
    def __init__(self, ad, ogrenciler=None):
        self.ad = ad
        self.ogrenciler = ogrenciler or []

    def __contains__(self, ogrenci):
        return ogrenci in self.ogrenciler


kurs = Kurs("Python", ["Ali", "Ayşe"])
print("Ali" in kurs)
print("Mehmet" in kurs)

İleri not: __contains__ en açık ve doğrudan çözümdür. Bu metot yoksa Python bazı durumlarda iterasyon üzerinden de üyelik kontrolü yapabilir.

__iter__: Dolaşılabilir Nesneler

Bir nesneyi for döngüsünde kullanmak istiyorsanız, iterasyon protokolüne uygun davranmalıdır.

İlk yaklaşım olarak en temiz seçenek çoğu zaman şudur:

Var olan bir koleksiyonun iterator’unu döndürmek.

Örnek: Ders Programı

class DersProgrami:
    def __init__(self, dersler):
        self.dersler = dersler

    def __iter__(self):
        return iter(self.dersler)


program = DersProgrami(["Matematik", "Programlama", "Fizik"])

for ders in program:
    print(ders)

Bu örnekte __iter__, var olan listenin iterator’unu döndürür. Böylece nesne doğrudan for döngüsünde kullanılabilir.

Ne Zaman Operatör Aşırı Yüklememeliyiz?

Her desteklenebilen şey, kullanılmalı anlamına gelmez.

  • davranış kullanıcı için sezgisel değilse,
  • aynı operatör farklı yerlerde farklı sürpriz sonuçlar üretiyorsa,
  • normal bir metot adı daha açık olacaksa.

Operatör aşırı yüklemeyin.

Örnek:

  • sepet.birlestir(diger_sepet) çoğu zaman sepet + diger_sepetten daha açık olabilir.

Sık Yapılan Hatalar

  1. Sırf konu işlendi diye her sınıfa özel metot eklemek
  2. __getitem__ içinde standart davranıştan sapıp bunu belirtmemek
  3. __add__ ve __eq__ içinde tür kontrolünü atlamak
  4. __repr__ ile __str__ farkını belirsiz bırakmak
  5. Tek derste çok fazla özel metodu aynı ağırlıkta anlatmak

Alıştırmalar

Alıştırma 1: Okuma Listesi

Bir OkumaListesi sınıfı yazın.

Sınıfın içinde bir kitap listesi tutulacak.

İstenen davranışlar:

  • len(liste) toplam kitap sayısını versin
  • kitap_adi in liste ifadesi çalışsın
  • for kitap in liste ile dolaşılabilsin

## Alıştırma 1: İskelet ve Beklenen Kullanım {.smaller}

Başlangıç iskeleti:

class OkumaListesi:
    def __init__(self, kitaplar):
        self.kitaplar = kitaplar

Beklenen kullanım:

liste = OkumaListesi(["Sefiller", "1984", "Tutunamayanlar"])

print(len(liste))
print("1984" in liste)

for kitap in liste:
    print(kitap)

Alıştırma 1 İçin İpucu

Bu soruda üç davranış gerekiyor:

  • uzunluk
  • üyelik kontrolü
  • dolaşım

Her davranış için hangi özel metodun gerektiğini siz belirleyin.

Çözüm sırası olarak önce uzunluk, sonra üyelik, en son dolaşım davranışını düşünmek işinizi kolaylaştırır.

Alıştırma 2: Telefon Rehberi

Bir TelefonRehberi sınıfı yazın.

Bu sınıf, kişi adına göre telefon numarası saklasın.

İstenen davranışlar:

  • rehber["Ali"] = "555-1234" şeklinde veri eklenebilsin
  • print(rehber["Ali"]) ile veri okunabilsin
  • del rehber["Ali"] ile veri silinebilsin

## Alıştırma 2: İskelet ve Beklenen Kullanım {.smaller}

Başlangıç iskeleti:

class TelefonRehberi:
    def __init__(self):
        self.veriler = {}

Beklenen kullanım:

rehber = TelefonRehberi()
rehber["Ali"] = "555-1234"
rehber["Ayse"] = "555-9876"

print(rehber["Ali"])
del rehber["Ayse"]

Alıştırma 2 İçin Dikkat Noktası

Bu soru, sözlük benzeri davranış tasarlama sorusudur.

Burada düşünülmesi gereken nokta:

Rehberde olmayan bir kişi istendiğinde ne olmalı?

İki seçenek konuşulabilir:

  1. doğrudan KeyError üretmek
  2. bilinçli olarak farklı bir davranış tasarlamak

Eğer yapı sözlük gibi davranıyorsa, varsayılan beklenti çoğu zaman KeyError olacaktır.

Alıştırma 3: İki Boyutlu Nokta

Bir Nokta sınıfı yazın.

İstenen davranışlar:

  • n1 + n2 yeni bir nokta üretsin
  • n1 == n2 karşılaştırması çalışsın
  • print(n1) daha okunabilir bir çıktı versin

## Alıştırma 3: İskelet ve Beklenen Kullanım {.smaller}

Başlangıç iskeleti:

class Nokta:
    def __init__(self, x, y):
        self.x = x
        self.y = y

Beklenen kullanım:

n1 = Nokta(2, 3)
n2 = Nokta(4, 5)

print(n1 + n2)
print(n1 == n2)

Alıştırma 3 İçin İpucu

Bu soruda üç farklı davranış bir araya geliyor:

  • toplama davranışı
  • eşitlik davranışı
  • okunabilir temsil davranışı

Ek kontrol sorusu:

n1 + 5 ifadesi yazılırsa ne olmalı?

Burada uygun tür kontrolü yapıp NotImplemented döndürmek daha sağlam bir çözümdür.

Özet

  • Özel metotlar, sınıfınızı Python’ın yerleşik işlemleriyle konuşturur.
  • Bu konu, kapsülleme ve polimorfizmin devamıdır.
  • Amaç ezber değil, doğru davranış tasarlamaktır.
  • Operatör aşırı yükleme ancak davranış gerçekten doğal ise kullanılmalıdır.
  • Sağlam örneklerde tür kontrolü ve NotImplemented önemli ayrıntılardır.

Kapanış

Bu haftanın ana sorusu şuydu:

“Bu nesne Python’a hangi doğal davranışları sunmalı?”

Son karar için şu üç soruyu sorun:

  1. Bu nesne gerçekten koleksiyon gibi mi davranıyor?
  2. Bu operatör bu nesne için gerçekten doğal mı?
  3. Python’ın standart beklentisini bozuyor muyum?