Bir Python sınıfı, eşitlik karşılaştırmasını özelleştirmek için __eq__ tanımladığında, yorumlayıcı otomatik olarak __hash__'ı None olarak ayarlar, aksi açıkça değiştirilmedikçe. Bu, örneğin, dict anahtarı veya set üyesi olarak kullanılmasını engelleyerek örneği hashlenemez hale getirir. Temel değişmezlik, __eq__ ile karşılaştırdığı nesnelerin aynı hash değerlerini vermesi gerektiğini gerektirir; bunun ihlali hash tabanlı koleksiyonlarda tanımsız davranışa neden olur. Sonuç olarak, böyle bir nesneyi bir haritalama anahtarı olarak kullanmaya çalışmak TypeError: unhashable type hatasını tetikler.
Bir geliştirme ekibi, User nesnelerinin aktif oturumları depolamak için bir bellek içi cache dict'inde anahtar olarak kullanıldığı bir oturum yönetim servisi geliştiriyordu. User sınıfı, iki farklı nesnenin aynı veritabanı kullanıcısını temsil ettiğinden emin olmak için, user_id'ye göre örnekleri karşılaştırmak için __eq__'u uyguladı. İlk uygulama şöyle görünüyordu:
class User: def __init__(self, user_id, name): self.user_id = user_id self.name = name def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.user_id == other.user_id
Başlangıçta, ekip __hash__'u uygulamayı atlayarak varsayılan davranışın yeterince olacağını düşündü. Ancak, hizmet bir oturumu önbelleğe almak için cache[user] = session_data kullanmaya çalıştığında, Python TypeError: unhashable type: 'User' hatası vererek hizmetin çökmesine neden oldu.
Ekip üç çözüm üzerinde düşündü. İlk yaklaşım, hash değeri olarak id(self) kullanmayı önerdi. Bu, iki farklı User örneğinin aynı user_id'ye sahip olmasına rağmen farklı hash'lere sahip olacağı için bu kritik değişmezliği ihlal edeceğinden reddedildi. Bu, onları farklı anahtarlar olarak göstermekte ve önbellek aramaları tamamen etkisiz hale getirmekteydi; aynı mantıksal kullanıcı için birden fazla girişe müsaade ediyordu.
İkinci yaklaşım, hash değeri olarak hash(self.user_id) kullanmayı önerdi. Bu, eşit kullanıcıların aynı user_id'ye sahip olmasından dolayı değişmezliği sağlıyordu. Ancak, bu, user_id'nin değişmez olmasını gerektiriyordu, çünkü değişken hash değerleri nesnenin eklenmesinden sonra kimliğinin değişmesi durumunda haritada "kaybolmasına" neden olabilirdi.
Üçüncü seçenek, User nesnelerini anahtar olarak kullanmayı bırakmak ve bunun yerine user_id'yi bir string olarak kullanmaktı. Güvenli ve basit olmasına rağmen, bu tür güvenliği feda etmekteydi ve User nesnelerine ayrı bir haritalamanın korunmasını gerektirmekteydi; bu da kod tabanını ek arama mantığı ile karmaşıklaştırıyordu.
Ekip, ikinci çözümü seçerek sınıfa aşağıdaki uygulamayı ekledi:
def __hash__(self): return hash(self.user_id)
Ayrıca, user_id'yi değiştirilemezlik sağlamak için yalnızca okunabilir bir özellik haline getirdiler. Bu, User örneklerini anahtar olarak kullanabilmeyi sağlarken eşitlik anlamlarını doğru bir şekilde korudu. Sonuç olarak, nesne örneği kimliğine bakılmaksızın kullanıcıları doğru bir şekilde tanımlayan sağlam bir önbellek elde ettiler.
Neden Python, __eq__ belirlendiğinde ama __hash__ uygulanmadığında otomatik olarak __hash__'ı None olarak ayarlar?
Bir sınıf __eq__ tanımladığında, object'tan miras alınan varsayılan kimlik bazlı hash mantıksal olarak geçersiz hale gelir. Varsayılan __hash__, id(self)'ye dayanarak iki ayrı nesnenin farklı hash'lere sahip olmasını sağlar. Eğer __eq__, değerleri karşılaştırmak için geçersiz hale getirilirse, iki farklı örnek eşit olabilir ancak farklı hash'lere sahip olacaktır; bu da a == b'nin hash(a) == hash(b)'yi gerektirdiği temel kuralı ihlal eder. Python bu tutarsızlığı önleyerek, özel davranışın tehlikeli olmasını engelleyerek sınıfı hashlenemez olarak işaretler.
Eğer bir değişken nesne, değişken alanlara dayanan __hash__ uygulanmış bir harita anahtarı olarak kullanılırsa ne olur?
Eğer __hash__ değişken bir duruma dayanıyorsa, hash değeri nesne bir dict'e eklenmişken değişebilir. Sözlükler anahtarları, ekleme anındaki hash değerine göre hash kovalara depolar. Eğer hash, değişim nedeniyle daha sonra değişirse, sonraki aramalar farklı bir hash hesaplar ve farklı bir kovada arama yapar; bu, orijinal anahtarı erişilemez hale getirir. Nesne bellek içinde kalır, ancak normal anahtar erişim yöntemleriyle bulunamaz veya silinemez. Bu bir bellek sızıntısı ve mantıksal tutarsızlık yaratır, bu nedenle Python, hashlenebilir nesnelerin değişmez veya değişmez tanımlayıcılara dayalı olmasını gerektirir.
@dataclass dekoratörü __eq__ ve __hash__ üretimini nasıl yönetir ve unsafe_hash=True kullanmanın riski nedir?
Varsayılan olarak, @dataclass alan değerlerine dayalı olarak __eq__ oluşturur ancak __hash__'ı None olarak ayarlar, bu da örnekleri hashlenemez hale getirir. Bu ihtiyatlı varsayılan, değişken dataclass'larla ilgili hataları önler. Hashlemeyi etkinleştirmek için, ya frozen=True ayarlanması (alanları yalnızca okunur yaparak güvenli bir __hash__ üretmek) ya da unsafe_hash=True açıkça ayarlanmalıdır. unsafe_hash=True parametresi, Python'un, alan değerlerine dayalı olarak hash üretmesini zorlamakta, bu da alanlar değişken olsa bile hash elde etmesine neden olmaktadır. Bu tehlikelidir çünkü herhangi bir alan, nesne bir sözlük anahtarı olarak kullanıldıktan sonra değişirse, hash de değişir ve anahtar erişilemez hale gelir; bu, daha önce tanımlanan "kaybolan anahtar" sorununa yol açar. Adayların sıklıkla gözden kaçırdığı, unsafe_hash'ın yalnızca bir uyarı değil, sözlük değişmezliklerini bozan bir işlev riskidir.