Je kunt Python dictionaries zien als key/value paren, waarbij keys uniek moeten zijn.
Als keys uniek moeten zijn, hoe kon ik dan de volgende dictionary maken?
{'Apple': 1, 'Apple': 2, 'Apple': 3}
Dit is het onderwerp van deze post, maar laten we eerst begrijpen hoe dictionaries en hun keys werken.
Dictionaries zijn bedoeld om het opzoeken van een waarde op basis van zijn key te versnellen. Daarom gebruiken ze hashes, en bijgevolg moeten keys hashable zijn.
Laten we onze eigen class maken, die een __hash__() methode implementeert. Dus het is hashable, en we kunnen zijn objecten gebruiken als dictionary keys.
class Fruit:
def __hash__(self):
return 1
d = {
Fruit(): 1,
Fruit(): 2
}
# Printen van d geeft ons het volgende
# {<__main__.Fruit at 0x10873eb10>: 1, <__main__.Fruit at 0x10873d9d0>: 2}
De __hash__() methode retourneert een constante waarde, 1. D.w.z. beide instanties van Fruit hebben dezelfde hash. Desondanks worden ze twee aparte keys. Waarom?
Je weet waarschijnlijk het volgende over hash functies:
"Dezelfde waarden hebben dezelfde output, maar dezelfde output hebben garandeert niet dat de invoer hetzelfde is."
Het geval waarin verschillende waarden dezelfde hash hebben staat bekend als collision. Typisch moet een goede hash functie collisions minimaliseren. Desondanks zijn collisions nog steeds mogelijk.
Daarom stoppen Python dictionaries niet bij het controleren van de hash van de objecten die als keys worden gebruikt. Ze controleren waarschijnlijk ook of de objecten gelijk zijn. En in het geval van collisions zullen twee ongelijke objecten met gelijke hashes worden gezien als aparte keys.
Ideeën voor wat je volgende moet proberen?
Laten we de __eq__() methode ook overschrijven, en ervoor zorgen dat onze vruchten gelijk zijn.
class Fruit:
def __hash__(self):
return 1
def __eq__(self, other):
return True
d = {
Fruit(): 1,
Fruit(): 2
}
# Deze keer geeft printen van d ons het volgende
# {<__main__.Fruit at 0x10873ed50>: 2}
Tada! We hebben eindelijk gelijke hashes en gelijke keys, en beide keys worden nu als hetzelfde beschouwd. D.w.z. de tweede overschrijft de eerste.
Natuurlijk kunnen we programmatisch beslissen of we willen dat dingen gelijk zijn of niet.
class Fruit:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name
d = {
Fruit("Apple"): 1,
Fruit("Orange"): 2,
Fruit("Apple"): 3
}
# Deze keer geeft printen van d ons het volgende
# {<__main__.Fruit at 0x10873cb90>: 3, <__main__.Fruit at 0x108247ad0>: 2}
Maar keys zien er lelijk uit, we houden niet van dit <__main__.Fruit .. blah blah ding. Kunnen we dat veranderen?
Natuurlijk, via de __repr__() methode.
class Fruit:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name
def __repr__(self):
return f"'{self.name}'"
d = {
Fruit("Apple"): 1,
Fruit("Orange"): 2,
Fruit("Apple"): 3
}
# En nu geeft printen van d ons het volgende
# {'Apple': 3, 'Orange': 2}
Eindelijk staan we op het punt het raadsel dat ik bovenaan had te ontcijferen.
Alles wat we nodig hebben is een class, waarbij objecten verschillende hashes hebben, maar hun representaties hetzelfde zijn. Zoiets als dit:
class Fruit:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name
def __repr__(self):
return "'Apple'"
d = {
Fruit("Apple"): 1,
Fruit("Orange"): 2,
Fruit("Banana"): 3
}
# En nu geeft printen van d ons het volgende
# {'Apple': 1, 'Apple': 2, 'Apple': 3}
Eigenlijk is wat ik net heb gedaan verdomd kwaadaardig.
Men zou denken dat de dictionary d string waarden als keys heeft, gezien hoe het eruit ziet.
En ze krijgen een fout als ze zoiets als dit proberen:
d['Apple']
Voeg daaraan toe dat hoewel de 3 keys er hetzelfde uitzien, ze niet hetzelfde zijn.
Daarom is deze post alleen voor educatieve doeleinden.
Trouwens, als een class geen __hash__() en __eq__() methoden implementeert, biedt Python een standaardimplementatie, waarbij objecten niet gelijk zijn.
"User-defined classes hebben standaard __eq__() en __hash__() methoden; hiermee vergelijken alle objecten ongelijk (behalve met zichzelf) en x.__hash__() retourneert een geschikte waarde zodat x == y impliceert dat zowel x is y als hash(x) == hash(y)". Python docs
En er is een reden waarom Python wil dat dictionary keys onveranderlijk zijn. Anders kan men het volgende doen:
class Fruit:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name
def __repr__(self):
return self.name
apple = Fruit("Apple")
orange = Fruit("Orange")
d = {
apple: 1,
orange: 2,
}
Nu als je apple muteert zul je in problemen komen.
apple.name = "banana"
d[apple]
# Dit zal klagen door een KeyError te verhogen
# KeyError: banana
Zoals ik zei, is deze post alleen voor educatieve doeleinden.
Ik vind het gewoon leuk om te zien hoe dingen werken wanneer je ze buigt. Ik denk dat jij het ook leuk vindt om dingen te buigen en te breken om te zien hoe ze eigenlijk werken.
Tarek Amr, 12 januari 2023