Python hashable dicts

92

Como exercício, e principalmente para minha própria diversão, estou implementando um analisador packrat retroativo. A inspiração para isso é que eu gostaria de ter uma ideia melhor sobre como as macros higiênicas funcionariam em uma linguagem semelhante ao algol (em comparação com os dialetos lisp livres de sintaxe em que você normalmente os encontra). Por causa disso, passagens diferentes pela entrada podem ver gramáticas diferentes, portanto, os resultados da análise em cache são inválidos, a menos que eu também armazene a versão atual da gramática junto com os resultados da análise em cache. ( EDITAR : uma consequência desse uso de coleções de valores-chave é que elas devem ser imutáveis, mas não pretendo expor a interface para permitir que sejam alteradas, portanto, coleções mutáveis ​​ou imutáveis ​​são adequadas)

O problema é que dicts python não podem aparecer como chaves para outros dicts. Mesmo usar uma tupla (como eu faria de qualquer maneira) não ajuda.

>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> 

Eu acho que tem que ser tuplas até o fim. Agora, a biblioteca padrão do python fornece aproximadamente o que eu preciso, collections.namedtupletem uma sintaxe muito diferente, mas pode ser usada como uma chave. continuando da sessão acima:

>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}

Está bem. Mas eu tenho que fazer uma classe para cada combinação possível de chaves na regra que eu gostaria de usar, o que não é tão ruim, porque cada regra de análise sabe exatamente quais parâmetros ela usa, para que essa classe possa ser definida ao mesmo tempo como a função que analisa a regra.

Edit: Um problema adicional com namedtuples é que eles são estritamente posicionais. Duas tuplas que parecem que deveriam ser diferentes podem na verdade ser as mesmas:

>>> you = namedtuple("foo",["bar","baz"])
>>> me = namedtuple("foo",["bar","quux"])
>>> you(bar=1,baz=2) == me(bar=1,quux=2)
True
>>> bob = namedtuple("foo",["baz","bar"])
>>> you(bar=1,baz=2) == bob(bar=1,baz=2)
False

tl'dr: Como obtenho dicts que podem ser usados ​​como chaves para outros dicts?

Tendo hackeado um pouco as respostas, aqui está a solução mais completa que estou usando. Observe que isso faz um trabalho um pouco extra para tornar os dictos resultantes vagamente imutáveis ​​para fins práticos. Claro que ainda é muito fácil contornar isso ligando, dict.__setitem__(instance, key, value)mas somos todos adultos aqui.

class hashdict(dict):
    """
    hashable dict implementation, suitable for use as a key into
    other dicts.

        >>> h1 = hashdict({"apples": 1, "bananas":2})
        >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
        >>> h1+h2
        hashdict(apples=1, bananas=3, mangoes=5)
        >>> d1 = {}
        >>> d1[h1] = "salad"
        >>> d1[h1]
        'salad'
        >>> d1[h2]
        Traceback (most recent call last):
        ...
        KeyError: hashdict(bananas=3, mangoes=5)

    based on answers from
       http://stackoverflow.com/questions/1151658/python-hashable-dicts

    """
    def __key(self):
        return tuple(sorted(self.items()))
    def __repr__(self):
        return "{0}({1})".format(self.__class__.__name__,
            ", ".join("{0}={1}".format(
                    str(i[0]),repr(i[1])) for i in self.__key()))

    def __hash__(self):
        return hash(self.__key())
    def __setitem__(self, key, value):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def __delitem__(self, key):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def clear(self):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def pop(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def popitem(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def setdefault(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def update(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    # update is not ok because it mutates the object
    # __add__ is ok because it creates a new object
    # while the new object is under construction, it's ok to mutate it
    def __add__(self, right):
        result = hashdict(self)
        dict.update(result, right)
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()
SingleNegationElimination
fonte
O hashdictdeve ser imutável, pelo menos depois de iniciar o hash, então por que não armazenar em cache os valores keye hashcomo atributos do hashdictobjeto? Modifiquei __key()e __hash__()e testei para confirmar que é muito mais rápido. O SO não permite código formatado nos comentários, então vou vinculá-lo aqui: sam.aiki.info/hashdict.py
Sam Watkins

Respostas:

68

Esta é a maneira fácil de fazer um dicionário hash. Apenas lembre-se de não transformá-los após incorporá-los em outro dicionário por razões óbvias.

class hashabledict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.items())))
Desconhecido
fonte
7
Isso não garante nitidamente a consistência de eq e hash, enquanto minha resposta anterior faz através do uso do método __key (na prática, qualquer uma das abordagens deve funcionar, embora esta possa ser retardada ao fazer uma lista itermediate desnecessária - corrigível por s / items / iteritems / - assumindo Python 2. * como você não disse ;-).
Alex Martelli
5
Provavelmente seria melhor usar apenas um Frozenset em vez de uma tupla com classificação. Isso não apenas seria mais rápido, mas você não pode assumir que as chaves de dicionário são comparáveis.
asmeurer em
1
Parece que deve haver uma maneira de evitar uma função hash O(n*log(n))onde nestá o número de dictentradas. Alguém sabe se a frozensetfunção hash do Python é executada em tempo linear?
Tom Karzes
2
@HelloGoodbye Um dicionário também pode ser criado assim dict(key1=value1, key2=value2,...)ou assim dict([(key1, value1), (key2, value2),...)]). O mesmo se aplica a este. A criação que você postou é chamada de literal
smido
2
@smido: Obrigado. Eu também achei que você pode simplesmente lançar um literal, ou seja hashabledict({key_a: val_a, key_b: val_b, ...}).
HelloGoodbye,
62

Os hashables devem ser imutáveis ​​- não reforçando isso, mas CONFIANDO que você não modifique um dicionário após seu primeiro uso como uma chave, a seguinte abordagem funcionaria:

class hashabledict(dict):
  def __key(self):
    return tuple((k,self[k]) for k in sorted(self))
  def __hash__(self):
    return hash(self.__key())
  def __eq__(self, other):
    return self.__key() == other.__key()

Se você precisa MUDAR seus dictos e AINDA quer usá-los como chaves, a complexidade explode centenas de vezes - para não dizer que não pode ser feito, mas vou esperar até uma indicação MUITO específica antes de entrar naquele pântano incrível! -)

Alex Martelli
fonte
Certamente não quero transformar os dictos depois de preparados. Isso faria com que o resto do algoritmo de packrad desmoronasse.
SingleNegationElimination
Então a subclasse que sugeri funcionará - observe como ela contorna o problema "posicional" ( antes de você editar sua pergunta para apontá-la ;-) com a sortedtecla __key ;-).
Alex Martelli
O comportamento dependente da posição de namedtuple me surpreendeu profundamente. Eu estava brincando com isso, pensando que ainda poderia ser uma maneira mais fácil de resolver o problema, mas isso praticamente acabou com todas as minhas esperanças (e exigirá uma reversão :()
SingleNegationElimination
Digamos que eu tenha um ditado e desejo convertê-lo em um hashabled. Como eu faria isso?
Jononomo
@JonCrowell veja estas perguntas para ideias e esclarecimentos: stackoverflow.com/questions/3464061/… , stackoverflow.com/questions/9112300/… , stackoverflow.com/questions/18020074/…
máximo de
32

Tudo o que é necessário para tornar os dicionários utilizáveis ​​para sua finalidade é adicionar um método __hash__:

class Hashabledict(dict):
    def __hash__(self):
        return hash(frozenset(self))

Observe, a conversão do Frozenset funcionará para todos os dicionários (ou seja, não requer que as chaves sejam classificáveis). Da mesma forma, não há restrição aos valores do dicionário.

Se houver muitos dicionários com chaves idênticas, mas com valores distintos, é necessário que o hash leve os valores em consideração. A maneira mais rápida de fazer isso é:

class Hashabledict(dict):
    def __hash__(self):
        return hash((frozenset(self), frozenset(self.itervalues())))

Isso é mais rápido do que frozenset(self.iteritems())por duas razões. Primeiro, a frozenset(self)etapa reutiliza os valores de hash armazenados no dicionário, salvando chamadas desnecessárias para hash(key). Em segundo lugar, usar itervalues acessará os valores diretamente e evitará as muitas chamadas de alocador de memória usando por itens para formar novas muitas tuplas de chave / valor na memória toda vez que você fizer uma pesquisa.

Raymond Hettinger
fonte
@RaymondHettinger Corrija-me se eu estiver errado, mas eu pensei que dictele mesmo não armazena em cache os valores de hash de suas chaves - embora classes individuais (como str) possam escolher armazenar em cache seus hashes. Pelo menos quando criei um dictcom minhas instâncias de classe personalizadas usadas como chaves, seus __hash__métodos foram chamados em todas as operações de acesso (python 3.4). Esteja ou não correto, não tenho certeza de como hash(frozenset(self))posso reutilizar os valores de hash pré-calculados, a menos que eles estejam armazenados em cache dentro das próprias chaves (nesse caso, hash(frozenset(self.items())reutilize-os também).
máx.
Quanto ao seu segundo ponto sobre a criação da tupla (chave / valor), pensei que os métodos .items () retornam uma visão em vez de uma lista de tuplas, e que a criação dessa visão não envolve a cópia das chaves e valores subjacentes. (Python 3.4 novamente.) Dito isso, vejo a vantagem de fazer hash apenas das chaves se a maioria das entradas tiver chaves diferentes - porque (1) é muito caro fazer hash de valores e (2) é bastante restritivo exigir que os valores sejam hash
máx.
6
Isso também tem a possibilidade de criar o mesmo hash para dois dicionários diferentes. Considere {'one': 1, 'two': 2}e{'one': 2, 'two': 1}
AgDude
Mike Graham em seu comentário afirma que Derivar dict por qualquer outro motivo, exceto para definir, __missing__é uma má ideia. O que você acha?
Piotr Dobrogost de
1
A subclasse de dict foi bem definida desde Python 2.2. Consulte Collections.OrderedDict e Collections.Counter para exemplos da biblioteca padrão do Python. O outro comentário é baseado na crença infundada de que apenas as subclasses de MutableMapping são bem definidas.
Raymond Hettinger
23

As respostas fornecidas estão corretas, mas podem ser melhoradas usando em frozenset(...)vez de tuple(sorted(...))para gerar o hash:

>>> import timeit
>>> timeit.timeit('hash(tuple(sorted(d.iteritems())))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
4.7758948802947998
>>> timeit.timeit('hash(frozenset(d.iteritems()))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
1.8153600692749023

A vantagem de desempenho depende do conteúdo do dicionário, mas na maioria dos casos que testei, o hash com frozenseté pelo menos 2 vezes mais rápido (principalmente porque não precisa classificar).

Oben Sonne
fonte
1
Observe que não há necessidade de incluir as chaves ee os valores. Esta solução seria muito mais rápido como: hash(frozenset(d)).
Raymond Hettinger
10
@RaymondHettinger: hash(frozenset(d))resulta em hashes idênticos para 2 dictos com chaves semelhantes, mas valores diferentes!
Oben Sonne
4
Isso não é problema. É função de __eq__ distinguir entre dictos de valores diferentes. O trabalho de __hash__ é meramente reduzir o espaço de busca.
Raymond Hettinger
5
Isso é verdade para o conceito teórico de hashes e mapeamentos, mas não é prático para caches com dicionários como pesquisas - não é incomum que dicionários com chaves semelhantes, mas com valores diferentes, sejam passados ​​para uma função armazenada em cache de memória. Nesse caso, o cache praticamente se transforma em uma lista em vez de um mapeamento se apenas as chaves forem usadas para construir um hash.
Oben Sonne
3
No caso especial de dicts com chaves idênticas e valores distintos, seria melhor armazenar apenas um hash baseado em frozenset(d.itervalues()). Nos casos em que os dicts têm chaves distintas, frozenset(d)é muito mais rápido e não impõe restrições à capacidade de hash das chaves. Por último, lembre-se de que o método dict .__ eq__ verificará se há pares de chave / valor iguais com muito mais rapidez que qualquer coisa pode calcular o hash para todas as tuplas de pares de chave / valor. Usar tuplas de chave / valor também é problemático porque joga fora os hashes armazenados para todas as chaves (por isso frozenset(d)é tão rápido).
Raymond Hettinger
11

Uma implementação razoavelmente limpa e direta é

import collections

class FrozenDict(collections.Mapping):
    """Don't forget the docstrings!!"""

    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __getitem__(self, key):
        return self._d[key]

    def __hash__(self):
        return hash(tuple(sorted(self._d.iteritems())))
Mike Graham
fonte
Por que é tão razoável, limpo e direto? Ou seja, explique as diferenças para outras respostas, por exemplo, a necessidade de __iter__e __len__.
Karl Richter
1
@KarlRichter, eu nunca disse que era razoável, apenas razoavelmente limpo. ;)
Mike Graham
@KarlRichter, eu defino __iter__e __len__porque preciso, já que estou derivando collections.Mapping; como usar collections.Mappingé abordado muito bem na documentação do módulo de coleções. Outras pessoas não sentem necessidade, pois estão derivando dict. Derivar dictpor qualquer outro motivo que não seja definir __missing__é uma má ideia. A especificação de dict não diz como o dict funciona em tal caso e, na realidade, isso acabará tendo toneladas de métodos não virtuais que são menos úteis em geral e, neste caso particular, terá métodos vestigiais com comportamento irrelevante.
Mike Graham
7

Eu sempre volto a este tópico ... Aqui está outra variação. Não estou confortável em dictcriar subclasses para adicionar um __hash__método; Praticamente não há como escapar do problema de que os dicts são mutáveis ​​e confiar que eles não mudarão parece uma ideia fraca. Em vez disso, observei a construção de um mapeamento baseado em um tipo embutido que é imutável. embora tupleseja uma escolha óbvia, acessar valores nela implica uma espécie e uma bissetriz; não é um problema, mas não parece estar aproveitando muito o poder do tipo em que é construído.

E se você transformar pares de chave e valor em um frozenset? O que isso exigiria, como funcionaria?

Parte 1, você precisa de uma maneira de codificar os itens de tal forma que um conjunto de congelamento os trate principalmente por suas chaves; Vou fazer uma pequena subclasse para isso.

import collections
class pair(collections.namedtuple('pair_base', 'key value')):
    def __hash__(self):
        return hash((self.key, None))
    def __eq__(self, other):
        if type(self) != type(other):
            return NotImplemented
        return self.key == other.key
    def __repr__(self):
        return repr((self.key, self.value))

Isso por si só o coloca à distância de um mapeamento imutável:

>>> frozenset(pair(k, v) for k, v in enumerate('abcd'))
frozenset([(0, 'a'), (2, 'c'), (1, 'b'), (3, 'd')])
>>> pairs = frozenset(pair(k, v) for k, v in enumerate('abcd'))
>>> pair(2, None) in pairs
True
>>> pair(5, None) in pairs
False
>>> goal = frozenset((pair(2, None),))
>>> pairs & goal
frozenset([(2, None)])

D'oh! Infelizmente, quando você usa os operadores de conjunto, os elementos são iguais, mas não são o mesmo objeto; qual deles acaba no valor de retorno é indefinido , teremos que passar por mais alguns giros.

>>> pairs - (pairs - goal)
frozenset([(2, 'c')])
>>> iter(pairs - (pairs - goal)).next().value
'c'

No entanto, examinar os valores dessa forma é complicado e, pior, cria muitos conjuntos intermediários; isso não vai dar certo! Criaremos um par de valores-chave 'falso' para contorná-lo:

class Thief(object):
    def __init__(self, key):
        self.key = key
    def __hash__(self):
        return hash(pair(self.key, None))
    def __eq__(self, other):
        self.value = other.value
        return pair(self.key, None) == other

O que resulta no menos problemático:

>>> thief = Thief(2)
>>> thief in pairs
True
>>> thief.value
'c'

Essa é toda a magia profunda; o resto é envolver tudo em algo que tenha uma interface como um dicionário. Como estamos criando uma subclasse de frozenset, que tem uma interface muito diferente, há muitos métodos; recebemos uma pequena ajuda de collections.Mapping, mas a maior parte do trabalho é substituir os frozensetmétodos para versões que funcionam como dictes, em vez disso:

class FrozenDict(frozenset, collections.Mapping):
    def __new__(cls, seq=()):
        return frozenset.__new__(cls, (pair(k, v) for k, v in seq))
    def __getitem__(self, key):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        raise KeyError(key)
    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            return dict(self.iteritems()) == other
        if len(self) != len(other):
            return False
        for key, value in self.iteritems():
            try:
                if value != other[key]:
                    return False
            except KeyError:
                return False
        return True
    def __hash__(self):
        return hash(frozenset(self.iteritems()))
    def get(self, key, default=None):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        return default
    def __iter__(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def iteritems(self):
        for item in frozenset.__iter__(self):
            yield (item.key, item.value)
    def iterkeys(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def itervalues(self):
        for item in frozenset.__iter__(self):
            yield item.value
    def __contains__(self, key):
        return frozenset.__contains__(self, pair(key, None))
    has_key = __contains__
    def __repr__(self):
        return type(self).__name__ + (', '.join(repr(item) for item in self.iteritems())).join('()')
    @classmethod
    def fromkeys(cls, keys, value=None):
        return cls((key, value) for key in keys)

que, em última análise, responde à minha própria pergunta:

>>> myDict = {}
>>> myDict[FrozenDict(enumerate('ab'))] = 5
>>> FrozenDict(enumerate('ab')) in myDict
True
>>> FrozenDict(enumerate('bc')) in myDict
False
>>> FrozenDict(enumerate('ab', 3)) in myDict
False
>>> myDict[FrozenDict(enumerate('ab'))]
5
SingleNegationElimination
fonte
5

A resposta aceita por @Unknown, bem como a resposta por @AlexMartelli funcionam perfeitamente, mas apenas sob as seguintes restrições:

  1. Os valores do dicionário devem ser hashable. Por exemplo, hash(hashabledict({'a':[1,2]}))aumentará TypeError.
  2. As chaves devem oferecer suporte à operação de comparação. Por exemplo, hash(hashabledict({'a':'a', 1:1}))aumentará TypeError.
  3. O operador de comparação nas chaves impõe a ordem total. Por exemplo, se as duas chaves em um dicionário são frozenset((1,2,3))e frozenset((4,5,6)), elas se comparam desiguais em ambas as direções. Portanto, classificar os itens de um dicionário com essas chaves pode resultar em uma ordem arbitrária e, portanto, violará a regra de que objetos iguais devem ter o mesmo valor hash.

A resposta muito mais rápida de @ObenSonne elimina as restrições 2 e 3, mas ainda está limitada pela restrição 1 (os valores devem ser hashable).

A resposta mais rápida, porém, de @RaymondHettinger remove todas as 3 restrições porque não inclui .values()no cálculo de hash. No entanto, seu desempenho é bom apenas se:

  1. A maioria dos dicionários (não iguais) que precisam ser hash não são idênticos .keys().

Se esta condição não for satisfeita, a função hash ainda será válida, mas pode causar muitas colisões. Por exemplo, no caso extremo em que todos os dicionários são gerados a partir de um modelo de site (nomes de campo como chaves, entrada do usuário como valores), as chaves serão sempre as mesmas e a função hash retornará o mesmo valor para todas as entradas . Como resultado, uma tabela de hash que depende dessa função hash se tornará tão lenta quanto uma lista ao recuperar um item (em O(N)vez de O(1)).

Acho que a solução a seguir funcionará razoavelmente bem, mesmo se todas as 4 restrições que listei acima forem violadas. Ele tem uma vantagem adicional de fazer hash não apenas de dicionários, mas de quaisquer contêineres, mesmo que eles tenham contêineres mutáveis ​​aninhados.

Agradeço muito qualquer feedback sobre isso, já que testei apenas levemente até agora.

# python 3.4
import collections
import operator
import sys
import itertools
import reprlib

# a wrapper to make an object hashable, while preserving equality
class AutoHash:
    # for each known container type, we can optionally provide a tuple
    # specifying: type, transform, aggregator
    # even immutable types need to be included, since their items
    # may make them unhashable

    # transformation may be used to enforce the desired iteration
    # the result of a transformation must be an iterable
    # default: no change; for dictionaries, we use .items() to see values

    # usually transformation choice only affects efficiency, not correctness

    # aggregator is the function that combines all items into one object
    # default: frozenset; for ordered containers, we can use tuple

    # aggregator choice affects both efficiency and correctness
    # e.g., using a tuple aggregator for a set is incorrect,
    # since identical sets may end up with different hash values
    # frozenset is safe since at worst it just causes more collisions
    # unfortunately, no collections.ABC class is available that helps
    # distinguish ordered from unordered containers
    # so we need to just list them out manually as needed

    type_info = collections.namedtuple(
        'type_info',
        'type transformation aggregator')

    ident = lambda x: x
    # order matters; first match is used to handle a datatype
    known_types = (
        # dict also handles defaultdict
        type_info(dict, lambda d: d.items(), frozenset), 
        # no need to include set and frozenset, since they are fine with defaults
        type_info(collections.OrderedDict, ident, tuple),
        type_info(list, ident, tuple),
        type_info(tuple, ident, tuple),
        type_info(collections.deque, ident, tuple),
        type_info(collections.Iterable, ident, frozenset) # other iterables
    )

    # hash_func can be set to replace the built-in hash function
    # cache can be turned on; if it is, cycles will be detected,
    # otherwise cycles in a data structure will cause failure
    def __init__(self, data, hash_func=hash, cache=False, verbose=False):
        self._data=data
        self.hash_func=hash_func
        self.verbose=verbose
        self.cache=cache
        # cache objects' hashes for performance and to deal with cycles
        if self.cache:
            self.seen={}

    def hash_ex(self, o):
        # note: isinstance(o, Hashable) won't check inner types
        try:
            if self.verbose:
                print(type(o),
                    reprlib.repr(o),
                    self.hash_func(o),
                    file=sys.stderr)
            return self.hash_func(o)
        except TypeError:
            pass

        # we let built-in hash decide if the hash value is worth caching
        # so we don't cache the built-in hash results
        if self.cache and id(o) in self.seen:
            return self.seen[id(o)][0] # found in cache

        # check if o can be handled by decomposing it into components
        for typ, transformation, aggregator in AutoHash.known_types:
            if isinstance(o, typ):
                # another option is:
                # result = reduce(operator.xor, map(_hash_ex, handler(o)))
                # but collisions are more likely with xor than with frozenset
                # e.g. hash_ex([1,2,3,4])==0 with xor

                try:
                    # try to frozenset the actual components, it's faster
                    h = self.hash_func(aggregator(transformation(o)))
                except TypeError:
                    # components not hashable with built-in;
                    # apply our extended hash function to them
                    h = self.hash_func(aggregator(map(self.hash_ex, transformation(o))))
                if self.cache:
                    # storing the object too, otherwise memory location will be reused
                    self.seen[id(o)] = (h, o)
                if self.verbose:
                    print(type(o), reprlib.repr(o), h, file=sys.stderr)
                return h

        raise TypeError('Object {} of type {} not hashable'.format(repr(o), type(o)))

    def __hash__(self):
        return self.hash_ex(self._data)

    def __eq__(self, other):
        # short circuit to save time
        if self is other:
            return True

        # 1) type(self) a proper subclass of type(other) => self.__eq__ will be called first
        # 2) any other situation => lhs.__eq__ will be called first

        # case 1. one side is a subclass of the other, and AutoHash.__eq__ is not overridden in either
        # => the subclass instance's __eq__ is called first, and we should compare self._data and other._data
        # case 2. neither side is a subclass of the other; self is lhs
        # => we can't compare to another type; we should let the other side decide what to do, return NotImplemented
        # case 3. neither side is a subclass of the other; self is rhs
        # => we can't compare to another type, and the other side already tried and failed;
        # we should return False, but NotImplemented will have the same effect
        # any other case: we won't reach the __eq__ code in this class, no need to worry about it

        if isinstance(self, type(other)): # identifies case 1
            return self._data == other._data
        else: # identifies cases 2 and 3
            return NotImplemented

d1 = {'a':[1,2], 2:{3:4}}
print(hash(AutoHash(d1, cache=True, verbose=True)))

d = AutoHash(dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars'),cache=True, verbose=True)
print(hash(d))
máx.
fonte
2

Você também pode adicionar esses dois métodos para fazer o protocolo de decapagem v2 funcionar com instâncias hashdict. Caso contrário, o cPickle tentará usar hashdict .____ setitem____ resultando em um TypeError. Curiosamente, com as outras duas versões do protocolo, seu código funciona perfeitamente.

def __setstate__(self, objstate):
    for k,v in objstate.items():
        dict.__setitem__(self,k,v)
def __reduce__(self):
    return (hashdict, (), dict(self),)
Giovanni
fonte
-2

Se você não colocar números no dicionário e nunca perder as variáveis ​​que contêm seus dicionários, você pode fazer isso:

cache[id(rule)] = "whatever"

já que id () é único para cada dicionário

EDITAR:

Oh, desculpe, sim, nesse caso o que os outros caras disseram seria melhor. Acho que você também pode serializar seus dicionários como uma string, como

cache[ 'foo:bar' ] = 'baz'

Se você precisar recuperar seus dicionários das chaves, então você terá que fazer algo mais feio como

cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )

Acho que a vantagem disso é que você não teria que escrever tanto código.

Deepthroat
fonte
Hmmm, não; não é isso que estou procurando cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache:, Ser capaz de criar chaves dinamicamente é importante para quando eu quiser usar dicts como chaves em primeiro lugar.
SingleNegationElimination
1
Pode ser que não haja problema em serializar os dictos. Você tem alguma recomendação de como serializá-los? é isso que estou procurando.
SingleNegationElimination