Qual é a maneira correta e boa de implementar __hash__()
?
Eu estou falando sobre a função que retorna um código hash que é usado para inserir objetos em tabelas de hash aka dicionários.
Como __hash__()
retorna um número inteiro e é usado para "agrupar" objetos em hashtables, presumo que os valores do número inteiro retornado sejam distribuídos uniformemente para dados comuns (para minimizar colisões). Qual é uma boa prática para obter esses valores? As colisões são um problema? No meu caso, eu tenho uma classe pequena que atua como uma classe de contêiner contendo algumas entradas, algumas flutuações e uma string.
fonte
__key
função, isso é tão rápido quanto qualquer hash pode ser. Claro, se se sabe que os atributos são números inteiros e não há muitos, suponho que você possa executar um pouco mais rápido com um hash de rolagem em casa, mas provavelmente não seria tão bem distribuído.hash((self.attr_a, self.attr_b, self.attr_c))
será surpreendentemente rápido (e correto ), pois a criação de pequenostuple
s é especialmente otimizada, e empurra o trabalho de obter e combinar hashes para os C embutidos, o que normalmente é mais rápido que o código no nível Python.None
uma vez que a chave muda. A maneira como resolvi foi armazenando o ID do objeto como uma chave em vez de apenas o objeto.John Millikin propôs uma solução semelhante a esta:
O problema com esta solução é que o
hash(A(a, b, c)) == hash((a, b, c))
. Em outras palavras, o hash colide com o da tupla de seus principais membros. Talvez isso não importe com muita frequência na prática?Atualização: os documentos do Python agora recomendam o uso de uma tupla, como no exemplo acima. Observe que a documentação declara
Note que o oposto não é verdadeiro. Objetos que não comparam iguais podem ter o mesmo valor de hash. Essa colisão de hash não fará com que um objeto substitua outro quando usado como uma chave de ditado ou elemento de conjunto , desde que os objetos também não sejam comparáveis .
Solução desatualizada / incorreta
A documentação do Python, o que nos dá o seguinte:__hash__
sugere que você combine os hashes dos subcomponentes usando algo como o XORAtualização: como Blckknght aponta, alterar a ordem de a, bec pode causar problemas. Eu adicionei um adicional
^ hash((self._a, self._b, self._c))
para capturar a ordem dos valores que estão sendo hash. Esta final^ hash(...)
pode ser removida se os valores combinados não puderem ser reorganizados (por exemplo, se eles tiverem tipos diferentes e, portanto, o valor de_a
nunca será atribuído a_b
ou_c
, etc.).fonte
hash(A(1, 2, 3))
será igual ahash(A(3, 1, 2))
(e eles vão tanto de hash igual a qualquer outraA
instância com uma permutação de1
,2
e3
como seus valores). Se você deseja evitar que sua instância tenha o mesmo hash que uma tupla de seus argumentos, simplesmente crie um valor sentinela (como uma variável de classe ou como global) e inclua-o na tupla a ser hash: return hash ((_ sentinel , self._a, self._b, self._c))isinstance
pode ser problemático, pois um objeto de uma subclasse detype(self)
agora pode ser igual a um objeto detype(self)
. Então você pode achar que a adição de umCar
e umFord
a umset()
podem resultar em apenas um objeto inserido, dependendo da ordem de inserção. Além disso, você pode ter uma situação em quea == b
é verdadeiro, masb == a
é falso.B
, pode mudar isso paraisinstance(othr, B)
hash((type(self), self._a, self._b, self._c))
.B
vez detype(self)
, também é considerado uma prática recomendada retornarNotImplemented
ao encontrar um tipo inesperado em__eq__
vez deFalse
. Isso permite que outros tipos definidos pelo usuário implementem um__eq__
que conheçaB
e possa comparar o mesmo, se desejar.Paul Larson, da Microsoft Research, estudou uma ampla variedade de funções de hash. Ele me disse aquilo
funcionou surpreendentemente bem para uma ampla variedade de strings. Descobri que técnicas polinomiais semelhantes funcionam bem para calcular um hash de subcampos díspares.
fonte
(hash * 1000003) XOR ord(c)
para strings com multiplicação envolvente de 32 bits. [Citação ]__hash__
método; não precisamos rolar os nossos. A questão é como implementar__hash__
para uma classe típica definida pelo usuário (com várias propriedades apontando para tipos internos ou talvez para outras classes definidas pelo usuário), que essa resposta não aborda de maneira alguma.Eu posso tentar responder a segunda parte da sua pergunta.
As colisões provavelmente resultarão não do próprio código de hash, mas do mapeamento do código de hash para um índice em uma coleção. Por exemplo, sua função hash pode retornar valores aleatórios de 1 a 10000, mas se sua tabela hash tiver apenas 32 entradas, você terá colisões na inserção.
Além disso, eu pensaria que as colisões seriam resolvidas internamente pela coleção, e existem muitos métodos para resolver colisões. O mais simples (e o pior) é, dada uma entrada para inserir no índice i, adicione 1 a i até encontrar um local vazio e insira nele. A recuperação funciona da mesma maneira. Isso resulta em recuperações ineficientes para algumas entradas, pois você pode ter uma entrada que requer percorrer toda a coleção para encontrar!
Outros métodos de resolução de colisão reduzem o tempo de recuperação movendo entradas na tabela de hash quando um item é inserido para espalhar as coisas. Isso aumenta o tempo de inserção, mas pressupõe que você leia mais do que insere. Também existem métodos que tentam ramificar diferentes entradas em colisão para que elas sejam agrupadas em um ponto específico.
Além disso, se você precisar redimensionar a coleção, precisará refazer tudo ou usar um método de hash dinâmico.
Em resumo, dependendo do que você estiver usando o código hash, talvez seja necessário implementar seu próprio método de resolução de colisão. Se você não os estiver armazenando em uma coleção, provavelmente poderá usar uma função de hash que apenas gera códigos de hash em um intervalo muito grande. Nesse caso, você pode garantir que seu contêiner seja maior do que precisa (quanto maior, melhor, é claro), dependendo das preocupações com a memória.
Aqui estão alguns links, se você estiver mais interessado:
hash coalescente na wikipedia
A Wikipedia também possui um resumo de vários métodos de resolução de colisões:
Além disso, " Organização e processamento de arquivos " da Tharp abrange muitos métodos de resolução de colisões extensivamente. IMO é uma ótima referência para algoritmos de hash.
fonte
Uma explicação muito boa sobre quando e como implementar a
__hash__
função está no site da programiz :Apenas uma captura de tela para fornecer uma visão geral: (Retirado em 2019-12-13)
Quanto à implementação pessoal do método, o site acima mencionado fornece um exemplo que corresponde à resposta do millerdev .
fonte
Depende do tamanho do valor do hash que você retorna. É uma lógica simples que, se você precisar retornar um int de 32 bits com base no hash de quatro ints de 32 bits, terá colisões.
Eu preferiria operações pouco. Como, o seguinte pseudocódigo C:
Esse sistema também poderia funcionar para carros alegóricos, se você simplesmente os considerasse seu valor de bit em vez de realmente representar um valor de ponto flutuante, talvez melhor.
Para cordas, tenho pouca ou nenhuma idéia.
fonte