Using object as key in dictionary in Python - Hash

2019-04-28 22:02发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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 Tests (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 Tests 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.