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.namedtuple
tem 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 namedtuple
s é 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 dict
s que podem ser usados como chaves para outros dict
s?
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()
hashdict
deve ser imutável, pelo menos depois de iniciar o hash, então por que não armazenar em cache os valoreskey
ehash
como atributos dohashdict
objeto? 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.pyRespostas:
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.
fonte
O(n*log(n))
onden
está o número dedict
entradas. Alguém sabe se afrozenset
função hash do Python é executada em tempo linear?dict(key1=value1, key2=value2,...)
ou assimdict([(key1, value1), (key2, value2),...)])
. O mesmo se aplica a este. A criação que você postou é chamada de literalhashabledict({key_a: val_a, key_b: val_b, ...})
.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:
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! -)
fonte
sorted
tecla __key ;-).Tudo o que é necessário para tornar os dicionários utilizáveis para sua finalidade é adicionar um método __hash__:
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 é:
Isso é mais rápido do que
frozenset(self.iteritems())
por duas razões. Primeiro, afrozenset(self)
etapa reutiliza os valores de hash armazenados no dicionário, salvando chamadas desnecessárias parahash(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.fonte
dict
ele mesmo não armazena em cache os valores de hash de suas chaves - embora classes individuais (comostr
) possam escolher armazenar em cache seus hashes. Pelo menos quando criei umdict
com 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 comohash(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).{'one': 1, 'two': 2}
e{'one': 2, 'two': 1}
__missing__
é uma má ideia. O que você acha?As respostas fornecidas estão corretas, mas podem ser melhoradas usando em
frozenset(...)
vez detuple(sorted(...))
para gerar o hash: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).fonte
hash(frozenset(d))
.hash(frozenset(d))
resulta em hashes idênticos para 2 dictos com chaves semelhantes, mas valores diferentes!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 issofrozenset(d)
é tão rápido).Uma implementação razoavelmente limpa e direta é
fonte
__iter__
e__len__
.__iter__
e__len__
porque preciso, já que estou derivandocollections.Mapping
; como usarcollections.Mapping
é abordado muito bem na documentação do módulo de coleções. Outras pessoas não sentem necessidade, pois estão derivandodict
. Derivardict
por 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.Eu sempre volto a este tópico ... Aqui está outra variação. Não estou confortável em
dict
criar 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. emboratuple
seja 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.
Isso por si só o coloca à distância de um mapeamento imutável:
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.
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:
O que resulta no menos problemático:
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 decollections.Mapping
, mas a maior parte do trabalho é substituir osfrozenset
métodos para versões que funcionam como dictes, em vez disso:que, em última análise, responde à minha própria pergunta:
fonte
A resposta aceita por @Unknown, bem como a resposta por @AlexMartelli funcionam perfeitamente, mas apenas sob as seguintes restrições:
hash(hashabledict({'a':[1,2]}))
aumentaráTypeError
.hash(hashabledict({'a':'a', 1:1}))
aumentaráTypeError
.frozenset((1,2,3))
efrozenset((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:.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 deO(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.
fonte
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.
fonte
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.
fonte
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.