Python'da özel metotlar, adı iki alt çizgiyle başlayıp biten ("double underscore", kısaca dunder) ve dilin kendi söz dizimi tarafından çağrılan metotlardır. print(x) __str__'ı, x == y __eq__'yu, len(x) __len__'i, x[i] __getitem__'ı, x + y __add__'i çalıştırır. Kendi sınıfınızda bu metotları tanımladığınızda nesneleriniz yerleşik tipler gibi davranır; Python'un operatör aşırı yükleme (operator overloading) yaklaşımı budur.

Bu yazıda __repr__ ve __str__ farkını, __eq__ ile __hash__ arasındaki zorunlu ilişkiyi, kapsayıcı (container) protokolünü, aritmetik operatörleri ve NotImplemented değerini, functools.total_ordering'i, __call__ ve __bool__ metotlarını ve özel metotların neden örnek üzerinde değil sınıf üzerinde aranması gerektiğini Python 3.15.2 ile çalıştırılmış örneklerle anlatıyoruz. Sınıf temelleri için class ve nesne yazısına, tüm seri için Python öğrenme rehberine göz atabilirsiniz.

Özel metotlar nasıl çağrılır?

Özel metotlar nasıl çağrılır?

Veri modeli belgesine göre x[i] ifadesi kabaca type(x).__getitem__(x, i) çağrısına denktir. Burada iki önemli ayrıntı var. Birincisi, metot tip üzerinden bulunur, örneğin kendi sözlüğünden değil. İkincisi, ilgili metot tanımlı değilse işlem genellikle TypeError ya da AttributeError ile başarısız olur. Bir özel metodu bilerek None yapmak da o işlemin desteklenmediğini bildirmenin resmî yoludur: __iter__ = None yazılan bir sınıfın örnekleri üzerinde iter() doğrudan TypeError verir.

Belgeler bir de tasarım uyarısı yapar: yerleşik bir tipi taklit eden sınıf, yalnızca modellediği nesne için anlamlı olan kadarını uygulamalıdır. Tek tek eleman okumanın anlamlı olduğu ama dilimlemenin anlamsız olduğu bir dizi yapısında dilimlemeyi zorla eklemek gerekmez.

__repr__ ve __str__: iki farklı metin

__repr__, repr() tarafından çağrılır ve nesnenin "resmî" gösterimini üretir. Belgelere göre mümkünse geçerli bir Python ifadesine benzemeli, yani aynı değerde nesneyi yeniden oluşturmaya yetecek bilgiyi içermelidir; mümkün değilse <...açıklama...> biçiminde bir metin döndürülmelidir. Hata ayıklamada kullanıldığı için bilgi dolu ve belirsizlikten uzak olması önemlidir. __str__ ise str(), print() ve varsayılan format() tarafından çağrılır ve okunaklı, kullanıcıya dönük gösterimdir.

python
class Urun:
    def __init__(self, ad, fiyat):
        self.ad = ad
        self.fiyat = fiyat

    def __repr__(self):
        return f'Urun({self.ad!r}, {self.fiyat!r})'

    def __str__(self):
        return f'{self.ad} ({self.fiyat} TL)'

u = Urun('Çay', 45)
print(u)                 # __str__
print(repr(u))           # __repr__
print(f'{u} / {u!r}')
print([u])               # kapsayıcılar elemanların repr'ini kullanır
# çıktı: Çay (45 TL)
# çıktı: Urun('Çay', 45)
# çıktı: Çay (45 TL) / Urun('Çay', 45)
# çıktı: [Urun('Çay', 45)]

Son satıra dikkat edin: liste, sözlük ve demet gibi kapsayıcılar yazdırılırken elemanlarının __repr__'ını kullanır. f-string içinde !r dönüşümü de repr()'ı zorlar. Biçimlendirme ayrıntıları için f-string yazısına bakabilirsiniz. Yalnızca birini yazacaksanız __repr__'ı yazın; __str__ yoksa Python onun yerine __repr__'ı kullanır:

python
class Ham:
    pass

class SadeceRepr:
    def __repr__(self):
        return 'SadeceRepr()'

h = Ham()
print(repr(h).startswith('<__main__.Ham object at 0x'))
print(str(SadeceRepr()))           # __str__ yoksa __repr__ kullanılır
# çıktı: True
# çıktı: SadeceRepr()

__eq__ ve __hash__: eşitlik ile özetin bağı

Varsayılan olarak object eşitliği kimlikle (is) karşılaştırır: iki ayrı nesne, içerikleri aynı olsa bile eşit sayılmaz. __eq__ yazarak değer eşitliği tanımlayabilirsiniz. Karşılaştırma metodu, desteklemediği bir tiple karşılaşınca False değil NotImplemented döndürmelidir; böylece Python diğer operandın yansıtılmış metodunu dener, hiçbiri sonuç vermezse == kimlik karşılaştırmasına düşer. __ne__ yazmanıza gerek yoktur; varsayılan sürüm __eq__'nun sonucunu tersine çevirir.

Asıl tuzak burada: __eq__'yu geçersiz kılıp __hash__ tanımlamayan bir sınıfın __hash__'i otomatik olarak None yapılır. Nesneler artık kümeye eklenemez ve sözlük anahtarı olamaz:

python
class Para:
    def __init__(self, tutar, birim):
        self.tutar = tutar
        self.birim = birim

    def __eq__(self, other):
        if not isinstance(other, Para):
            return NotImplemented
        return (self.tutar, self.birim) == (other.tutar, other.birim)

a = Para(10, 'TL')
b = Para(10, 'TL')
print(a == b, a is b, a != b)
print(a == 10)                      # NotImplemented sonrası kimliğe döner
print(Para.__hash__)
try:
    {a}
except TypeError as e:
    print(e)
# çıktı: True False False
# çıktı: False
# çıktı: None
# çıktı: cannot use 'Para' as a set element (unhashable type: 'Para')

Belgelerin koyduğu tek zorunlu kural, eşit karşılaştırılan nesnelerin aynı hash değerine sahip olmasıdır. Önerilen yöntem, karşılaştırmada kullanılan alanları bir demette toplayıp o demetin hash'ini almaktır. İkinci kural da önemlidir: değiştirilebilir (mutable) nesneler __hash__ tanımlamamalıdır, çünkü hash değeri değişirse nesne kümede yanlış yerde kalır. Aşağıda alanlar alt çizgiyle "dokunulmaz" olarak işaretlendi:

python
class Para:
    def __init__(self, tutar, birim):
        self._tutar = tutar
        self._birim = birim

    def __eq__(self, other):
        if not isinstance(other, Para):
            return NotImplemented
        return (self._tutar, self._birim) == (other._tutar, other._birim)

    def __hash__(self):
        return hash((self._tutar, self._birim))

kume = {Para(10, 'TL'), Para(10, 'TL'), Para(5, 'USD')}
print(len(kume))
print(hash(Para(1, 'TL')) == hash(Para(1, 'TL')))
# çıktı: 2
# çıktı: True

Kapsayıcı protokolü: __len__, __getitem__, __contains__, __iter__

Bir sınıfı liste ya da sözlük gibi kullanılabilir yapmak için birkaç metot yeterlidir. __len__ len()'i, __getitem__ köşeli parantezle erişimi, __contains__ in işlecini, __iter__ da for döngüsünü besler. Dilimleme ayrı bir metot gerektirmez: r[1:3] çağrısı __getitem__'a bir slice nesnesi gönderir, eksik kısımlar None ile doldurulur.

python
class Raf:
    def __init__(self, *kitaplar):
        self._kitaplar = list(kitaplar)

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

    def __getitem__(self, i):
        sonuc = self._kitaplar[i]
        if isinstance(i, slice):
            return Raf(*sonuc)
        return sonuc

    def __contains__(self, ad):
        return ad.casefold() in (k.casefold() for k in self._kitaplar)

    def __repr__(self):
        return f'Raf{tuple(self._kitaplar)}'

r = Raf('Saatleri Ayarlama Enstitüsü', 'Tutunamayanlar', 'Kürk Mantolu Madonna')
print(len(r), bool(r), bool(Raf()))
print(r[0])
print(r[-1])
print(r[:2])
print('tutunamayanlar' in r)
for kitap in r:                 # __iter__ yok: __getitem__ ile 0, 1, 2... denenir
    print('-', kitap)
# çıktı: 3 True False
# çıktı: Saatleri Ayarlama Enstitüsü
# çıktı: Kürk Mantolu Madonna
# çıktı: Raf('Saatleri Ayarlama Enstitüsü', 'Tutunamayanlar')
# çıktı: True
# çıktı: - Saatleri Ayarlama Enstitüsü
# çıktı: - Tutunamayanlar
# çıktı: - Kürk Mantolu Madonna

Örnekte __iter__ yazılmadığı hâlde for döngüsü çalıştı. Bunun nedeni eski dizi protokolüdür: Python __getitem__'ı 0, 1, 2 diye çağırır ve IndexError görünce durur. Bu yüzden dizi gibi davranan bir __getitem__, geçersiz indekste mutlaka IndexError fırlatmalıdır (sözlük benzeri yapılarda KeyError). __bool__ tanımlı değilse doğruluk testi __len__'e bakar; uzunluğu sıfır olan nesne yanlış sayılır, bu yüzden bool(Raf()) False döndü. Sözlük benzeri bir sınıf yazıyorsanız collections.abc.MutableMapping taban sınıfı, dört temel metottan geri kalanları üretir. Kendi yineleyicinizi yazmak için generator ve iterator yazısına bakın.

Aritmetik operatörler ve NotImplemented

Aritmetik operatörler ve NotImplemented: x + y, x - y, x * y, x / y

x + y ifadesinde Python önce type(x).__add__(x, y)'yi çağırır. Bu metot NotImplemented döndürürse, operandların tipleri farklıysa y'nin yansıtılmış metodu __radd__ denenir. İkisi de başarısız olursa TypeError oluşur. 3 * v gibi ifadelerin çalışması için bu yüzden __rmul__ gerekir; int bir Vektor ile nasıl çarpılacağını bilmez.

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

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

    def __mul__(self, k):
        if not isinstance(k, (int, float)):
            return NotImplemented
        return Vektor(self.x * k, self.y * k)

    __rmul__ = __mul__          # 3 * v için

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

    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

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

v = Vektor(3, 4)
print(v + Vektor(1, 1), v * 2, 3 * v, -v, abs(v))
t = v
t += Vektor(1, 0)               # __iadd__ yok: t = t + ... olur, yeni nesne
print(t, v, t is v)
try:
    v + 5
except TypeError as e:
    print(e)
# çıktı: Vektor(4, 5) Vektor(6, 8) Vektor(9, 12) Vektor(-3, -4) 5.0
# çıktı: Vektor(4, 4) Vektor(3, 4) False
# çıktı: unsupported operand type(s) for +: 'Vektor' and 'int'

+= gibi artırılmış atamalar önce __iadd__'i arar. Belgelere göre bu metot işlemi yerinde yapmaya çalışmalı ve sonucu döndürmelidir. __iadd__ yoksa Python t = t + ... şeklinde normal toplamaya düşer; çıktıdaki t is v sonucunun False olması yeni bir nesne oluştuğunu gösteriyor. Listeler __iadd__ tanımladığı için liste += [...] listenin kendisini değiştirir; bu fark, aynı listeye birden çok adın bağlı olduğu durumlarda önemlidir (liste metotları yazısı). Python 3.14 ile üç argümanlı pow() da gerektiğinde __rpow__'u dener hâle geldi.

İfadeÇağrılan metotYansıtılmış / yerinde
x + y__add____radd__ / __iadd__
x - y__sub____rsub__ / __isub__
x * y__mul____rmul__ / __imul__
x / y__truediv____rtruediv__ / __itruediv__
x // y__floordiv____rfloordiv__ / __ifloordiv__
x % y__mod____rmod__ / __imod__
x ** y__pow____rpow__ / __ipow__
x @ y__matmul____rmatmul__ / __imatmul__
-x, abs(x)__neg__, __abs__yok

Sıralama: karşılaştırma metotları ve total_ordering

__lt__, __le__, __gt__ ve __ge__ sıralama işleçlerini tanımlar. Belgeler bu metotlar arasında örtük bir ilişki olmadığını vurgular: x < y or x == y doğru diye x <= y kendiliğinden çalışmaz. __lt__ ile __gt__ birbirinin yansımasıdır; sol operand işlemi desteklemezse Python sağdakinin yansıtılmış metodunu dener. Altısını da yazmak yerine functools.total_ordering dekoratörü, bir sıralama metodu ve __eq__'dan geri kalanları türetir:

python
from functools import total_ordering

@total_ordering
class Surum:
    def __init__(self, metin):
        self.parca = tuple(int(p) for p in metin.split('.'))

    def __eq__(self, other):
        if not isinstance(other, Surum):
            return NotImplemented
        return self.parca == other.parca

    def __lt__(self, other):
        if not isinstance(other, Surum):
            return NotImplemented
        return self.parca < other.parca

    def __repr__(self):
        return '.'.join(map(str, self.parca))

liste = [Surum('3.15.2'), Surum('3.9.0'), Surum('3.13.1')]
print(sorted(liste))
print(Surum('3.10.0') >= Surum('3.9.9'), max(liste))
print('3.10.0' < '3.9.9')       # metin olarak karşılaştırma yanıltır
# çıktı: [3.9.0, 3.13.1, 3.15.2]
# çıktı: True 3.15.2
# çıktı: True

Son satır neden böyle bir sınıfa ihtiyaç duyduğumuzu gösteriyor: metin olarak karşılaştırıldığında "3.10.0", "3.9.9"'dan küçük çıkar. total_ordering belgesi, türetilen metotların biraz daha yavaş çalıştığını ve yığın izlerinin daha karmaşık olduğunu, performans kritikse altı metodu da elle yazmanın hızlandırabileceğini not eder. Yalnızca sıralamak için sınıf yazmak yerine sorted(..., key=...) kullanmak da bir seçenektir (lambda ve sorted yazısı).

__format__: f-string biçimlendirmesini özelleştirmek

f'{x:.2f}' ifadesindeki iki noktadan sonraki kısım, yani biçim belirtimi (format spec), nesnenin __format__ metoduna metin olarak iletilir. Aynı metot format() yerleşik fonksiyonu ve str.format() tarafından da çağrılır. Belgelere göre bu metinin nasıl yorumlanacağı tamamen tipi uygulayan sınıfa kalmıştır; çoğu sınıf işi yerleşik bir tipe devreder ya da standart biçim mini dilini taklit eder. Aşağıdaki sınıf sayısal kısmı float'a devrediyor ve sonuna kendi F son ekini ekliyor:

python
class Sicaklik:
    def __init__(self, derece):
        self.derece = derece

    def __format__(self, bicim):
        if bicim.endswith('F'):
            return format(self.derece * 9 / 5 + 32, bicim[:-1] or '') + ' °F'
        return format(self.derece, bicim) + ' °C'

s = Sicaklik(21.456)
print(f'{s}', f'{s:.1f}', f'{s:.0fF}')
print(format(s, '>10.2f'))

class Ham:
    pass

try:
    format(Ham(), '>10')
except TypeError as e:
    print(e)
# çıktı: 21.456 °C 21.5 °C 71 °F
# çıktı:      21.46 °C
# çıktı: unsupported format string passed to Ham.__format__

object sınıfının varsayılan __format__'ı yalnızca boş bir belirtimi kabul eder ve bu durumda str() sonucunu verir. Boş olmayan bir belirtim verildiğinde, Python 3.4'ten beri TypeError oluşur; son satırdaki hata budur. Yani __format__ tanımlamayan bir sınıfı f'{x:>10}' ile hizalamak istiyorsanız önce str(x)'e çevirmeniz ya da !s dönüşümünü kullanmanız gerekir: f'{x!s:>10}'. Biçim mini dilinin kendisi f-string yazısında ayrıntılı olarak anlatılıyor. Benzer şekilde __bytes__ metodu da bytes(x) çağrısında kullanılır ve object tarafından sağlanmaz.

Özel metotlar sınıfta aranır

Python, örtük çağrılarda özel metotları nesnenin sözlüğünde değil, doğrudan tipinde arar. Bu yüzden bir örneğe sonradan __len__ eklemek len()'i etkilemez; aynı metodu sınıfa eklemek ise hemen işe yarar:

python
class Sinif:
    pass

s = Sinif()
s.__len__ = lambda: 5           # örneğe eklenen özel metot
print(s.__len__())
try:
    len(s)
except TypeError as e:
    print(e)

Sinif.__len__ = lambda self: 7  # sınıfa eklenince çalışır
print(len(s))
# çıktı: 5
# çıktı: object of type 'Sinif' has no len()
# çıktı: 7

Belgeler bu tasarımın iki gerekçesini verir. Birincisi doğruluk: __hash__ ve __repr__ gibi metotlar tip nesnelerinde de vardır; sıradan arama kullanılsaydı hash(int) gibi çağrılar karışırdı. İkincisi hız: örtük arama __getattribute__ mekanizmasını da atlar, bu da yorumlayıcıya optimizasyon alanı açar.

__call__ ve __bool__

__call__ tanımlayan bir sınıfın örnekleri fonksiyon gibi çağrılabilir. Bu, ayar tutan küçük fonksiyon nesneleri için kullanışlıdır ve map() gibi fonksiyon bekleyen her yere verilebilir. __bool__ ise if nesne: ve bool() için kullanılır; True ya da False döndürmelidir. İkisi de tanımlı değilse (__bool__ ve __len__) her örnek doğru sayılır.

python
class Carpan:
    def __init__(self, k):
        self.k = k

    def __call__(self, x):
        return x * self.k

class Sepet:
    def __init__(self):
        self.urunler = []

    def __bool__(self):
        return bool(self.urunler)

iki_kat = Carpan(2)
print(iki_kat(21), callable(iki_kat))
print(list(map(Carpan(10), [1, 2, 3])))
sepet = Sepet()
print('dolu' if sepet else 'boş')
# çıktı: 42 True
# çıktı: [10, 20, 30]
# çıktı: boş

Bağlam yöneticisi protokolü olan __enter__ ve __exit__ da özel metotlardır; onları with ve context manager yazısında ayrıca anlatıyoruz. match deyiminde sınıf desenlerinin konumsal argümanlarını belirleyen __match_args__ için de match-case yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: __eq__ yazıp hash'i unutmak, NotImplemented yerine False döndürmek, __repr__ içinde print kullanmak, __getitem__'da yanlış istisna
  • __eq__ yazıp hash'i unutmak: Nesneler kümeye ve sözlük anahtarına giremez hâle gelir. Nesne değişmiyorsa __hash__ ekleyin, değişiyorsa hash'lenemez kalmasına izin verin.
  • NotImplemented yerine False döndürmek: Diğer operandın yansıtılmış metoduna şans tanımaz. NotImplemented bir istisna değildir; raise ile değil return ile döndürülür.
  • __repr__ içinde print kullanmak: Metot bir metin döndürmelidir; dönüş değeri str değilse Python 3.15.2 "__repr__() must return a str, not int" benzeri bir TypeError verir.
  • __getitem__'da yanlış istisna: Dizi gibi davranan bir sınıf, aralık dışı indekste IndexError fırlatmazsa for döngüsü sonsuza kadar sürebilir.
  • Özel metodu örneğe atamak: nesne.__len__ = ... örtük çağrılarda görülmez; metodu sınıfta tanımlayın.
  • Kendi dunder adını uydurmak: __ozel__ biçimindeki adlar Python'a ayrılmıştır; kendi yardımcı metotlarınızda bu biçimi kullanmayın.

Sık Sorulan Sorular

__repr__ ve __str__ ikisini de yazmak zorunda mıyım?

Hayır. Yalnızca __repr__ yazmak çoğu zaman yeterlidir, çünkü __str__ yoksa onun yerine kullanılır. Kullanıcıya farklı, daha sade bir metin göstermek istediğinizde __str__'ı ayrıca ekleyin.

Neden x.__len__() yerine len(x) yazılır?

len() gibi yerleşik fonksiyonlar özel metodu tip üzerinden, doğru kurallarla çağırır ve sonucu denetler; örneğin __len__ negatif bir sayı döndürürse len() "__len__() should return >= 0" mesajıyla ValueError verir. Özel metotları doğrudan çağırmak gerekmez.

Hangi özel metotların listesine nereden bakarım?

Tam liste resmî belgelerdeki veri modeli (Data model) sayfasının "Special method names" bölümündedir. Asenkron protokoller (__await__, __aiter__) için asyncio yazısına bakabilirsiniz.

__del__ yıkıcı (destructor) mıdır?

Belgeler bu adlandırmayı "uygunsuz" bulur. __del__, nesnenin referans sayısı sıfıra indiğinde çağrılır, del x ise yalnızca bir referansı siler. Yorumlayıcı kapanırken çağrılacağı da garanti değildir; kaynakları kapatmak için with deyimini tercih edin.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Python öğrenme rehberi · Class ve nesne · property ve descriptor · dataclasses · Generator ve iterator