Пользовательский диктант с хешируемыми ключами не может обрабатывать рекурсивные структуры

1

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

  1. Проверка того, был ли элемент уже просмотрен в O (1) при перемещении списка

  2. Сопоставление каждого элемента с идентификатором, например, символом

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

пример

d = MutableKeyDict()

d[[1, 2, 3]] = 'a'

print([1, 2, 3] in d)  # True
print((1, 2, 3) in d)  # False

Реализация

tl; dr я реализовал что-то, что не работает. Если вы видите канонический способ реализации этого, пропустите эту часть.

На данный момент я написал класс-оболочку, который реализует метод __hash__ который возвращается к неизменяемому типу, эквивалентному хэш-данным.

class ForcedHashable:
    @staticmethod
    def hashable(obj):
        try:
            hash(obj)
            return obj
        except TypeError:
            if isinstance(obj, (list, tuple)):
                return tuple(ForcedHashable.hashable(o) for o in obj)
            elif isinstance(obj, set):
                return frozenset(ForcedHashable(o) for o in obj)
            elif isinstance(obj, dict):
                return tuple((k, ForcedHashable.hashable(v)) for k, v in obj.items())
            ...

    def __init__(self, data):
        self.data = data

    def __eq__(self, other):
        return self.data == other.data

    def __hash__(self):
        return hash(self.hashable(self.data))

Это позволило мне написать проект пользовательского класса dict, который использует ForcedHashable для ForcedHashable своих ключей.

class MutableKeyDict(UserDict):
    def __setitem__(self, key, value):
        self.data[ForcedHashable(key)] = value

    def __getitem__(self, item):
        return self.data[ForcedHashable(item)]

    def __contains__(self, item):
        return ForcedHashable(item) in self.data

Он работает для основных случаев...

d = MutableKeyDict()

d[[1, 2, 3]] = 'a'

print([1, 2, 3] in d)  # True
print((1, 2, 3) in d)  # False

Но сталкивается с некоторыми проблемами с объектами, вложенными в себя.

d = MutableKeyDict()

x = []
x.append(x)

d[x] = 'foo' # raises a 'RecursionError: maximum recursion depth exceeded'

Разумеется, рекурсия исходит из этого утверждения:

if isinstance(obj, (list, tuple)):
    return tuple(ForcedHashable.hashable(o) for o in obj)

Я был на полпути через внедрение исправления с memo, вроде как тот, copy.deepcopy используется copy.deepcopy, но потом я понял, что даже если я это сделаю, этот метод также вызовет RecursionError.

def __eq__(self, other):
    return self.data == other.data

Вопрос

Я бы хотел, чтобы вышеупомянутое работало, по крайней мере, для встроенных типов.

Будет ли умный способ обойти этот RecursionError? А если нет, существует ли канонический способ связать равные элементы (только встроенные типы) с временным хэшем? Другие подходы более чем приветствуются.

Теги:
python-3.x
dictionary
hash
hashmap

1 ответ

2
Лучший ответ

Нет причин, по deepcopy техника deepcopy не будет работать для решения проблемы рекурсии.

Я думаю, что вам может не deepcopy это то, что deepcopy основано на id значений. Вам нужно только ловить объекты, которые содержат себя, идентично, а не объекты, которые содержат одинаковые, но разные объекты. В конце концов, вы не можете иметь бесконечную глубину различных, но равных объектов; это потребует бесконечной памяти.

На самом деле, вы можете сделать это проще, чем deepcopy и pickle, потому что на самом деле не имеет значения, что вы возвращаете для повторяющегося объекта, если только он hashable и уникальный. 1

Так, например:

def hashable(obj, *, memo=None):
    if memo is None:
        memo = set()
    if id(obj) in memo:
        return (..., id(obj))
    memo.add(id(obj))
    try:
        hash(obj)
        return obj
    except TypeError:
        if isinstance(obj, (list, tuple)):
            return tuple(ForcedHashable.hashable(o, memo=memo) for o in obj)
        elif isinstance(obj, set):
            return frozenset(ForcedHashable(o, memo=memo) for o in obj)
        elif isinstance(obj, dict):
            return frozenset((k, ForcedHashable.hashable(v, memo=memo)) for k, v in obj.items())
        raise

И сейчас:

>>> x = []
>>> x.append(x)
>>> ForcedHashable.hashable(x)
((Ellipsis, 4658316360),)
>>> d = MutableKeyDict()
>>> d[x] = d
>>> d[x]
{<__main__.ForcedHashable object at 0x115855240>: 2, <__main__.ForcedHashable object at 0x115a247f0>: {...}}

Пока мы это делаем, сделайте следующее:

elif isinstance(obj, (dict, MutableKeyDict)):
    return frozenset((k, ForcedHashable.hashable(v, memo=memo)) for k, v in obj.items())

… и сейчас:

>>> d = MutableKeyDict()
>>> d[d] = d
>>> d
{<__main__.ForcedHashable object at 0x11584b320>: {...}}

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню