Hashing un diccionario?

156

Para propósitos de almacenamiento en caché, necesito generar una clave de caché a partir de argumentos GET que están presentes en un dict.

Actualmente estoy usando sha1(repr(sorted(my_dict.items())))( sha1()es un método de conveniencia que usa hashlib internamente) pero tengo curiosidad por saber si hay una mejor manera.

ThiefMaster
fuente
44
Esto podría no funcionar con dict anidado. La solución más corta es usar json.dumps (my_dict, sort_keys = True) en su lugar, que se repetirá en valores dict.
Andrey Fedorov
2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 dice "No se garantiza que la salida de pickle sea canónica por razones similares para dictar y establecer el orden como no determinista. No use pickle o pprint o repr para hashing ".
Matthew Cornell
ordenar las claves dict, no los elementos, también enviaría las claves a la función hash.
nyuwec
2
Historia de fondo interesante sobre el hashing de estructuras de datos mutables (como diccionarios): se propuso python.org/dev/peps/pep-0351 para permitir la congelación arbitraria de objetos, pero se rechazó. Para una explicación, vea este hilo en python-dev: mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur
Si sus datos están en formato json y desea un hashing semánticamente invariable, consulte github.com/schollii/sandals/blob/master/json_sem_hash.py . Funciona en estructuras anidadas (por supuesto, desde json), y no depende de elementos internos de dict como el orden preservado (que ha evolucionado durante la vida útil de python), y dará el mismo hash si dos estructuras de datos son semánticamente iguales ( me gusta {'a': 1, 'b':2}es semánticamente lo mismo que {'b':2, 'a':1}). Todavía no lo he usado en nada demasiado complicado, así que YMMV, pero los comentarios son bienvenidos.
Oliver

Respuestas:

110

Si su diccionario no está anidado, puede hacer un congelamiento con los elementos del dict y usar hash():

hash(frozenset(my_dict.items()))

Esto es mucho menos computacionalmente intenso que generar la cadena JSON o la representación del diccionario.

ACTUALIZACIÓN: consulte los comentarios a continuación, por qué este enfoque podría no producir un resultado estable.

Imran
fuente
9
Esto no funcionó para mí con un diccionario anidado. No he probado la solución a continuación (demasiado complicada). La solución del OP funciona perfectamente bien. Sustituí sha1 con hash para guardar una importación.
spatel
9
@Ceaser Eso no funcionará porque la tupla implica ordenar pero los elementos dictados no están ordenados. Frozenset es mejor.
Antimonio
28
Tenga cuidado con el hash incorporado si algo necesita ser consistente en diferentes máquinas. Las implementaciones de python en plataformas en la nube como Heroku y GAE devolverán diferentes valores para hash () en diferentes instancias, lo que lo hace inútil para cualquier cosa que deba compartirse entre dos o más "máquinas" (dynos en el caso de heroku)
Ben Roberts
66
Puede ser interesante que la hash()función no produzca una salida estable. Esto significa que, dada la misma entrada, devuelve resultados diferentes con instancias diferentes del mismo intérprete de Python. Para mí, parece que se genera algún tipo de valor inicial cada vez que se inicia el intérprete.
Hermann Schachner
77
esperado. la semilla se introduce por razones de seguridad por lo que recuerdo agregar algún tipo de aleatorización de memoria. Por lo tanto, no puede esperar que el hash sea el mismo entre dos procesos de Python
Nikokrock
137

Usar sorted(d.items())no es suficiente para obtener una repr estable. Algunos de los valores en dpodrían ser diccionarios también, y sus claves seguirán saliendo en un orden arbitrario. Mientras todas las teclas sean cadenas, prefiero usar:

json.dumps(d, sort_keys=True)

Dicho esto, si los hashes necesitan ser estables en diferentes máquinas o versiones de Python, no estoy seguro de que esto sea a prueba de balas. Es posible que desee agregar los argumentos separatorsy ensure_asciipara protegerse de cualquier cambio en los valores predeterminados allí. Agradecería los comentarios.

Jack O'Connor
fuente
66
Esto es solo paranoico, pero JSON permite que la mayoría de los caracteres aparezcan en cadenas sin ningún escape literal, por lo que el codificador puede elegir si escapar de los caracteres o simplemente pasarlos. El riesgo es que diferentes versiones (o futuras versiones) del codificador podrían tomar diferentes opciones de escape de forma predeterminada, y luego su programa calcularía diferentes valores hash para el mismo diccionario en diferentes entornos. El ensure_asciiargumento protegería contra este problema completamente hipotético.
Jack O'Connor
44
Probé el rendimiento de esto con un conjunto de datos diferente, es mucho más rápido que make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax
3
@charlax ujson no garantiza el orden de los pares de dict, por lo que no es seguro hacerlo
arthurprs
11
Esta solución solo funciona mientras todas las claves sean cadenas, por ejemplo, json.dumps ({'a': {(0, 5): 5, 1: 3}}) falla.
kadee
55
@LorenzoBelli, puede superar eso agregando default=stral dumpscomando. Parece funcionar bien.
mlissner
63

EDITAR : Si todas sus claves son cadenas , entonces antes de continuar leyendo esta respuesta, consulte la solución significativamente más simple (y más rápida) de Jack O'Connor (que también funciona para los diccionarios anidados hash).

Aunque se ha aceptado una respuesta, el título de la pregunta es "Hashing a python dictionary", y la respuesta es incompleta con respecto a ese título. (Con respecto al cuerpo de la pregunta, la respuesta está completa).

Diccionarios anidados

Si uno busca en Stack Overflow cómo hash un diccionario, uno puede tropezar con esta pregunta acertadamente titulada, y dejar insatisfecho si está intentando hash multiplicar diccionarios anidados. La respuesta anterior no funcionará en este caso, y tendrá que implementar algún tipo de mecanismo recursivo para recuperar el hash.

Aquí hay uno de esos 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()))))

Bonus: objetos y clases hash

La hash()función funciona muy bien cuando hash clases o instancias. Sin embargo, aquí hay un problema que encontré con hash, en lo que respecta a los objetos:

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

El hash es el mismo, incluso después de haber modificado foo. Esto se debe a que la identidad de foo no ha cambiado, por lo que el hash es el mismo. Si desea que el hash haga hash de manera diferente dependiendo de su definición actual, la solución es eliminar lo que realmente está cambiando. En este caso, el __dict__atributo:

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

Por desgracia, cuando intentas hacer lo mismo con la clase misma:

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

La __dict__propiedad de clase no es un diccionario normal:

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

Aquí hay un mecanismo similar al anterior que manejará las clases adecuadamente:

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()))))

Puede usar esto para devolver una tupla hash de la cantidad de elementos que desee:

# -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 el código anterior supone Python 3.x. No probé en versiones anteriores, aunque supongo make_hash()que funcionará en, digamos, 2.7.2. En lo que a hacer el trabajo de ejemplos, que no sé que

func.__code__ 

debe ser reemplazado con

func.func_code
jomido
fuente
isinstance toma una secuencia para el segundo argumento, por lo que isinstance (o, (set, tuple, list)) funcionaría.
Xealot
gracias por hacerme darme cuenta de que frozenset podría hash constantemente parámetros de cadena de consulta :)
Xealot
1
Los elementos deben clasificarse para crear el mismo hash si el orden de los elementos dictados es diferente pero los valores clave no son -> return hash (tuple (frozenset (sorted (new_o.items ())))))
Bas Koopmans
¡Agradable! También agregué una llamada a las hashlistas y tuplas. De lo contrario, toma mis listas de enteros que resultan ser valores en mi diccionario y devuelve listas de hashes, que no es lo que quiero.
osa
Un conjunto congelado es una colección SIN ORDENAR, por lo que no hay nada que ganar al ordenar sus entradas. Las listas y las tuplas, por otro lado, son colecciones ORDENADAS ("secuencias"), por lo que el valor hash debe verse afectado por el orden de los elementos dentro. ¡No deberías ordenarlos!
RobM
14

Aquí hay una solución más 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
fuente
Si cambia if isinstance(o,list):a if isinstance(obj, (set, tuple, list)):, esta función puede funcionar en cualquier objeto.
Peter Schorn
10

El siguiente código evita el uso de la función hash () de Python porque no proporcionará hashes que sean consistentes en los reinicios de Python (ver la función hash en Python 3.3 devuelve resultados diferentes entre sesiones ). make_hashable()convertirá el objeto en tuplas anidadas y make_hash_sha256()también lo convertirá repr()en un hash SHA256 codificado en 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
fuente
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))). Esta no es la solución que estoy buscando, pero es un buen intermedio. Estoy pensando en agregar type(o).__name__al comienzo de cada una de las tuplas para forzar la diferenciación.
Poik
Si desea ordenar la lista también:tuple(sorted((make_hashable(e) for e in o)))
Suraj
make_hash_sha256 () - ¡bien!
jtlz2
1
@Suraj No debería ordenar la lista antes del hash porque las listas que tienen su contenido en diferentes órdenes definitivamente no son lo mismo. Si el orden de los elementos no importa, el problema es que está utilizando una estructura de datos incorrecta. Debería usar un conjunto en lugar de una lista.
scottclowe el
@scottclowe Eso es muy cierto. Gracias por agregar ese punto. Hay 2 escenarios en los que aún desea una lista (sin necesidades específicas de pedido): 1. Lista de elementos repetidos. 2. Cuando tienes que usar un JSON directamente. Como JSON no admite la representación "set".
Suraj
5

Actualizado desde 2013 respuesta ...

Ninguna de las respuestas anteriores me parece confiable. La razón es el uso de elementos (). Hasta donde yo sé, esto sale en un orden dependiente de la máquina.

¿Qué tal esto en su lugar?

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
fuente
¿Por qué crees que importa que dict.itemsno devuelva una lista ordenada de manera predecible? frozensetse encarga de eso
glarrain
2
Un conjunto, por definición, no está ordenado. Por lo tanto, el orden en que se agregan los objetos es irrelevante. Debes darte cuenta de que a la función integrada hashno le importa cómo se imprimen los contenidos congelados o algo así. Pruébelo en varias máquinas y versiones de python y verá.
glarrain
¿Por qué utiliza la llamada extra hash () en value = hash ('% s ::% s'% (value, type (value))) ??
RuiDo
4

Para preservar el orden de las claves, en lugar de hash(str(dictionary))o hash(json.dumps(dictionary))preferiría una solución rápida y sucia:

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

Funcionará incluso para tipos como DateTimey más que no son serializables JSON.

shirk3y
fuente
3
¿Quién garantiza que pformat o json siempre usan el mismo orden?
ThiefMaster
1
@ThiefMaster, "Modificado en la versión 2.5: los diccionarios se ordenan por clave antes de que se calcule la pantalla; antes de 2.5, un diccionario se clasificaba solo si su pantalla requería más de una línea, aunque eso no estaba documentado" ( docs.python. org / 2 / library / pprint.html )
Arel
2
Esto no me parece válido. Los autores entienden que los módulos y el formato pprint son para mostrar y no para serializar. Debido a esto, no debe sentirse seguro al suponer que pformat siempre devolverá un resultado que funcione.
David Sanders
3

Podrías usar el frozendictmódulo de terceros para congelar tu dict y hacerlo hashaable.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Para manejar objetos anidados, puede ir con:

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__))

Si desea admitir más 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
fuente
Esto no funciona, por ejemplo, para un dictde DataFramelos objetos.
James Hirschorn el
@JamesHirschorn: actualizado para fallar en voz alta
Eric
¡Mejor! Agregué la siguiente elifcláusula para que funcione con DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist())
editaré
1
OKAY. Veo que estaba pidiendo algo más que "hashable", que solo garantiza que los objetos que son iguales tendrán el mismo hash. Estoy trabajando en una versión que dará el mismo valor entre ejecuciones e independiente de la versión de Python, etc.
James Hirschorn
1
hashla aleatorización es una función de seguridad deliberada habilitada por defecto en python 3.7.
Eric
1

Puede usar la biblioteca de mapas para hacer esto. Específicamente, mapas . Mapa congelado

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

Para instalar maps, solo haz:

pip install maps

También maneja el dictcaso anidado :

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

Descargo de responsabilidad: soy el autor de la mapsbiblioteca.

Pedro Cattori
fuente
La biblioteca no ordena la lista dentro de un dict. Y, por lo tanto, esto podría producir un hash diferente. Debería haber una opción para ordenar una lista también. Un conjunto congelado debería ayudar, pero me pregunto cómo manejaría el caso con un dictado anidado que contiene una lista de dictados. Como dict son inquebrantables.
Suraj
1
@Suraj: se hace mango estructura anidada vía .recurse. Ver maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . Ordenar en listas tiene un significado semántico, si desea independencia de orden puede convertir sus listas en conjuntos antes de llamar .recurse. También puede usar el list_fnparámetro para .recurseusar una estructura de datos hashaable diferente a tuple(.eg frozenset)
Pedro Cattori
0

Una forma de abordar el problema es hacer una tupla de los elementos del diccionario:

hash(tuple(my_dict.items()))
Anónimo
fuente
-8

Lo hago así:

hash(str(my_dict))
garbanzio
fuente
1
¿Alguien puede explicar qué tiene de malo este método?
mhristache
77
Los diccionarios @maximi no son estables en términos de orden, por lo tanto hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(aunque podría funcionar para algunos diccionarios, no se garantiza que funcione en todos).
Vlad Frolov