← Kursa Dön
📄 Text · 18 min

Magic Methods

Python'da sınıf yazarken __init__, __str__ gibi çift alt çizgiyle çevrili metodlar gördün. Bunlara magic methods veya dunder methods (double underscore) denir. "Sihirli" denmesinin sebebi, Python'un bu metodları otomatik olarak çağırmasıdır.

Mesela print(obj) yazdığında Python arka planda obj.__str__() çağırır. len(obj) yazdığında obj.__len__() çağırır. a + b yazdığında a.__add__(b) çağırır. Sen bu metodları tanımlayarak nesnelerinin Python ile nasıl etkileşime gireceğini belirliyorsun.

Bu derste en önemli magic method'ları öğrenip, kendi nesnelerimizi Python'un yerleşik operatörleri ve fonksiyonlarıyla uyumlu hale getireceğiz.


Magic Methods Nedir?

Magic method'lar, Python'un belirli işlemlerde (operatörler, built-in fonksiyonlar, döngüler, vb.) otomatik olarak çağırdığı özel metodlardır.

Analoji: Bir orkestra düşün. Şef (Python) belirli işaretler yapar ve müzisyenler (magic methods) otomatik olarak doğru partisyonu çalar. Şef "forte!" dediğinde davulcu gümbürdemeye başlar — şef tek tek "davul çal" demez, işaret yeterlidir. Magic method'lar da böyle: Python + operatörünü gördüğünde __add__ metodunu otomatik çalar.

# Python bu işaretleri magic method'lara çevirir
x = 42
y = 8

x + y      # x.__add__(y)
x - y      # x.__sub__(y)
x * y      # x.__mul__(y)
len("abc") # "abc".__len__()
str(x)     # x.__str__()
x == y     # x.__eq__(y)
x < y      # x.__lt__(y)

Sen kendi sınıflarında bu metodları tanımlayarak, nesnelerinin Python'un doğal söz dizimiyle çalışmasını sağlarsın.


__str__ vs __repr__: Kullanıcı vs Geliştirici

En sık kullanılan iki magic method:

__str__: Kullanıcı İçin

print() ve str() çağrıldığında devreye girer. Amacı: okunabilir, güzel bir çıktı vermek.

__repr__: Geliştirici İçin

Interactive shell'de nesneyi yazdığında veya repr() çağrıldığında devreye girer. Amacı: nesneyi yeniden oluşturmaya yetecek kadar bilgi vermek.

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

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

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

p = Point(3, 5)

print(p)       # (3, 5)        — __str__ çağrılır
print(str(p))  # (3, 5)        — __str__ çağrılır
print(repr(p)) # Point(3, 5)   — __repr__ çağrılır

# Liste içindeki nesneler __repr__ kullanır!
points = [Point(1, 2), Point(3, 4)]
print(points)  # [Point(1, 2), Point(3, 4)] — __repr__!

Hangisini Yazmalısın?

  • Sadece birini yazacaksan `__repr__` yaz. Python, __str__ bulamazsa __repr__'a düşer.

  • İkisini de yazacaksan: __repr__ teknik, __str__ güzel olsun.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def __repr__(self):
        return f"User(username='{self.username}', email='{self.email}')"

    def __str__(self):
        return f"@{self.username}"

u = User("ali", "ali@mail.com")
print(u)       # @ali
print(repr(u)) # User(username='ali', email='ali@mail.com')

💡 İpucu: __repr__'ın çıktısı ideal olarak eval() ile nesneyi yeniden oluşturabilmeli: eval(repr(obj)) == obj. Her zaman mümkün olmaz ama hedefiniz bu olsun.


Karşılaştırma Metodları

Python'da ==, !=, <, >, <=, >= operatörlerinin her biri bir magic method'a karşılık gelir:

OperatörMethodAnlamı
==__eq__Eşit mi?
!=__ne__Eşit değil mi?
<__lt__Küçük mü?
>__gt__Büyük mü?
<=__le__Küçük veya eşit mi?
>=__ge__Büyük veya eşit mi?
class Money:
    def __init__(self, amount, currency="TL"):
        self.amount = amount
        self.currency = currency

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.amount == other.amount and self.currency == other.currency

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError("Farklı para birimleri karşılaştırılamaz!")
        return self.amount < other.amount

    def __le__(self, other):
        return self == other or self < other

    def __repr__(self):
        return f"Money({self.amount}, '{self.currency}')"

m1 = Money(100, "TL")
m2 = Money(200, "TL")
m3 = Money(100, "TL")

print(m1 == m3)  # True
print(m1 < m2)   # True
print(m1 <= m3)  # True
print(m2 > m1)   # True — Python __lt__'yi ters çevirerek kullanır

NotImplemented Nedir?

NotImplemented döndürmek Python'a "ben bu karşılaştırmayı bilmiyorum, karşı tarafın metodunu dene" der:

class Meter:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if isinstance(other, Meter):
            return self.value == other.value
        if isinstance(other, (int, float)):
            return self.value == other
        return NotImplemented  # "Ben bilmiyorum, sen dene"

m = Meter(5)
print(m == Meter(5))  # True
print(m == 5)         # True
print(m == "hello")   # False — Python son çare olarak False döner

⚠️ Dikkat: NotImplemented ile NotImplementedError karıştırma! NotImplemented bir değer (return value), NotImplementedError bir exception (raise edilir). Karşılaştırma metodlarında her zaman NotImplemented döndür, exception fırlatma.


__hash__: Hashable Nesneler

Python'da bir nesneyi set'e eklemek veya dict key'i olarak kullanmak istiyorsan, nesne hashable olmalı. Bunun için __hash__ metodu gerekir.

class Color:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def __eq__(self, other):
        if not isinstance(other, Color):
            return NotImplemented
        return (self.r, self.g, self.b) == (other.r, other.g, other.b)

    def __hash__(self):
        return hash((self.r, self.g, self.b))

    def __repr__(self):
        return f"Color({self.r}, {self.g}, {self.b})"

# Set'e ekleyebiliriz
colors = {Color(255, 0, 0), Color(0, 255, 0), Color(255, 0, 0)}
print(colors)  # {Color(255, 0, 0), Color(0, 255, 0)} — duplicate temizlendi!

# Dict key'i olarak kullanabiliriz
color_names = {
    Color(255, 0, 0): "Kırmızı",
    Color(0, 255, 0): "Yeşil",
    Color(0, 0, 255): "Mavi",
}
print(color_names[Color(255, 0, 0)])  # Kırmızı

Hash Kuralları

  1. __eq__ tanımlarsan, Python __hash__'i otomatik None yapar (unhashable).

  2. __eq__ ve __hash__ tutarlı olmalı: a == b ise hash(a) == hash(b) olmalı.

  3. Mutable nesneler genellikle hashable olmamalı (değişirse hash da değişir, sorun çıkar).

# __eq__ tanımladın ama __hash__ tanımlamadın
class Broken:
    def __init__(self, x):
        self.x = x

    def __eq__(self, other):
        return self.x == other.x

b = Broken(5)
# {b}  # TypeError: unhashable type: 'Broken'
# __hash__ tanımlanmadığı için set'e eklenemez!

Container Protocol: __len__, __getitem__, __setitem__, __contains__

Bu metodları tanımlayarak kendi "koleksiyon" sınıflarını oluşturabilirsin:

class Playlist:
    def __init__(self, name):
        self.name = name
        self._songs = []

    def add(self, song):
        self._songs.append(song)

    def __len__(self):
        """len(playlist) çalışsın."""
        return len(self._songs)

    def __getitem__(self, index):
        """playlist[0], playlist[1:3] çalışsın."""
        return self._songs[index]

    def __setitem__(self, index, value):
        """playlist[0] = 'yeni şarkı' çalışsın."""
        self._songs[index] = value

    def __contains__(self, item):
        """'şarkı' in playlist çalışsın."""
        return item in self._songs

    def __repr__(self):
        return f"Playlist('{self.name}', {len(self._songs)} songs)"

pl = Playlist("Favori")
pl.add("Bohemian Rhapsody")
pl.add("Stairway to Heaven")
pl.add("Hotel California")

print(len(pl))                      # 3
print(pl[0])                        # Bohemian Rhapsody
print(pl[1:3])                      # ['Stairway to Heaven', 'Hotel California']
print("Hotel California" in pl)     # True

pl[0] = "Imagine"
print(pl[0])                        # Imagine

# Bonus: __getitem__ tanımladığın için for döngüsü de çalışır!
for song in pl:
    print(f"  🎵 {song}")

__iter__ ve __next__

__getitem__ ile döngü çalışır ama daha Pythonic olan __iter__ kullanmaktır:

class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        current = self.start
        while current > 0:
            yield current
            current -= 1

for n in Countdown(5):
    print(n, end=" ")  # 5 4 3 2 1

Operatör Overloading: __add__, __mul__ ve Diğerleri

Aritmetik operatörleri kendi sınıflarında tanımlayabilirsin:

OperatörMethodTers (Reverse)
+__add____radd__
-__sub____rsub__
*__mul____rmul__
/__truediv____rtruediv__
//__floordiv____rfloordiv__
%__mod____rmod__
**__pow____rpow__
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

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

    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return NotImplemented

    def __rmul__(self, scalar):
        """scalar * vector için (ters sıra)."""
        return self.__mul__(scalar)

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

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)     # Vector(4, 6)
print(v2 - v1)     # Vector(2, 2)
print(v1 * 3)      # Vector(3, 6)
print(3 * v1)      # Vector(3, 6) — __rmul__ sayesinde

__radd__, __rmul__ — Reverse Metodları

a + b yazıldığında Python önce a.__add__(b) dener. Eğer NotImplemented dönerse, b.__radd__(a) dener. Bu sayede farklı tipteki nesneler arasında operatör kullanabilirsin.

# v1 * 3 → v1.__mul__(3) ✅
# 3 * v1 → (3).__mul__(v1) → NotImplemented → v1.__rmul__(3) ✅

__bool__: Truthy/Falsy Davranışı

Python'da her nesne bool() ile True veya False olarak değerlendirilir. __bool__ metodu ile bu davranışı kontrol edebilirsin:

class Inventory:
    def __init__(self):
        self.items = {}

    def add(self, item, quantity=1):
        self.items[item] = self.items.get(item, 0) + quantity

    def __bool__(self):
        """Envanterde ürün varsa True."""
        return bool(self.items)

    def __len__(self):
        return sum(self.items.values())

inv = Inventory()

if not inv:
    print("Envanter boş!")  # Bu çalışır

inv.add("Elma", 5)
inv.add("Armut", 3)

if inv:
    print(f"Envanterde {len(inv)} ürün var!")  # Envanterde 8 ürün var!

Bool Çözümleme Sırası

Python bir nesnenin bool değerini şöyle belirler:

  1. __bool__ varsa → onu kullan.

  2. __bool__ yoksa ama __len__ varsa → len() != 0 ise True.

  3. İkisi de yoksa → her zaman True.

class A:
    def __bool__(self):
        return False

class B:
    def __len__(self):
        return 0

class C:
    pass

print(bool(A()))  # False — __bool__ var
print(bool(B()))  # False — __len__ = 0
print(bool(C()))  # True  — ikisi de yok, default True

__call__: Nesneyi Fonksiyon Gibi Çağırma

__call__ tanımlayarak nesneyi () ile çağrılabilir hale getirebilirsin:

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))   # 10 — double.__call__(5)
print(triple(5))   # 15

# callable() ile kontrol
print(callable(double))  # True
print(callable(42))      # False

Pratik Kullanım: Fonksiyon Fabrikası

class Validator:
    def __init__(self, min_val, max_val):
        self.min_val = min_val
        self.max_val = max_val

    def __call__(self, value):
        if self.min_val <= value <= self.max_val:
            return True
        raise ValueError(f"{value}, {self.min_val}-{self.max_val} arasında olmalı!")

# Validator'lar oluştur
validate_age = Validator(0, 150)
validate_score = Validator(0, 100)
validate_temp = Validator(-50, 60)

print(validate_age(25))     # True
print(validate_score(85))   # True

try:
    validate_temp(100)
except ValueError as e:
    print(e)  # 100, -50-60 arasında olmalı!

Decorator Olarak Callable Sınıf

class Timer:
    """Fonksiyonun çalışma süresini ölçen decorator sınıfı."""

    def __init__(self, func):
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):
        import time
        self.call_count += 1
        start = time.perf_counter()
        result = self.func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{self.func.__name__}() — {elapsed:.4f}s (#{self.call_count})")
        return result

@Timer
def slow_function(n):
    return sum(range(n))

slow_function(1_000_000)   # slow_function() — 0.0312s (#1)
slow_function(10_000_000)  # slow_function() — 0.3124s (#2)

@functools.total_ordering: Kısayol

6 karşılaştırma metodu yazmak yorucu. @functools.total_ordering ile sadece __eq__ ve bir sıralama metodu (__lt__, __gt__, __le__ veya __ge__) tanımlamanız yeterli — geri kalanı otomatik oluşturulur:

from functools import total_ordering

@total_ordering
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def __eq__(self, other):
        if not isinstance(other, Student):
            return NotImplemented
        return self.grade == other.grade

    def __lt__(self, other):
        if not isinstance(other, Student):
            return NotImplemented
        return self.grade < other.grade

    def __repr__(self):
        return f"Student('{self.name}', {self.grade})"

s1 = Student("Ali", 85)
s2 = Student("Veli", 92)
s3 = Student("Ayşe", 85)

print(s1 == s3)  # True  — __eq__
print(s1 < s2)   # True  — __lt__
print(s2 > s1)   # True  — otomatik oluşturuldu!
print(s1 >= s3)  # True  — otomatik oluşturuldu!
print(s1 <= s2)  # True  — otomatik oluşturuldu!

# Sıralama da çalışır!
students = [s2, s1, s3]
print(sorted(students))
# [Student('Ali', 85), Student('Ayşe', 85), Student('Veli', 92)]

⚠️ Dikkat: @total_ordering küçük bir performans maliyeti var çünkü otomatik oluşturulan metodlar her seferinde birden fazla karşılaştırma yapar. Performans kritik kodda tüm metodları elle yazmak daha iyi olabilir. Ama çoğu durumda bu maliyet ihmal edilebilir.


Pratik: Vector2D Sınıfı

Tüm magic method'ları kullanan kapsamlı bir örnek:

from functools import total_ordering
import math

@total_ordering
class Vector2D:
    """2 boyutlu vektör sınıfı."""

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

    # ─── String Temsilleri ─────────────────
    def __str__(self):
        return f"({self.x}, {self.y})"

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

    # ─── Aritmetik Operatörler ─────────────
    def __add__(self, other):
        if isinstance(other, Vector2D):
            return Vector2D(self.x + other.x, self.y + other.y)
        return NotImplemented

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

    def __mul__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector2D(self.x * scalar, self.y * scalar)
        if isinstance(scalar, Vector2D):
            # Dot product
            return self.x * scalar.x + self.y * scalar.y
        return NotImplemented

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __neg__(self):
        return Vector2D(-self.x, -self.y)

    def __abs__(self):
        """Vektörün büyüklüğü (magnitude)."""
        return math.sqrt(self.x**2 + self.y**2)

    # ─── Karşılaştırma (büyüklüğe göre) ───
    def __eq__(self, other):
        if isinstance(other, Vector2D):
            return self.x == other.x and self.y == other.y
        return NotImplemented

    def __lt__(self, other):
        if isinstance(other, Vector2D):
            return abs(self) < abs(other)
        return NotImplemented

    def __hash__(self):
        return hash((self.x, self.y))

    # ─── Container Protocol ────────────────
    def __len__(self):
        return 2  # 2D vektör

    def __getitem__(self, index):
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        raise IndexError("Vector2D index 0 veya 1 olmalı")

    def __iter__(self):
        yield self.x
        yield self.y

    def __contains__(self, value):
        return value in (self.x, self.y)

    # ─── Bool ──────────────────────────────
    def __bool__(self):
        """Sıfır vektör değilse True."""
        return self.x != 0 or self.y != 0

    # ─── Callable ──────────────────────────
    def __call__(self, scalar):
        """Kısayol: v(3) == v * 3"""
        return self * scalar

    # ─── Ek Metodlar ──────────────────────
    @property
    def magnitude(self):
        return abs(self)

    def normalize(self):
        """Birim vektör döner."""
        mag = abs(self)
        if mag == 0:
            raise ValueError("Sıfır vektör normalize edilemez!")
        return Vector2D(self.x / mag, self.y / mag)

    def dot(self, other):
        """Dot product."""
        return self.x * other.x + self.y * other.y

    def angle_between(self, other):
        """İki vektör arasındaki açı (radyan)."""
        cos_angle = self.dot(other) / (abs(self) * abs(other))
        return math.acos(max(-1, min(1, cos_angle)))
# Kullanım
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)

# String temsilleri
print(v1)           # (3, 4)
print(repr(v1))     # Vector2D(3, 4)

# Aritmetik
print(v1 + v2)      # (4, 6)
print(v1 - v2)      # (2, 2)
print(v1 * 2)       # (6, 8)
print(2 * v1)       # (6, 8)
print(-v1)          # (-3, -4)

# Büyüklük
print(abs(v1))      # 5.0
print(v1.magnitude) # 5.0

# Karşılaştırma
print(v1 > v2)      # True (5.0 > 2.236...)
print(v1 == Vector2D(3, 4))  # True

# Container
print(len(v1))      # 2
print(v1[0])        # 3
print(v1[1])        # 4
print(3 in v1)      # True

# İterasyon
x, y = v1           # Unpacking!
print(x, y)         # 3 4

# Bool
print(bool(v1))              # True
print(bool(Vector2D(0, 0)))  # False

# Callable
print(v1(3))        # (9, 12) — v1 * 3

# Set/dict key
vectors = {v1, v2, Vector2D(3, 4)}
print(len(vectors))  # 2 — duplicate temizlendi

# Normalize
print(v1.normalize())  # (0.6, 0.8)

# Dot product
print(v1 * v2)  # 11 (3*1 + 4*2)
print(v1.dot(v2))  # 11

Tüm Önemli Magic Method'lar: Referans Tablosu

String / Temsil

MethodTetikleyenAçıklama
__str__str(), print()Kullanıcı dostu çıktı
__repr__repr(), interactive shellGeliştirici dostu çıktı
__format__format(), f-stringÖzel formatlama

Karşılaştırma

MethodTetikleyen
__eq__==
__ne__!=
__lt__<
__gt__>
__le__<=
__ge__>=

Aritmetik

MethodTetikleyen
__add__ / __radd__+
__sub__ / __rsub__-
__mul__ / __rmul__*
__truediv__ / __rtruediv__/
__floordiv__//
__mod__%
__pow__**
__neg__-obj (unary)
__abs__abs()

Container

MethodTetikleyen
__len__len()
__getitem__obj[key]
__setitem__obj[key] = val
__delitem__del obj[key]
__contains__item in obj
__iter__for x in obj

Diğer

MethodTetikleyen
__bool__bool(), if obj
__call__obj()
__hash__hash(), set/dict key
__init__Nesne oluşturma
__del__Nesne silinme

Yaygın Hatalar

1. __eq__ Yazıp __hash__ Unutmak

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

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    # __hash__ yok! Python otomatik None yapar.

p = BadPoint(1, 2)
# {p}  # TypeError: unhashable type: 'BadPoint'

2. NotImplemented Yerine False Döndürmek

# ❌ Kötü
def __eq__(self, other):
    if not isinstance(other, MyClass):
        return False  # Diğer tarafın şansını elinden aldın!

# ✅ İyi
def __eq__(self, other):
    if not isinstance(other, MyClass):
        return NotImplemented  # "Ben bilmiyorum, karşı taraf denesin"

3. Magic Method'u Doğrudan Çağırmak

# ❌ Yapma
obj.__len__()
obj.__str__()
obj.__eq__(other)

# ✅ Yap
len(obj)
str(obj)
obj == other

💡 İpucu: Magic method'ları doğrudan çağırmak antipattern'dir. Her zaman Python'un operatörlerini veya built-in fonksiyonlarını kullan. Tek istisna: super().__init__() gibi kalıtım senaryoları.


Özet

  • Magic methods (dunder methods), Python'un operatör ve fonksiyonlarda otomatik çağırdığı özel metodlardır.

  • `__str__` kullanıcı için okunabilir çıktı, `__repr__` geliştirici için teknik çıktı verir. Sadece birini yazacaksan __repr__ yaz.

  • Karşılaştırma (__eq__, __lt__ vb.) ile nesneleri karşılaştırabilir, `@functools.total_ordering` ile sadece 2 metod yeterli.

  • `__hash__` tanımlayarak nesneleri set/dict'te kullanabilirsin. __eq__ ile tutarlı olmalı.

  • Container protocol (__len__, __getitem__, __contains__) ile kendi koleksiyon sınıflarını yapabilirsin.

  • Operatör overloading (__add__, __mul__) ile nesnelerin +, * gibi operatörlerle çalışmasını sağlarsın.

  • `__bool__` truthy/falsy davranışı, `__call__` nesneyi fonksiyon gibi çağırma yeteneği verir.