web-dev-qa-db-fra.com

Hacher un dictionnaire?

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

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

110
ThiefMaster

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 nécessite beaucoup moins de calculs que de générer la chaîne JSON ou la représentation du dictionnaire.

81
Imran

Utiliser sorted(d.items()) n'est pas suffisant pour nous procurer un repr. Stable. Certaines des valeurs de d pourraient également être des dictionnaires, et leurs clés apparaîtront 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 sûr que ce soit pare-balles. Vous voudrez peut-être ajouter les arguments separators et ensure_ascii pour vous protéger des modifications apportées aux valeurs par défaut. J'apprécierais les commentaires.

96
Jack O'Connor

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

Bien qu'une réponse ait été acceptée, le titre de la question est "Hachage d'un dictionnaire python" 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 on cherche à Stack Overflow comment hacher un dictionnaire, on peut trébucher sur cette question qui s'intitule bien et laisser insatisfait si l'on essaie de hacher des dictionnaires multiples. La réponse ci-dessus ne fonctionnera pas dans ce cas, et vous devrez implémenter un mécanisme récursif pour récupérer le hachage.

Voici un tel mécanisme:

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

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

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

Le hash 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 hash est le même. Si vous voulez que foo hache différemment en fonction de sa définition actuelle, la solution consiste à supprimer tout ce qui change réellement. Dans ce cas, l'attribut __dict__:

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 propriété class __dict__ n'est pas un dictionnaire normal:

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

Voici un mécanisme similaire à 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 retourner un tuple de hachage contenant le nombre d'éléments souhaités:

# -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 précédentes, bien que je suppose que make_hash () fonctionne dans, par exemple, 2.7.2. En ce qui concerne le bon fonctionnement des exemples, je fais sais que 

func.__code__ 

devrait être remplacé par 

func.func_code
54
jomido

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))  
11
smartnut007

Mise à jour à partir de 2013 répondre ...

Aucune des réponses ci-dessus ne me semble fiable. La raison en est l'utilisation de items (). Autant que je sache, cela vient 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
5
Steve Yeago

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 des types tels que DateTime et davantage qui ne sont pas sérialisables JSON.

4
shirk3y

Le code ci-dessous évite l'utilisation de la fonction hash () de Python car elle ne fournira pas de hachages cohérents lors des redémarrages de Python (voir La fonction hash dans Python 3.3 renvoie différents résultats entre les sessions ). make_hashable() convertira l'objet en n-uplets imbriqués et make_hash_sha256() convertira également repr() en un hachage SHA256 codé 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=
4
Claudio Fahey

Vous pouvez utiliser la bibliothèque maps pour le faire. Plus précisément, maps.FrozenMap

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

Pour installer maps, procédez comme suit:

pip install maps

Il gère également le cas imbriqué dict:

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

Disclaimer: Je suis l'auteur de la bibliothèque maps.

0
Pedro Cattori

L’approche générale est satisfaisante, mais vous pouvez envisager la méthode de hachage.

SHA a été conçu pour la force cryptographique (la vitesse aussi, mais la force est plus importante). Vous voudrez peut-être en tenir compte. Par conséquent, l’utilisation de la fonction hash intégrée est probablement une bonne idée, à moins que la sécurité ne soit d’une importance capitale ici.

0
Eli Bendersky

Vous pouvez utiliser le module tiers frozendict pour figer votre dict et le rendre utilisable.

from frozendict import frozendict
my_dict = frozendict(my_dict)

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

import collections.abc

def make_hashable(x):
    if isinstance(x, collections.abc.Sequence):
        return Tuple(freeze(xi) for xi in x)
    Elif isinstance(x, collections.abc.Set):
        return frozenset(freeze(xi) for xi in x)
    Elif isinstance(x, collections.abc.Mapping):
        return frozendict({k: freeze(v) for k, v in x.items()})
    else:
        return x
0
Eric