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

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

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

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

Пример:

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

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

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

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

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

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

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

Ответ 1

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

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)

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

Чтобы проиллюстрировать:

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

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

Ответ 5

С рекурсией!

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

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

Ответ 6

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

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

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

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

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

Ответ 7

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

Для тех, кто требует совместимости с 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)

Ответ 8

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

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

import pickle
hashable_str = pickle.dumps(unhashable_object)