Использование не хешируемых объектов Python в качестве ключей в словарях

Python не позволяет использовать не хэшируемые объекты в качестве ключей в других словарях. Как указал Андрей Власовских, есть хороший обходной путь для частного случая использования в качестве ключей не вложенных словарей:

frozenset(a.items())#Can be put in the dictionary instead

Есть ли способ использования произвольных объектов в качестве ключей в словарях?

Пример:

Как это будет использоваться в качестве ключа?

{"a":1, "b":{"c":10}}

Крайне редко вам придется использовать что-то подобное в своем коде. Если вы считаете, что это так, сначала подумайте об изменении своей модели данных.

Точный вариант использования

Вариант использования - кэширование вызовов произвольной функции только для ключевых слов. Каждый ключ в словаре представляет собой строку (имя аргумента), и объекты могут быть довольно сложными, состоящими из многоуровневых словарей, списков, кортежей и т. Д.

Связанные проблемы

Эта подзадача была отделена от проблемы здесь. Решения здесь имеют дело со случаем, когда словари не являются многоуровневыми.

8 ответов

Решение

Основано на решении Криса Лутца. Обратите внимание, что это не обрабатывает объекты, которые изменяются путем итерации, такие как потоки, и не обрабатывает циклы.

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)

На основе решения Криса Лутца снова.

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

Не. Я согласен с комментарием Андрея по предыдущему вопросу, который не имеет смысла иметь словари в качестве ключей, и особенно не вложенные. Ваша модель данных, очевидно, довольно сложна, и словари, вероятно, не правильный ответ. Вы должны попробовать OO вместо этого.

Я согласен с Леннартом Регебро, что вы этого не делаете. Однако я часто нахожу полезным кэшировать некоторые вызовы функций, вызываемые объекты и / или объекты Flyweight, поскольку они могут использовать ключевые аргументы.

Но если вы действительно этого хотите, попробуйте pickle.dumps (или же cPickle если python 2.6) как быстрый и грязный хак. Это намного быстрее, чем любой из ответов, использующих рекурсивные вызовы, чтобы сделать элементы неизменяемыми, а строки можно хэшировать.

import pickle
hashable_str = pickle.dumps(unhashable_object)

Если вам действительно нужно, сделайте ваши объекты хэшируемыми. Подкласс, который вы хотите вставить в качестве ключа, и предоставляет функцию __hash__, которая возвращает уникальный ключ для этого объекта.

Проиллюстрировать:

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

Если ваш хэш не достаточно уникален, вы получите коллизии. Может быть и медленным.

Я полностью не согласен с комментариями и ответами, говоря, что это не должно быть сделано по причине чистоты модели данных.

Словарь связывает объект с другим объектом, используя первый в качестве ключа. Словари нельзя использовать в качестве ключей, потому что они не могут быть хэшируемыми. Это не делает ничего менее значимым / практичным / необходимым для отображения словарей на другие объекты.

Как я понимаю систему привязки Python, вы можете привязать любой словарь к ряду переменных (или наоборот, зависит от вашей терминологии), что означает, что все эти переменные знают один и тот же уникальный "указатель" на этот словарь. Разве нельзя было бы использовать этот идентификатор в качестве ключа хеширования? Если ваша модель данных гарантирует / обеспечивает, что вы не можете иметь два словаря с одинаковым содержимым, используемым в качестве ключей, то это кажется мне безопасным методом.

Я должен добавить, что я понятия не имею о том, как это может / должно быть сделано, хотя.

Я не совсем, должен ли это быть ответ или комментарий. Пожалуйста, поправьте меня, если это необходимо.

Я столкнулся с этой проблемой при использовании декоратора, который кэширует результаты предыдущих вызовов на основе подписи вызова. Я не согласен с комментариями / ответами здесь о том, что "вы не должны этого делать", но я думаю, что важно признать потенциал неожиданного и неожиданного поведения при переходе по этому пути. Я думаю, что поскольку экземпляры являются как изменяемыми, так и хешируемыми, и изменить это не представляется практичным, нет ничего изначально неправильного в создании хешируемых эквивалентов нехешируемых типов или объектов. Но, конечно, это только мое мнение.

Для тех, кому требуется совместимость с Python 2.5, может быть полезно следующее. Я основал это на предыдущем ответе.

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)

С рекурсией!

def make_hashable(h):
    items = h.items()
    for item in items:
        if type(items) == dict:
            item = make_hashable(item)
    return frozenset(items)

Вы можете добавить другие типы тестов для любых других изменяемых типов, которые вы хотите сделать хэшируемыми. Это не должно быть сложно.

Другие вопросы по тегам