Hashing um dicionário?

156

Para fins de armazenamento em cache, eu preciso gerar uma chave de cache a partir dos argumentos GET que estão presentes em um dict.

Atualmente estou usando sha1(repr(sorted(my_dict.items())))( sha1()é um método de conveniência que usa hashlib internamente), mas estou curioso para saber se existe uma maneira melhor.

ThiefMaster
fonte
4
isso pode não funcionar com o dict aninhado. A solução mais curta é usar json.dumps (my_dict, sort_keys = True), que retornará aos valores do dict.
Andrey Fedorov
2
Para sua informação: dump, stackoverflow.com/a/12739361/1082367 diz "Não é garantido que a saída do pickle seja canônica por motivos semelhantes para ditar e definir a ordem como não determinística. Não use pickle ou pprint ou repr para hashing. . "
Matthew Cornell
classificar as chaves dict, não os itens, eu também enviaria as chaves para a função hash.
Nyuwec 26/05
2
Histórico interessante sobre o hash de estruturas de dados mutáveis ​​(como dicionários): python.org/dev/peps/pep-0351 foi proposto para permitir o congelamento arbitrário de objetos, mas foi rejeitado. Para justificativa, consulte este tópico em python-dev: mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur
Se seus dados estiverem no formato json e você desejar hash semanticamente invariável, consulte github.com/schollii/sandals/blob/master/json_sem_hash.py . Ele funciona em estruturas aninhadas (é claro, desde json) e não depende de elementos internos do dict como ordem preservada (que evoluiu ao longo da vida útil do python) e fornecerá o mesmo hash se duas estruturas de dados forem semanticamente iguais ( like {'a': 1, 'b':2}é semanticamente o mesmo que {'b':2, 'a':1}). Eu não o usei em nada muito complicado ainda, então YMMV, mas feedback é bem-vindo.
Oliver

Respostas:

110

Se o seu dicionário não estiver aninhado, você poderá criar um frozenset com os itens do dict e usar hash():

hash(frozenset(my_dict.items()))

Isso é muito menos computacionalmente intenso do que gerar a sequência JSON ou representação do dicionário.

ATUALIZAÇÃO: Por favor, veja os comentários abaixo, por que essa abordagem pode não produzir um resultado estável.

Imran
fonte
9
Isso não funcionou para mim com um dicionário aninhado. Não tentei a solução abaixo (muito complicada). A solução do OP funciona perfeitamente bem. Substituí sha1 por hash para salvar uma importação.
Spatel
9
@ Ceaser Isso não funcionará porque a tupla implica pedidos, mas os itens de ditado não são ordenados. frozenset é melhor.
Antimony
28
Cuidado com o hash embutido, se algo precisar ser consistente em diferentes máquinas. Implementações de python em plataformas de nuvem como Heroku e GAE retornarão valores diferentes para hash () em diferentes instâncias, tornando-o inútil para qualquer coisa que deva ser compartilhada entre duas ou mais "máquinas" (dynos no caso de heroku)
Ben Roberts
6
Pode ser interessante que a hash()função não produza uma saída estável. Isso significa que, com a mesma entrada, ele retorna resultados diferentes com instâncias diferentes do mesmo interpretador python. Para mim, parece que algum tipo de valor inicial é gerado toda vez que o intérprete é iniciado.
Hermann Schachner
7
esperado. a semente é introduzida por motivos de segurança, tanto quanto me lembro de adicionar algum tipo de randomização de memória. Então você não pode esperar que o hash a ser o mesmo entre dois processos python
Nikokrock
137

Usar sorted(d.items())não é suficiente para obter um repr estável. Alguns dos valores também dpodem ser dicionários, e suas chaves ainda serão exibidas em uma ordem arbitrária. Contanto que todas as chaves sejam strings, eu prefiro usar:

json.dumps(d, sort_keys=True)

Dito isto, se os hashes precisarem ser estáveis ​​em diferentes máquinas ou versões do Python, não tenho certeza de que isso seja à prova de balas. Você pode adicionar os argumentos separatorse ensure_asciipara se proteger de quaisquer alterações nos padrões lá. Eu apreciaria comentários.

Jack O'Connor
fonte
6
Isso está sendo paranóico, mas o JSON permite que a maioria dos caracteres apareça em strings sem nenhum escape literal, de modo que o codificador pode fazer algumas escolhas sobre se deseja escapar ou apenas passar por eles. O risco, então, é que versões diferentes (ou versões futuras) do codificador possam fazer diferentes opções de escape por padrão e, em seguida, seu programa calculará diferentes valores de hash para o mesmo dicionário em ambientes diferentes. O ensure_asciiargumento protegeria contra esse problema inteiramente hipotético.
Jack O'Connor
4
Eu testei o desempenho disso com diferentes conjuntos de dados, é muito mais rápido que make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax
3
@charlax ujson não garante a ordem dos pares de dicionários, por isso não é seguro para fazer isso
arthurprs
11
Esta solução funciona apenas desde que todas as chaves sejam strings, por exemplo, json.dumps ({'a': {(0, 5): 5, 1: 3}}) falha.
Kdee
5
@ LorenzoBelli, você pode superar isso adicionando default=strao dumpscomando. Parece funcionar bem.
Mlissner
63

EDIT : Se todas as suas chaves são strings , então antes de continuar a ler esta resposta, consulte significativamente de Jack O'Connor solução mais simples (e mais rápido) (que também trabalha para hash dicionários aninhados).

Embora uma resposta tenha sido aceita, o título da pergunta é "Hashing um dicionário python" e a resposta está incompleta em relação a esse título. (No que diz respeito ao corpo da pergunta, a resposta está completa.)

Dicionários aninhados

Se alguém pesquisar Stack Overflow para saber como fazer hash em um dicionário, poderá se deparar com essa pergunta com título adequado e ficar insatisfeito se estiver tentando hash multiplicar dicionários aninhados. A resposta acima não funcionará neste caso, e você precisará implementar algum tipo de mecanismo recursivo para recuperar o hash.

Aqui está um desses mecanismos:

import copy

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that contains
  only other hashable types (including any lists, tuples, sets, and
  dictionaries).
  """

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

Bônus: Hashing de objetos e classes

A hash()função funciona muito bem quando você hash classes ou instâncias. No entanto, aqui está um problema que encontrei com o hash, no que diz respeito aos objetos:

class Foo(object): pass
foo = Foo()
print (hash(foo)) # 1209812346789
foo.a = 1
print (hash(foo)) # 1209812346789

O hash é o mesmo, mesmo depois que eu mudei de foo. Isso ocorre porque a identidade de foo não mudou, então o hash é o mesmo. Se você deseja que o hash seja diferente de acordo com sua definição atual, a solução é fazer o hash do que está realmente mudando. Nesse caso, o __dict__atributo:

class Foo(object): pass
foo = Foo()
print (make_hash(foo.__dict__)) # 1209812346789
foo.a = 1
print (make_hash(foo.__dict__)) # -78956430974785

Infelizmente, quando você tenta fazer o mesmo com a própria classe:

print (make_hash(Foo.__dict__)) # TypeError: unhashable type: 'dict_proxy'

A __dict__propriedade da classe não é um dicionário normal:

print (type(Foo.__dict__)) # type <'dict_proxy'>

Aqui está um mecanismo semelhante ao anterior, que manipulará as classes adequadamente:

import copy

DictProxyType = type(object.__dict__)

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that 
  contains only other hashable types (including any lists, tuples, sets, and
  dictionaries). In the case where other kinds of objects (like classes) need 
  to be hashed, pass in a collection of object attributes that are pertinent. 
  For example, a class can be hashed in this fashion:

    make_hash([cls.__dict__, cls.__name__])

  A function can be hashed like so:

    make_hash([fn.__dict__, fn.__code__])
  """

  if type(o) == DictProxyType:
    o2 = {}
    for k, v in o.items():
      if not k.startswith("__"):
        o2[k] = v
    o = o2  

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

Você pode usar isso para retornar uma tupla de hash de quantos elementos você desejar:

# -7666086133114527897
print (make_hash(func.__code__))

# (-7666086133114527897, 3527539)
print (make_hash([func.__code__, func.__dict__]))

# (-7666086133114527897, 3527539, -509551383349783210)
print (make_hash([func.__code__, func.__dict__, func.__name__]))

NOTA: todo o código acima assume o Python 3.x. Não testou em versões anteriores, embora eu assuma make_hash()que funcionará, digamos, 2.7.2. Tanto quanto fazer a exemplos de trabalho, eu não sei que

func.__code__ 

deve ser substituído por

func.func_code
jomido
fonte
isinstance usa uma sequência para o segundo argumento, para que isinstance (o, (set, tuple, list)) funcione.
Xealot #
Obrigado por me fazer perceber frozenset poderia consistentemente parâmetros de hash querystring :)
Xealot
1
Os itens precisam ser classificados para criar o mesmo hash se a ordem do item dict for diferente, mas os valores-chave não forem -> retornar hash (tupla (frozenset (classificado (new_o.items ()))))
Bas Koopmans
Agradável! Também adicionei uma chamada para hashlistas e tuplas. Caso contrário, ele pega minhas listas de números inteiros que são valores no meu dicionário e retorna listas de hashes, que não é o que eu quero.
Osa
Um frozenset é uma coleção UNORDERED, portanto não há nada a ganhar classificando suas entradas. Listas e tuplas, por outro lado, são coleções ORDERED ("sequências") e, portanto, o valor do hash deve ser afetado pela ordem dos itens. Você não deve classificá-los!
RobM
14

Aqui está uma solução mais clara.

def freeze(o):
  if isinstance(o,dict):
    return frozenset({ k:freeze(v) for k,v in o.items()}.items())

  if isinstance(o,list):
    return tuple([freeze(v) for v in o])

  return o


def make_hash(o):
    """
    makes a hash out of anything that contains only list,dict and hashable types including string and numeric types
    """
    return hash(freeze(o))  
smartnut007
fonte
Se você mudar if isinstance(o,list):para if isinstance(obj, (set, tuple, list)):, esta função poderá funcionar em qualquer objeto.
Peter Schorn
10

O código abaixo evita o uso da função hash () do Python porque não fornecerá hashes consistentes nas reinicializações do Python (consulte a função hash no Python 3.3 retorna resultados diferentes entre as sessões ). make_hashable()converterá o objeto em tuplas aninhadas e make_hash_sha256()também converterá repr()em um hash SHA256 codificado em base64.

import hashlib
import base64

def make_hash_sha256(o):
    hasher = hashlib.sha256()
    hasher.update(repr(make_hashable(o)).encode())
    return base64.b64encode(hasher.digest()).decode()

def make_hashable(o):
    if isinstance(o, (tuple, list)):
        return tuple((make_hashable(e) for e in o))

    if isinstance(o, dict):
        return tuple(sorted((k,make_hashable(v)) for k,v in o.items()))

    if isinstance(o, (set, frozenset)):
        return tuple(sorted(make_hashable(e) for e in o))

    return o

o = dict(x=1,b=2,c=[3,4,5],d={6,7})
print(make_hashable(o))
# (('b', 2), ('c', (3, 4, 5)), ('d', (6, 7)), ('x', 1))

print(make_hash_sha256(o))
# fyt/gK6D24H9Ugexw+g3lbqnKZ0JAcgtNW+rXIDeU2Y=
Claudio Fahey
fonte
1
make_hash_sha256(((0,1),(2,3)))==make_hash_sha256({0:1,2:3})==make_hash_sha256({2:3,0:1})!=make_hash_sha256(((2,3),(0,1))). Essa não é a solução que estou procurando, mas é um bom intermediário. Estou pensando em adicionar type(o).__name__ao início de cada uma das tuplas para forçar a diferenciação.
Poik 17/09/17
Se você também deseja classificar a lista:tuple(sorted((make_hashable(e) for e in o)))
Suraj
make_hash_sha256 () - legal!
Jtlz2 31/10/19
1
@Suraj Você não deve querer ordenar a lista antes do hash, porque listas com conteúdo em ordens diferentes definitivamente não são a mesma coisa. Se a ordem dos itens não importa, o problema é que você está usando a estrutura de dados incorreta. Você deve usar um conjunto em vez de uma lista.
scottclowe
@scottclowe Isso é muito verdade. Obrigado por adicionar esse ponto. Existem 2 cenários em que você ainda deseja uma lista (sem necessidades específicas de pedidos) - 1. Lista de itens repetidos. 2. Quando você deve usar um JSON diretamente. Como o JSON não suporta a representação "definida".
Suraj
5

Atualizado a partir da resposta de 2013 ...

Nenhuma das respostas acima me parece confiável. O motivo é o uso de itens (). Tanto quanto eu sei, isso sai em uma ordem dependente da máquina.

Que tal isso?

import hashlib

def dict_hash(the_dict, *ignore):
    if ignore:  # Sometimes you don't care about some items
        interesting = the_dict.copy()
        for item in ignore:
            if item in interesting:
                interesting.pop(item)
        the_dict = interesting
    result = hashlib.sha1(
        '%s' % sorted(the_dict.items())
    ).hexdigest()
    return result
Steve Yeago
fonte
Por que você acha importante dict.itemsnão retornar uma lista previsível? frozensetcuida disso
glarrain
2
Um conjunto, por definição, não é ordenado. Portanto, a ordem na qual os objetos são adicionados é irrelevante. Você precisa perceber que a função hashinterna não se importa com a forma como o conteúdo do frozenset é impresso ou algo parecido. Teste-o em várias máquinas e versões em python e você verá.
glarrain
Por que você usa a chamada extra hash () no valor = hash ('% s ::% s'% (valor, tipo (valor))) ??
RuiDo 6/07
4

Para preservar a ordem das chaves, em vez de hash(str(dictionary))ou hash(json.dumps(dictionary))prefiro uma solução rápida e suja:

from pprint import pformat
h = hash(pformat(dictionary))

Funcionará mesmo para tipos como DateTimee mais que não são JSON serializáveis.

shirk3y
fonte
3
Quem garante que pformat ou json sempre usam a mesma ordem?
ThiefMaster
1
@ThiefMaster, "Alterado na versão 2.5: os dicionários são classificados por chave antes do cálculo da exibição; antes do 2.5, um dicionário era classificado apenas se sua exibição exigisse mais de uma linha, embora isso não estivesse documentado." ( Docs.python. org / 2 / library / pprint.html )
Arel
2
Isso não me parece válido. Os módulos pprint e pformat são entendidos pelos autores como para fins de exibição e não para serialização. Por esse motivo, você não deve se sentir seguro ao assumir que o pformat sempre retornará um resultado que funcione.
David Sanders
3

Você pode usar o frozendictmódulo de terceiros para congelar seu ditado e torná-lo lavável.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Para manipular objetos aninhados, você pode usar:

import collections.abc

def make_hashable(x):
    if isinstance(x, collections.abc.Hashable):
        return x
    elif isinstance(x, collections.abc.Sequence):
        return tuple(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Set):
        return frozenset(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Mapping):
        return frozendict({k: make_hashable(v) for k, v in x.items()})
    else:
        raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

Se você deseja dar suporte a mais tipos, use functools.singledispatch(Python 3.7):

@functools.singledispatch
def make_hashable(x):
    raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

@make_hashable.register
def _(x: collections.abc.Hashable):
    return x

@make_hashable.register
def _(x: collections.abc.Sequence):
    return tuple(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Set):
    return frozenset(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Mapping):
    return frozendict({k: make_hashable(v) for k, v in x.items()})

# add your own types here
Eric
fonte
Isso não funciona, por exemplo, para um dictde DataFrameobjetos.
James Hirschorn 12/03/19
@JamesHirschorn: Atualizado para falhar alto #
Eric
Melhor! Eu adicionei o seguinte elifcláusula para que ele funcione com DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) eu vou editar a resposta e ver se você aceitá-lo ...
James Hirschorn
1
ESTÁ BEM. Vejo que estava pedindo algo mais do que "hashable", que apenas garante que objetos iguais tenham o mesmo hash. Eu estou trabalhando em uma versão que vai dar ao o mesmo valor entre as execuções, e independente da versão python, etc ..
James Hirschorn
1
hashrandomization é um recurso de segurança deliberado ativado por padrão no python 3.7.
Eric
1

Você pode usar a biblioteca de mapas para fazer isso. Especificamente, maps.FrozenMap

import maps
fm = maps.FrozenMap(my_dict)
hash(fm)

Para instalar maps, basta fazer:

pip install maps

Ele também lida com o dictcaso aninhado :

import maps
fm = maps.FrozenMap.recurse(my_dict)
hash(fm)

Disclaimer: Eu sou o autor da mapsbiblioteca.

Pedro Cattori
fonte
A biblioteca não classifica a lista dentro de um ditado. E, portanto, isso pode produzir hash diferente. Também deve haver uma opção para classificar uma lista. Um frozenset deve ajudar, mas eu me pergunto como você lidaria com o caso com um ditado aninhado contendo uma lista de dictos. Como ditado são unhashable.
Suraj
1
@Suraj: ela faz estrutura aninhada alça via .recurse. Consulte maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . A ordenação em listas é semanticamente significativa. Se você deseja independência de ordem, pode converter suas listas em conjuntos antes da chamada .recurse. Você também pode fazer uso do list_fnparâmetro para .recurseusar uma estrutura de dados Hashable diferente tuple(.eg frozenset)
Pedro Cattori
0

Uma maneira de abordar o problema é fazer uma tupla dos itens do dicionário:

hash(tuple(my_dict.items()))
Anônimo
fonte
-8

Eu faço assim:

hash(str(my_dict))
garbanzio
fonte
1
Alguém pode explicar o que há de errado com esse método?
mhristache
7
Os dicionários do @maximi não são estáveis ​​nos termos de ordem, portanto hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(embora possa funcionar para alguns dicionários, não é garantido que funcione em todos).
Vlad Frolov