Hachage d'un dictionnaire?

156

À des fins de mise en cache, je dois générer une clé de cache à partir des arguments GET qui sont présents dans un dict.

J'utilise actuellement sha1(repr(sorted(my_dict.items())))( sha1()c'est une méthode pratique qui utilise hashlib en interne) mais je suis curieux de savoir s'il existe un meilleur moyen.

ThiefMaster
la source
4
cela pourrait ne pas fonctionner avec dict imbriqué. la solution la plus courte consiste à utiliser json.dumps (my_dict, sort_keys = True) à la place, qui se réinitialisera en valeurs dict.
Andrey Fedorov
2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 dit "La sortie de pickle n'est pas garantie d'être canonique pour des raisons similaires de dict et de définir l'ordre étant non déterministe. N'utilisez pas pickle ou pprint ou repr pour le hachage . "
Matthew Cornell
trier les clés dict, pas les éléments, j'enverrais également les clés à la fonction de hachage.
nyuwec
2
Contexte intéressant sur le hachage des structures de données mutables (comme les dictionnaires): python.org/dev/peps/pep-0351 a été proposé pour permettre le gel arbitraire des objets, mais rejeté. Pour la justification, voir ce fil de discussion dans python-dev: mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur
Si vos données sont au format json et que vous souhaitez un hachage sémantiquement invariant, consultez github.com/schollii/sandals/blob/master/json_sem_hash.py . Il fonctionne sur des structures imbriquées (bien sûr, depuis json), et ne dépend pas des internes de dict comme l'ordre conservé (qui a évolué au cours de la vie de python), et donnera le même hachage si deux structures de données sont sémantiquement identiques ( like {'a': 1, 'b':2}est sémantiquement le même que {'b':2, 'a':1}). Je ne l'ai pas encore utilisé sur quelque chose de trop compliqué, donc YMMV, mais les commentaires sont les bienvenus.
Oliver

Réponses:

110

Si votre dictionnaire n'est pas imbriqué, vous pouvez créer un frozenset avec les éléments du dict et utiliser hash():

hash(frozenset(my_dict.items()))

Cela demande beaucoup moins de calculs que de générer la chaîne JSON ou la représentation du dictionnaire.

MISE À JOUR: S'il vous plaît voir les commentaires ci-dessous, pourquoi cette approche peut ne pas produire un résultat stable.

Imran
la source
9
Cela n'a pas fonctionné pour moi avec un dictionnaire imbriqué. Je n'ai pas essayé la solution ci-dessous (trop compliquée). La solution de l'OP fonctionne parfaitement bien. J'ai remplacé sha1 par hash pour enregistrer une importation.
spatel
9
@Ceaser Cela ne fonctionnera pas car le tuple implique un ordre mais les éléments dict ne sont pas ordonnés. frozenset est meilleur.
Antimoine
28
Méfiez-vous du hachage intégré si quelque chose doit être cohérent sur différentes machines. Les implémentations de python sur des plates-formes cloud comme Heroku et GAE renverront des valeurs différentes pour hash () sur différentes instances, ce qui le rend inutile pour tout ce qui doit être partagé entre deux ou plusieurs «machines» (dynos dans le cas de heroku)
Ben Roberts
6
Il peut être intéressant que la hash()fonction ne produit pas une sortie stable. Cela signifie que, étant donné la même entrée, il renvoie des résultats différents avec différentes instances du même interpréteur python. Pour moi, il semble qu'une sorte de valeur de départ soit générée à chaque démarrage de l'interpréteur.
Hermann Schachner
7
attendu. la graine est introduite pour des raisons de sécurité dans la mesure où je me souviens d'ajouter une sorte de randomisation de la mémoire. Vous ne pouvez donc pas vous attendre à ce que le hachage soit le même entre deux processus python
Nikokrock
137

L'utilisation sorted(d.items())n'est pas suffisante pour nous procurer une reproduction stable. Certaines des valeurs de dpeuvent également être des dictionnaires, et leurs clés sortiront toujours dans un ordre arbitraire. Tant que toutes les clés sont des chaînes, je préfère utiliser:

json.dumps(d, sort_keys=True)

Cela dit, si les hachages doivent être stables sur différentes machines ou versions de Python, je ne suis pas certain que ce soit à l'épreuve des balles. Vous souhaiterez peut-être ajouter les arguments separatorset ensure_asciipour vous protéger de toute modification des valeurs par défaut. J'apprécierais les commentaires.

Jack O'Connor
la source
6
C'est juste paranoïaque, mais JSON permet à la plupart des caractères de s'afficher dans des chaînes sans échappement littéral, de sorte que l'encodeur peut choisir entre échapper ou simplement les passer. Le risque est alors que différentes versions (ou futures versions) de l'encodeur puissent faire des choix d'échappement différents par défaut, puis votre programme calculerait différentes valeurs de hachage pour le même dictionnaire dans différents environnements. L' ensure_asciiargument protégerait contre ce problème entièrement hypothétique.
Jack O'Connor
4
J'ai testé les performances de cela avec différents ensembles de données, c'est beaucoup plus rapide que make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax
3
@charlax ujson ne garantit pas l'ordre des paires de dict, donc ce n'est pas sûr de le faire
arthurprs
11
Cette solution ne fonctionne que tant que toutes les clés sont des chaînes, par exemple json.dumps ({'a': {(0, 5): 5, 1: 3}}) échoue.
kadee
5
@LorenzoBelli, vous pouvez surmonter cela en ajoutant default=strà la dumpscommande. Semble bien fonctionner.
mlissner
63

EDIT : Si toutes vos clés sont des chaînes , avant de continuer à lire cette réponse, veuillez consulter la solution nettement plus simple (et plus rapide) de Jack O'Connor (qui fonctionne également pour le hachage de dictionnaires imbriqués).

Bien qu'une réponse ait été acceptée, le titre de la question est "Hashing a python dictionary", et la réponse est incomplète en ce qui concerne ce titre. (En ce qui concerne le corps de la question, la réponse est complète.)

Dictionnaires imbriqués

Si l'on recherche Stack Overflow pour savoir comment hacher un dictionnaire, on peut tomber sur cette question bien intitulée, et ne pas être satisfait si l'on tente de hacher plusieurs dictionnaires imbriqués. La réponse ci-dessus ne fonctionnera pas dans ce cas, et vous devrez implémenter une sorte de mécanisme récursif pour récupérer le hachage.

Voici un de ces mécanismes:

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: Objets et classes de hachage

La hash()fonction fonctionne très bien lorsque vous hachez des classes ou des instances. Cependant, voici un problème que j'ai trouvé avec le hachage, en ce qui concerne les objets:

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

Le hachage est le même, même après avoir modifié foo. C'est parce que l'identité de foo n'a pas changé, donc le hachage est le même. Si vous voulez que foo hache différemment en fonction de sa définition actuelle, la solution est de hacher tout ce qui change réellement. Dans ce cas, l' __dict__attribut:

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

Hélas, lorsque vous essayez de faire la même chose avec la classe elle-même:

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

La __dict__propriété class n'est pas un dictionnaire normal:

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

Voici un mécanisme similaire au précédent qui gérera les classes de manière appropriée:

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

Vous pouvez l'utiliser pour renvoyer un tuple de hachage du nombre d'éléments que vous souhaitez:

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

REMARQUE: tout le code ci-dessus suppose Python 3.x. N'a pas testé dans les versions antérieures, bien que je suppose que make_hash()cela fonctionnera dans, disons, 2.7.2. En ce qui concerne les travaux faisant des exemples, je ne sais que

func.__code__ 

doit être remplacé par

func.func_code
Jomido
la source
isinstance prend une séquence pour le deuxième argument, donc isinstance (o, (set, tuple, list)) fonctionnerait.
Xealot
merci de m'avoir fait réaliser que frozenset pouvait systématiquement hacher les paramètres de chaîne de requête :)
Xealot
1
Les éléments doivent être triés afin de créer le même hachage si l'ordre des éléments de dict est différent mais que les valeurs clés ne le sont pas -> return hash (tuple (frozenset (sorted (new_o.items ()))))
Bas Koopmans
Agréable! J'ai également ajouté un appel aux hashlistes et aux tuples autour. Sinon, il prend mes listes d'entiers qui se trouvent être des valeurs dans mon dictionnaire, et renvoie des listes de hachages, ce qui n'est pas ce que je veux.
osa
Un frozenset est une collection UNORDERED, il n'y a donc rien à gagner en triant ses entrées. Les listes et les tuples, quant à eux, sont des collections ORDERED ("séquences"), et donc la valeur de hachage doit être affectée par l'ordre des éléments à l'intérieur. Vous ne devriez pas les trier!
RobM
14

Voici une solution plus claire.

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
la source
Si vous changez if isinstance(o,list):en if isinstance(obj, (set, tuple, list)):alors cette fonction peut fonctionner sur n'importe quel objet.
Peter Schorn le
10

Le code ci-dessous évite d'utiliser la fonction Python hash () car elle ne fournira pas de hachages cohérents entre les redémarrages de Python (voir la fonction de hachage dans Python 3.3 renvoie des résultats différents entre les sessions ). make_hashable()convertira l'objet en tuples imbriqués et make_hash_sha256()convertira également le repr()en un hachage SHA256 encodé 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
la source
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))). Ce n'est pas tout à fait la solution que je recherche, mais c'est un bon intermédiaire. Je pense ajouter type(o).__name__au début de chacun des tuples pour forcer la différenciation.
Poik du
Si vous souhaitez également trier la liste:tuple(sorted((make_hashable(e) for e in o)))
Suraj
make_hash_sha256 () - sympa!
jtlz2
1
@Suraj Vous ne devriez pas vouloir trier la liste avant le hachage car les listes dont le contenu est dans des ordres différents ne sont certainement pas la même chose. Si l'ordre des éléments n'a pas d'importance, le problème est que vous utilisez la mauvaise structure de données. Vous devriez utiliser un ensemble au lieu d'une liste.
scottclowe
@scottclowe C'est très vrai. Merci d'avoir ajouté ce point. Il existe 2 scénarios où vous voudriez toujours une liste (sans besoins de commande spécifiques) - 1. Liste des éléments répétitifs. 2. Lorsque vous devez utiliser directement un JSON. Comme JSON ne prend pas en charge la représentation "set".
Suraj du
5

Mis à jour à partir de la réponse de 2013 ...

Aucune des réponses ci-dessus ne me semble fiable. La raison en est l'utilisation d'éléments (). Autant que je sache, cela sort dans un ordre dépendant de la machine.

Que diriez-vous de cela à la place?

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
la source
Pourquoi pensez-vous qu'il est important de dict.itemsne pas renvoyer une liste ordonnée de manière prévisible? frozensets'occupe de ça
glarrain
2
Un ensemble, par définition, n'est pas ordonné. Ainsi, l'ordre dans lequel les objets sont ajoutés n'est pas pertinent. Vous devez comprendre que la fonction intégrée hashne se soucie pas de la façon dont le contenu du frozenset est imprimé ou quelque chose du genre. Testez-le sur plusieurs machines et versions de python et vous verrez.
glarrain
Pourquoi utilisez-vous l'appel de hachage supplémentaire () dans value = hash ('% s ::% s'% (valeur, type (valeur))) ??
RuiDo
4

Pour préserver l'ordre des clés, au lieu de hash(str(dictionary))ou hash(json.dumps(dictionary))je préférerais une solution rapide et sale:

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

Cela fonctionnera même pour les types comme DateTimeet plus qui ne sont pas sérialisables JSON.

shirk3y
la source
3
Qui garantit que pformat ou json utilisent toujours le même ordre?
ThiefMaster
1
@ThiefMaster, "Modifié dans la version 2.5: les dictionnaires sont triés par clé avant le calcul de l'affichage; avant 2.5, un dictionnaire était trié uniquement si son affichage nécessitait plus d'une ligne, bien que cela n'ait pas été documenté." ( Docs.python. org / 2 / library / pprint.html )
Arel
2
Cela ne me semble pas valable. Les modules pprint et pformat sont compris par les auteurs comme étant à des fins d'affichage et non de sérialisation. Pour cette raison, vous ne devriez pas vous sentir en sécurité en supposant que pformat renverra toujours un résultat qui fonctionne.
David Sanders
3

Vous pouvez utiliser le frozendictmodule tiers pour geler votre dict et le rendre hachable.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Pour gérer les objets imbriqués, vous pouvez utiliser:

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 vous souhaitez prendre en charge plus de types, utilisez 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
la source
Cela ne fonctionne pas, par exemple, pour dictdes DataFrameobjets.
James Hirschorn
@JamesHirschorn: mise à jour pour échouer bruyamment
Eric
Mieux! J'ai ajouté la elifclause suivante pour que cela fonctionne avec DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) Je vais éditer la réponse et voir si vous l'acceptez ...
James Hirschorn
1
D'ACCORD. Je vois que je demandais quelque chose de plus que «hashable», ce qui garantit seulement que les objets égaux auront le même hachage. Je travaille sur une version qui donnera la même valeur entre les exécutions, et indépendante de la version python, etc.
James Hirschorn
1
hashLa randomisation est une fonctionnalité de sécurité délibérée activée par défaut dans python 3.7.
Eric
1

Vous pouvez utiliser la bibliothèque de cartes pour ce faire. Plus précisément, les cartes.

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

Pour l'installer maps, faites simplement:

pip install maps

Il gère également le dictcas imbriqué :

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

Avertissement: Je suis l'auteur de la mapsbibliothèque.

Pedro Cattori
la source
La bibliothèque ne trie pas la liste dans un dict. Et par conséquent, cela pourrait produire un hachage différent. Il devrait également y avoir une option pour trier une liste. Un frozenset devrait aider, mais je me demande comment géreriez-vous le cas avec un dict imbriqué contenant une liste de dictés. Comme dict sont insurmontables.
Suraj du
1
@Suraj: il fait la structure de poignée imbriquée via .recurse. Voir maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . L'ordre dans les listes est sémantiquement significatif, si vous voulez l'indépendance de l'ordre, vous pouvez convertir vos listes en ensembles avant d'appeler .recurse. Vous pouvez également utiliser le list_fnparamètre pour .recurseutiliser une structure de données hachable différente de tuple(.eg frozenset)
Pedro Cattori
0

Une façon d'aborder le problème consiste à créer un tuple des éléments du dictionnaire:

hash(tuple(my_dict.items()))
Anonyme
la source
-8

Je fais ça comme ça:

hash(str(my_dict))
garbanzio
la source
1
Quelqu'un peut-il expliquer ce qui ne va pas avec cette méthode?
mhristache
7
@maximi Les dictionnaires ne sont pas stables en termes d'ordre, donc hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(bien que cela puisse fonctionner pour certains dictionnaires, il n'est pas garanti de fonctionner sur tous).
Vlad Frolov