Wanneer een Python-klasse __eq__ definieert om de gelijkheidsvergelijking aan te passen, stelt de interpreter automatisch __hash__ in op None tenzij deze expliciet wordt overschreven. Dit maakt de instantie onhashbaar, waardoor het gebruik als een dict sleutel of set lid wordt verhinderd. De onderliggende invariant vereist dat objecten die gelijk zijn via __eq__ identieke hashwaarden moeten opleveren; het schenden hiervan veroorzaakt ongedefinieerd gedrag in hash-gebaseerde verzamelingen. Bijgevolg leidt het proberen om zo'n object als een mapping sleutel te gebruiken tot een TypeError: unhashable type.
Een ontwikkelingsteam was bezig met het bouwen van een sessiebeheerservice waarbij User-objecten als sleutels dienden in een in-memory cache dict om actieve sessies op te slaan. De User-klasse implementeerde __eq__ om instanties te vergelijken op basis van user_id, waardoor ervoor gezorgd werd dat twee verschillende objecten die dezelfde databasegebruiker vertegenwoordigden, als gelijk werden behandeld. De initiële implementatie zag er als volgt uit:
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
In eerste instantie had het team __hash__ niet geïmplementeerd, in de veronderstelling dat het standaardgedrag voldoende zou zijn. Echter, toen de service probeerde een sessie te cachen met cache[user] = session_data, gaf Python een TypeError: unhashable type: 'User', waardoor de service crashte.
Het team overwoog drie oplossingen. De eerste aanpak gebruikte id(self) als de hashwaarde. Dit werd verworpen omdat het de kritieke invariant schond: twee verschillende User-instanties met dezelfde user_id zouden verschillende hashes hebben ondanks dat ze gelijk waren via __eq__. Dit zorgde ervoor dat ze als verschillende sleutels verschenen, waardoor cache-opzoekingen helemaal kapot gingen en dubbele entries voor dezelfde logische gebruiker mogelijk waren.
De tweede aanpak gebruikte hash(self.user_id) als de hashwaarde. Dit voldeed aan de invariant omdat gelijkwaardige gebruikers dezelfde user_id delen. Dit vereiste echter ervoor te zorgen dat user_id onveranderlijk was, omdat veranderlijke hashwaarden ervoor zouden zorgen dat het object "verloren" raakte in de dictionary als de ID veranderde na invoer.
De derde optie verliet het gebruik van User-objecten als sleutels en gebruikte in plaats daarvan direct de string user_id. Hoewel dit veilig en eenvoudig was, offerde dit typeveiligheid op en vereiste het het onderhouden van een aparte mapping van ID's naar User-objecten, waardoor de codebase werd gecompliceerd met extra opzoeklogica.
Het team koos voor de tweede oplossing en voegde de volgende implementatie toe aan de klasse:
def __hash__(self): return hash(self.user_id)
Ze maakten ook user_id een alleen-lezen eigenschap om onveranderlijkheid te waarborgen. Dit behoudt de mogelijkheid om User-instanties als sleutels te gebruiken en tegelijkertijd de juiste gelijkheidssemantiek te behouden. Het resultaat was een robuuste cache die gebruikers correct identificeerde, ongeacht de identiteit van het object.
Waarom stelt Python automatisch __hash__ in op None wanneer __eq__ wordt gedefinieerd maar __hash__ niet?
Wanneer een klasse __eq__ definieert, wordt de standaard identiteit-gebaseerde hash geërfd van object logisch ongeldig. De standaard __hash__ is gebaseerd op id(self), wat betekent dat twee verschillende objecten verschillende hashes hebben. Als __eq__ wordt overschreven om waarden te vergelijken, kunnen twee verschillende instanties gelijk zijn maar verschillende hashes hebben, wat de fundamentele regel schendt dat a == b impliceert hash(a) == hash(b). Python voorkomt deze inconsistentie door __hash__ in te stellen op None, wat de klasse expliciet markeert als onhashbaar in plaats van gevaarlijk standaardgedrag toe te staan dat zou leiden tot onvoorspelbare dictionary-prestaties of onbereikbare sleutels.
Wat gebeurt er als een veranderlijk object wordt gebruikt als een dictionary-sleutel na het implementeren van __hash__ op basis van veranderlijke velden?
Als __hash__ afhankelijk is van veranderlijke staat, kan de hashwaarde veranderen nadat het object in een dict is ingevoegd. Dictionaries slaan sleutels op in hash-buckets op basis van de hashwaarde op het moment van invoer. Als de hash later verandert door mutatie, berekenen opvolgende opzoekingen een andere hash en zoeken ze in een andere bucket, waardoor de oorspronkelijke sleutel onbereikbaar wordt. Het object blijft in het geheugen, maar kan niet worden gevonden of verwijderd via normale sleuteltoegang. Dit creëert een geheugenlek en logische inconsistentie, wat de reden is waarom Python vereist dat hashbare objecten onveranderlijk zijn of gebaseerd zijn op onveranderlijke identificatoren.
Hoe gaat de @dataclass-decorator om met de generatie van __eq__ en __hash__, en wat is het risico van het gebruik van unsafe_hash=True?
Standaard genereert @dataclass __eq__ op basis van veldwaarden, maar stelt __hash__ in op None, waardoor instanties onhashbaar zijn. Dit conservatieve standaard voorkomt bugs met veranderlijke dataclasses. Om hashing mogelijk te maken, moet je ofwel frozen=True instellen (waardoor velden alleen-lezen worden en een veilige __hash__ genereert) of expliciet unsafe_hash=True instellen. De parameter unsafe_hash=True dwingt Python om __hash__ te genereren op basis van veldwaarden, zelfs als de velden veranderlijk zijn. Dit is gevaarlijk omdat als een veld verandert nadat het object als een dictionary-sleutel is gebruikt, de hash verandert en de sleutel onbereikbaar wordt, wat leidt tot het "verloren sleutel"-probleem dat eerder is beschreven. Kandidaten missen vaak dat unsafe_hash niet slechts een waarschuwing is, maar een functioneel risico dat de dictionary-invarianten breekt.