I am trying to use an object as the key value to a dictionary in Python. I follow the recommendations from some other posts that we need to implement 2 functions: hash and eq
And with that, I am expecting the following to work but it didn't.
class Test:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(str(self.name))
def __eq__(self, other):
return str(self.name) == str(other,name)
def TestMethod():
test_Dict = {}
obj = Test('abc')
test_Dict[obj] = obj
print "%s" %(test_Dict[hash(str('abc'))].name) # expecting this to print "abc"
But it is giving me a key error message:
KeyError: 1453079729188098211
Can someone help enlightening me why this doesn't work?
Elements of a mapping are not accessed by their hash, even though their hash is used to place them within the mapping. You must use the same value when indexing both for storage and for retrieval.
You don't need to redefine hash
and eq
to use an object as dictionary key.
class Test:
def __init__(self, name):
self.name = name
test_Dict = {}
obj = Test('abc')
test_Dict[obj] = obj
print test_Dict[obj].name
This works fine and print abc
.
As explained by Ignacio Vazquez-Abrams you don't use the hash of the object but the object itself as key to access the dictionary value.
The examples you found like python: my classes as dict keys. how? or Object of custom type as dictionary key redefine hash
and eq
for specific purpose.
For example consider these two objects obj = Test('abc')
and obj2 = Test('abc')
.
test_Dict[obj] = obj
print test_Dict[obj2].name
This will throw a KeyError
exception because obj and obj2 are not the same object.
class Test:
def __init__(self, name):
self.name = name
def __hash__(self):
return hash(str(self.name))
def __eq__(self, other):
return str(self.name) == str(other.name)
obj = Test('abc')
obj2 = Test('abc')
test_Dict[obj] = obj
print test_Dict[obj2].name
This print abc
. obj
and obj2
are still different objects but now they have the same hash and are evaluated as equals when compared.
Error Explanation
Given the code provided in the post, I don't actually see how you get the KeyError because you should be receiving an AttributeError (assuming the str(other,name)
was a typo meant to be str(other.name)
). The AttributeError comes from the __eq__
method when comparing the name of self against the name of other because your key during lookup, hash(str('abc'))
, is an int/long, not a Test
object.
When looking up a key in a dict, the first operation performed is getting the hash of the key using the key's __hash__
method. Second, if a value for this hash exists in the dict, the __eq__
method of the key is called to compare the key against whatever value was found. This is to make sure that in the event objects with the same hash are stored in the dict (through open addressing), the correct object is retrieved. On average this lookup is still O(1).
Looking at this one step at a time, the hash of hash(str('abc'))
and obj
are the same. In Test
, you define __hash__
as the hash of a string. When performing lookup with test_Dict[hash(str('abc'))]
, you are actually looking up the hash of a hash, but this is still fine since the hash of an int is itself in python.
When comparing these two values according to your defined __eq__
method, you compare the names of the objects, but the value you are comparing against is an int (hash(str('abc'))
), which does not have a name
property, so the AttributeError is raised.
Solution
Firstly, you do not need to (and should not) call hash()
when performing the actual dict lookup since this key is also passed as the second argument to your __eq__
method. So
test_Dict[hash(str('abc'))].name
should become
test_Dict[str('abc')].name
or just
test_Dict['abc'].name
since calling str()
on a string literal doesn't make much sense.
Secondly, you will need to edit your __eq__
method such that it takes into account the type of the other
object you are comparing against. You have different options for this depending on what else will be stored in the same dict with your Test
instances as keys.
If you plan to store your Test
instances in a dictionary only with other Test
s (or any objects that have a name
property), then you can keep what you currently have
def __eq__(self, other):
return str(self.name) == str(other.name)
since you guarantee that every other key you are comparing against in the dict is of type Test
and has a name
.
If you plan to mix your Test
instances in a dictionary with just strings, you will have to check if the object you compare against is a string or not since strings do not have a name
property in python.
def __eq__(self, other):
if isinstance(other, str):
return str(self.name) == other
return str(self.name) == str(other.name)
If you plan to use as keys a mix of Test
s and any other types of objects, you will need to check if the other
object has a name
to compare against.
def __eq__(self, other):
if hasattr(other, "name"):
return str(self.name) == str(other.name)
return self.name == other # Or some other logic since here since I don't know how you want to handle other types of classes.
I'm not much of a fan of the last 2 since you kind of go against duck typing in python with those, but there are always exceptions in life.