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 aynı çağrının farklı nesnelerde farklı davranabildiğini gördük.

Bu hafta bu fikri Python’ın yerleşik işlemlerine taşıyoruz.

Python da nesnelerinizden bazı davranışlar bekler.

Örneğin:

  • print(obj)
  • len(obj)
  • obj[0]
  • obj1 + obj2

Bu ifadelerin çalışabilmesi için sınıfınızın Python’ın veri modeline uygun davranması gerekir.

Bu Derste Ne Göreceğiz?

Bu dersin odağı şudur:

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

Bu amaçla özellikle şu başlıklara bakacağız:

  • nesnenin temsil edilmesi
  • uzunluk, erişim ve dolaşım davranışları
  • üyelik kontrolü
  • operatör aşırı yükleme
  • doğru tasarım kararları

Amaç; hangi davranışın ne zaman anlamlı olduğunu görmek.

İlk Ö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 İlk Örnek Bize Ne Gösteriyor?

Bu örnekte henüz bütün özel metotları bilmeniz gerekmiyor.

Şimdilik şu fikri görmek yeterlidir:

  • print(ders) yazdığımızda Python nesneyi nasıl göstereceğini arar.
  • repr(ders) yazdığımızda daha teknik bir temsil ister.
  • Biz de bu davranışları sınıf içinde tanımlamış oluruz.

Yani temel mantık şudur:

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

__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 verebilir; ancak asıl amaç daha teknik ve daha az belirsiz bir temsil sunmaktır.

__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 noktalar:

  • 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))

Şimdi Genel Resmi Görelim

Şu ana kadar iki örnek gördük:

  • nesnenin temsil edilmesi
  • nesnenin uzunluğunun verilmesi

Aslında Python başka birçok ifadede de benzer biçimde özel metotlar arar.

Hangi İfade, Hangi Metotla İlgilidir?

Yazdığımız ifade Temel olarak ilişkili metot
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 bu temel eşleşmelerin mantığına odaklanacağız.

__getitem__, __setitem__, __delitem__

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

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

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

Kritik soru şudur:

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

Değilse sırf konu işlendi diye 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 şudur:

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

__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 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.

Başlangıç düzeyinde en temiz yaklaşım ç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.

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:

  • iki vektörü toplamak mantıklıdır,
  • iki para miktarını toplamak mantıklıdır,
  • iki öğ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 en sık karşılaşılan davranışlara odaklanı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, “çalışan örnek” ile “sağlam tasarım” arasındaki farkı görmek 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:

  • iki öğ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ı üretebilir.

Ö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 örnek özellikle tartışma üretmek için seçilmiştir. Gerçek sistemlerde eşitlik ve sıralama ölçütlerinin birlikte nasıl sonuçlar doğurduğu dikkatle düşünülmelidir.

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_sepet ifadesinden 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

Şimdi üç farklı davranış ailesi için kısa uygulamalar yapacağız:

  • koleksiyon benzeri davranış
  • sözlük benzeri davranış
  • operatör ve temsil davranışı

Amaç sadece kodu çalıştırmak değil, hangi davranışın hangi özel metotla tasarlandığını görmek.

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

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

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

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 uyumlu hale getirir.
  • Bu konu, kapsülleme ve polimorfizmin devamı olarak düşünülebilir.
  • 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?