Karşılaştırma Operatörleri ve Sortable Class: __eq__, __lt__ ve total_ordering Sırrı
< > == != <= >= görünüşte basit ama Python'da chained comparison (`0 < x < 10`), her tip için custom karşılaştırma, ve `@total_ordering` decorator gibi süslü özellikler var. Bu derste: 6 karşılaştırma operatörü derinlemesine, custom sortable class yapımı, __hash__ ve __eq__ kontratı, list/tuple/string karşılaştırma kuralları, ve sıralama için key fonksiyon pattern'leri.
Şükrü Yusuf KAYA
22 dakikalık okuma
Orta🎯 Karşılaştırma — sıralamanın temeli
Bir liste sortlamak istediğinde Python ne yapıyor? Elemanları karşılaştırıyor. "<" operatörünün bilmediği bir tip varsa hata atıyor. Bu derste, custom sınıfını sortable yapmak ve neden her eq değişikliğinde hash'ı düşünmek gerektiğini öğreniyoruz.
6 Karşılaştırma Operatörü#
| Operatör | Anlam | Magic method |
|---|---|---|
== | Eşit | __eq__ |
!= | Eşit değil | __ne__ |
< | Küçük | __lt__ |
> | Büyük | __gt__ |
<= | Küçük eşit | __le__ |
>= | Büyük eşit | __ge__ |
Tip-spesifik davranışlar:
# Sayılar (numeric) print(5 == 5.0) # True (int ve float, değer eşit) print(5 == "5") # False (farklı tip) print(Decimal("5") == 5) # True (Decimal int ile karşılaştırılabilir) # String — lexicographic print("apple" < "banana") # True (alfabetik) print("Apple" < "apple") # True (büyük harf önce — ASCII 65 < 97) print("ç" < "d") # False! Türkçe sıralama Unicode'la sürpriz # List — eleman eleman print([1, 2, 3] == [1, 2, 3]) # True print([1, 2, 3] < [1, 2, 4]) # True (3 < 4) print([1, 2] < [1, 2, 3]) # True (kısa olan küçük) print([1, 2, 3] < [1, 2]) # False print([1, "a"] < [1, "b"]) # True # Tuple — list ile aynı print((1, 2) == (1, 2)) # True print((1, "a") < (1, "b")) # True # Dict — sadece == ve !=, < > yok! print({1: "a"} == {1: "a"}) # True # print({1: "a"} < {1: "b"}) # TypeError! # Set — alt küme operatörü print({1, 2} < {1, 2, 3}) # True (proper subset) print({1, 2} == {1, 2}) # True
Önemli: dict'in davranışı yok. Set'in davranışı "subset" anlamı taşıyor — sayısal değil mantıksal.
<<Chained comparison — Python'un zarafeti#
Python'da matematiksel notasyon doğal yazılır:
x = 5 # Pythonic if 0 < x < 10: print("Aralıkta") # Java/C tarzı (uzun) if 0 < x and x < 10: print("Aralıkta")
İkisi de mantıksal olarak aynı ama chained versiyon sadece bir kez evaluate ediyor. Yan etkili (side effect) işlemlerde fark eder:
xdef get_value(): print("Çağrıldı!") return 5 # Klasik if 0 < get_value() and get_value() < 10: print("OK") # Çağrıldı! (1) # Çağrıldı! (2 — fonksiyon iki kez çalıştı) # OK # Chained if 0 < get_value() < 10: print("OK") # Çağrıldı! (sadece 1) # OK
Daha karmaşık chain'ler#
# Üç sınır age = 25 if 18 <= age < 65: print("Çalışma yaşında") # Eşitlik chain'i a, b, c = 5, 5, 5 if a == b == c: print("Üçü eşit") # Karışık operatör x, y, z = 1, 5, 10 if x < y < z: print("Sıralı") # OK # Garip ama geçerli if 1 < 2 > 0: print("True ama anlam karışık") # 1<2 (True) AND 2>0 (True) → True
🚨 Kafayı karıştıran chain'lerden kaçın:
# Anlam karışık — okumak zor if a < b > c < d: # 🚫 ... # Daha temiz if a < b and b > c and c < d: # ✅ ...
Chain'i sadece "x bir aralıkta mı" gibi doğal durumlarda kullan.
__eq__ ve __hash__ kontratı#
__eq____hash__Bu Python'un kritik kurallarından biri: değiştirirsen de düşün.
__eq____hash__class Point: def __init__(self, x, y): self.x = x self.y = y p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # False (default __eq__ nesne kimliği) print(hash(p1)) # bir sayı (default — id'ye dayalı) # Eğer Point'i değer-bazlı eşit yapmak istersek class Point2: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point2): return NotImplemented return self.x == other.x and self.y == other.y p1 = Point2(1, 2) p2 = Point2(1, 2) print(p1 == p2) # True print(hash(p1)) # TypeError: unhashable type: 'Point2'
🚨 tanımlayınca otomatik None oluyor! Yani sınıf set'te veya dict key olarak kullanılamıyor.
__eq____hash__Çözüm: da tanımla.
__hash__class Point3: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point3): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y)) # tuple'ın hash'ini kullan p1 = Point3(1, 2) p2 = Point3(1, 2) print(p1 == p2) # True print(hash(p1) == hash(p2)) # True print({p1, p2}) # {Point3(...)} — tek eleman (eşit oldukları için)
Kontrat (Python language reference)#
İki nesne eşitse (),a == bolmalı.hash(a) == hash(b)
Tersi şart değil — iki nesnenin hash'i eşit olabilir ama eşit olmayabilirler (hash çakışması, normal). Ama eşit nesnelerin hash'i mutlaka eşit olmalı.
Bu kuralı bozmak: dict/set'in iç işlemleri bozulur. Aynı veri farklı yerlere konur, lookup başarısız olur. Production'da çok sinsi bug.
Mutable nesne hashable olmamalı#
class MutablePoint: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y)) p = MutablePoint(1, 2) s = {p} p.x = 100 # mutate print(p in s) # False! — hash değişti, set artık bulamıyor # Yani mutate edilebilen sınıf hashable yapma
Pratik kural: mutable sınıfı hashable yapma. Frozen dataclass veya immutable tuple-tabanlı sınıf — evet. Mutable list/dict-tabanlı — hayır.
@total_ordering — kısa yol#
@total_orderingBir sınıfı sortable yapmak için 6 method tanımlaman gerekiyor: , , , , , . Sıkıcı.
__eq____lt____le____gt____ge____ne__functools.total_orderingfrom functools import total_ordering @total_ordering class Version: def __init__(self, major, minor, patch): self.major = major self.minor = minor self.patch = patch def __eq__(self, other): if not isinstance(other, Version): return NotImplemented return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch) def __lt__(self, other): if not isinstance(other, Version): return NotImplemented return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) def __hash__(self): return hash((self.major, self.minor, self.patch)) def __repr__(self): return f"v{self.major}.{self.minor}.{self.patch}" v1 = Version(1, 2, 3) v2 = Version(1, 2, 4) v3 = Version(2, 0, 0) # total_ordering otomatik üretti — hepsi çalışıyor print(v1 == v2) # False print(v1 < v2) # True print(v1 <= v2) # True (otomatik üretildi!) print(v3 > v1) # True (otomatik!) print(v3 >= v1) # True (otomatik) print(v1 != v2) # True (otomatik) # Sorting versions = [v3, v1, v2] sorted_versions = sorted(versions) print(sorted_versions) # [v1.2.3, v1.2.4, v2.0.0] # min/max print(min(versions)) # v1.2.3 print(max(versions)) # v2.0.0
🎯 Tuple comparison hack: — tuple'ları karşılaştırarak çoklu-alan sıralama. Python tuple'ı eleman-eleman karşılaştırıyor, ilk farklı bulduğunda karar veriyor. Major eşitse minor'a bakıyor, vs.
(self.major, self.minor, self.patch) < (other...)Performance: runtime'da method üretiyor — biraz yavaş. Performance-critical kodda 6 method'u manuel yaz. Çoğu uygulama için fark yok.
@total_orderingModern alternatif: dataclass(order=True)#
from dataclasses import dataclass @dataclass(order=True, frozen=True) class Version2: major: int minor: int patch: int v1 = Version2(1, 2, 3) v2 = Version2(1, 2, 4) print(v1 < v2) # True (otomatik) print(sorted([v2, v1])) # [Version2(1, 2, 3), Version2(1, 2, 4)]
Daha az kod. Field sırası önemli — Python attribute sırasına göre karşılaştırıyor. ile hashable de oldu.
frozen=TruePratik tavsiye: Yeni sınıf yazıyorsan ile başla; özel logic gerekirse veya manuel.
@dataclass(order=True, frozen=True)@total_orderingSorting — key parametresi#
keySıralama için sınıfı sortable yapmak şart değil; parametresi yeter.
keypeople = [ {"name": "Ali", "age": 30, "salary": 50000}, {"name": "Ayşe", "age": 25, "salary": 60000}, {"name": "Mehmet", "age": 35, "salary": 45000}, ] # Yaşa göre sorted_by_age = sorted(people, key=lambda p: p["age"]) # Maaşa göre azalan sorted_by_salary_desc = sorted(people, key=lambda p: p["salary"], reverse=True) # Multi-key: önce yaşa, eşitlikte maaşa sorted_multi = sorted(people, key=lambda p: (p["age"], -p["salary"])) # operator.itemgetter — daha hızlı (lambda'dan) from operator import itemgetter sorted_fast = sorted(people, key=itemgetter("age")) # Multi-key itemgetter sorted_multi_fast = sorted(people, key=itemgetter("age", "salary")) # Sınıflar için attrgetter from operator import attrgetter class Person: def __init__(self, name, age, salary): self.name = name self.age = age self.salary = salary people_obj = [Person("Ali", 30, 50000), Person("Ayşe", 25, 60000)] # Yaşa göre sorted(people_obj, key=attrgetter("age")) sorted(people_obj, key=lambda p: p.age) # eşdeğer
itemgetterattrgetterreverse parametresi#
reverse# Yaşa göre azalan sorted(people, key=itemgetter("age"), reverse=True) # Multi-key + reverse # Önce yaşa azalan, sonra maaşa artan? # Doğrudan reverse=True her iki sıralamayı da etkiler — istemediğin şey # Çözüm: negative trick sorted(people, key=lambda p: (-p["age"], p["salary"]))
Stable sort#
Python'un ve Timsort algoritması — stable (eşit elemanların orijinal sırası korunur).
sorted()list.sort()# Önce maaşa, sonra yaşa sırala data = [ {"name": "A", "age": 30, "salary": 5000}, {"name": "B", "age": 25, "salary": 5000}, {"name": "C", "age": 30, "salary": 6000}, ] # Multi-key tek seferde result = sorted(data, key=itemgetter("salary", "age")) # Veya stable sort kullanarak (önce ikinci sırada olanı sırala, sonra birincil) result = sorted(data, key=itemgetter("age")) result = sorted(result, key=itemgetter("salary")) # Aynı sonuç — Timsort stable olduğu için
Timsort 2002'de Tim Peters (Zen of Python yazarı!) tarafından geliştirildi. Java, Android, V8 (Node.js) — hepsi Timsort kullanıyor.
Yaygın tuzaklar#
1. == ile float#
==(Önceki ders Modül 2/4 hatırla)
# 🚫 if 0.1 + 0.2 == 0.3: ... # ✅ import math if math.isclose(0.1 + 0.2, 0.3): ...
2. is ile değer karşılaştırma#
is# 🚫 — implementation detail'a güvenme x = 5 if x is 5: ... # ✅ if x == 5: ... # Sadece sentinel için is if x is None: ... if x is True: ... # nadiren — 'if x:' daha iyi
3. Mixed type comparison#
# Python 3'te eşitsizlik karşılaştırma farklı tipler arasında — TypeError 1 < "1" # TypeError [1, 2] < (1, 2) # TypeError # Sadece eşitlik check'i çalışır (False döner) 1 == "1" # False (uyarı yok, OK)
Python 2'de garip kurallar vardı (str ve int sıralanabilirdi); Python 3'te düzeldi — explicit hata.
4. NaN comparison (önceki ders)#
nan = float('nan') print(nan == nan) # False! print(nan != nan) # True print(nan < 1) # False print(nan > 1) # False
NaN list'i sıralarsan random sıraya girebilir. Önce ile filtrele.
math.isnan5. __eq__ tanımlayıp __hash__ unutma#
__eq____hash__class MyClass: def __eq__(self, other): return ... # __hash__ otomatik None oluyor! # Set/dict key olarak kullanılamaz
__eq____hash__6. Mutable ile karşılaştırma#
class Cart: def __init__(self, items): self.items = items def __eq__(self, other): return self.items == other.items # liste karşılaştırma c1 = Cart([1, 2, 3]) c2 = Cart([1, 2, 3]) print(c1 == c2) # True c1.items.append(4) print(c1 == c2) # False (liste değişti) # Eğer c1'i set'e koymuştuysak (hashable yapmıştık varsayalım) # hash değişti — set artık bulamıyor — bug
Mutable sınıfı eşit yapmak OK ama hashable yapma.
7. Inheritance ile karşılaştırma#
class Animal: def __eq__(self, other): return type(self) == type(other) and self.name == other.name class Dog(Animal): pass class Cat(Animal): pass d = Dog("Karabaş") d2 = Dog("Karabaş") c = Cat("Karabaş") print(d == d2) # True print(d == c) # False (type kontrolü)
Inheritance ile karışırsa: mu yoksa mı? Karar bağlama: aynı subclass mı eşit (strict)? Yoksa Animal hierarchy'sindeki herhangi bir sınıf eşit mi (gevşek)?
type(self) == type(other)isinstanceLiskov substitution prensibi: subclass instance'ı, parent class instance'ı yerine geçebilmeli. Strict tip kontrolü bunu bozar. Genelde tercih edilir.
isinstanceBu derste neler kazandın?#
✓ 6 karşılaştırma operatörü + tip-spesifik davranışlar (str lexicographic, list element-by-element, set subset).
✓ Chained comparison '0 < x < 10' — Pythonic, side-effect güvenli.
✓ 'eq' ve 'hash' kontratı — eşit nesneler aynı hash dönmeli.
✓ '@total_ordering' decorator — 4 method'u eq + lt'den otomatik.
✓ '@dataclass(order=True, frozen=True)' — modern minimal kod.
✓ Tuple comparison ile multi-field sorting.
✓ 'key' parametresi, 'itemgetter', 'attrgetter' — performance optimal.
✓ Stable sort (Timsort) — eşit elemanların sırası korunuyor.
✓ Multi-key sorting trick'leri — negative for desc, sequential sorts.
✓ 7 yaygın tuzak — float ==, is değer, mixed type, NaN, hash unutma, mutable + hash, inheritance.
Sıradaki ders: Mantıksal operatörler (, , ). De Morgan kanunları, ve built-in'leri, conditional expression (ternary), validator pattern'leri.
andornotany()all()Sık Sorulan Sorular
Bağlama bağlı. **Dataclass**: Field sırasına göre karşılaştırma, hızlı, az kod. Dezavantaj: tüm field'ları karşılaştırıyor; bazı field'ları dahil etmek istemiyorsan field'da `compare=False`. **total_ordering**: Custom logic mümkün — sadece bazı alanları karşılaştır, transformasyon uygula, type checks. Kural: basit dataclass-style ise `@dataclass(order=True)`; özel logic ise `@total_ordering` + manuel `__eq__/__lt__`.
Yorumlar & Soru-Cevap
(0)Yorum yazmak için giriş yap.
Yorumlar yükleniyor...
İlgili İçerikler
Modül 1: Giriş ve Kurulum
Python Nedir, Neden Bu Kadar Popüler?
Öğrenmeye BaşlaModül 1: Giriş ve Kurulum
Python Sürümlerinin Tarihi: 2'den 3.14'e, AI Winter'lardan 'No-GIL' Devrimine
Öğrenmeye BaşlaModül 1: Giriş ve Kurulum