Using non-hashable Python objects as keys in dictionaries

后端 未结 8 1549
栀梦
栀梦 2020-12-19 16:00

Python doesn\'t allow non-hashable objects to be used as keys in other dictionaries. As pointed out by Andrey Vlasovskikh, there is a nice workaround for the special case of

相关标签:
8条回答
  • 2020-12-19 16:06

    Don't. I agree with Andreys comment on the previous question that is doesn't make sense to have dictionaries as keys, and especially not nested ones. Your data-model is obviously quite complex, and dictionaries are probably not the right answer. You should try some OO instead.

    0 讨论(0)
  • 2020-12-19 16:06

    I totally disagree with comments & answers saying that this shouldn't be done for data model purity reason.

    A dictionary associates an object with another object using the former one as a key. Dictionaries can't be used as keys because they're not hashable. This doesn't make any less meaningful/practical/necessary to map dictionaries to other objects.

    As I understand the Python binding system, you can bind any dictionary to a number of variables (or the reverse, depends on your terminology) which means that these variables all know the same unique 'pointer' to that dictionary. Wouldn't it be possible to use that identifier as the hashing key ? If your data model ensures/enforces that you can't have two dictionaries with the same content used as keys then that seems to be a safe technique to me.

    I should add that I have no idea whatsoever of how that can/should be done though.

    I'm not entirely whether this should be an answer or a comment. Please correct me if needed.

    0 讨论(0)
  • 2020-12-19 16:06

    I encountered this issue when using a decorator that caches the results of previous calls based on call signature. I do not agree with the comments/answers here to the effect of "you should not do this", but I think it is important to recognize the potential for surprising and unexpected behavior when going down this path. My thought is that since instances are both mutable and hashable, and it does not seem practical to change that, there is nothing inherently wrong with creating hashable equivalents of non-hashable types or objects. But of course that is only my opinion.

    For anyone who requires Python 2.5 compatibility, the below may be useful. I based it on the earlier answer.

    from itertools import imap
    tuplemap = lambda f, data: tuple(imap(f, data))
    def make_hashable(obj):
      u"Returns a deep, non-destructive conversion of given object to an equivalent hashable object"
      if isinstance(obj, list):
        return tuplemap(make_hashable, iter(obj))
      elif isinstance(obj, dict):
        return frozenset(tuplemap(make_hashable, obj.iteritems()))
      elif hasattr(obj, '__hash__') and callable(obj.__hash__):
        try:
          obj.__hash__()
        except:
          if hasattr(obj, '__iter__') and callable(obj.__iter__):
            return tuplemap(make_hashable, iter(obj))
          else:
            raise NotImplementedError, 'object of type %s cannot be made hashable' % (type(obj),)
        else:
          return obj
      elif hasattr(obj, '__iter__') and callable(obj.__iter__):
        return tuplemap(make_hashable, iter(obj))
      else:
        raise NotImplementedError, 'object of type %s cannot be made hashable' % (type, obj)
    
    0 讨论(0)
  • 2020-12-19 16:12

    Based off solution by Chris Lutz. Note that this doesn't handle objects that are changed by iteration, such as streams, nor does it handle cycles.

    import collections
    
    def make_hashable(obj):
        """WARNING: This function only works on a limited subset of objects
        Make a range of objects hashable. 
        Accepts embedded dictionaries, lists or tuples (including namedtuples)"""
        if isinstance(obj, collections.Hashable):
            #Fine to be hashed without any changes
            return obj
        elif isinstance(obj, collections.Mapping):
            #Convert into a frozenset instead
            items=list(obj.items())
            for i, item in enumerate(items):
                    items[i]=make_hashable(item)
            return frozenset(items)
        elif isinstance(obj, collections.Iterable):
            #Convert into a tuple instead
            ret=[type(obj)]
            for i, item in enumerate(obj):
                    ret.append(make_hashable(item))
            return tuple(ret)
        #Use the id of the object
        return id(obj)
    
    0 讨论(0)
  • 2020-12-19 16:13

    If you really must, make your objects hashable. Subclass whatever you want to put in as a key, and provide a __hash__ function which returns an unique key to this object.

    To illustrate:

    >>> ("a",).__hash__()
    986073539
    >>> {'a': 'b'}.__hash__()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'NoneType' object is not callable
    

    If your hash is not unique enough you will get collisions. May be slow as well.

    0 讨论(0)
  • 2020-12-19 16:17

    Based off solution by Chris Lutz again.

    import collections
    
    def hashable(obj):
        if isinstance(obj, collections.Hashable):
            items = obj
        elif isinstance(obj, collections.Mapping):
            items = frozenset((k, hashable(v)) for k, v in obj.iteritems())
        elif isinstance(obj, collections.Iterable):
            items = tuple(hashable(item) for item in obj)
        else:
            raise TypeError(type(obj))
    
        return items
    
    0 讨论(0)
提交回复
热议问题