Hashing di un dizionario?


156

Ai fini della memorizzazione nella cache, è necessario generare una chiave cache dagli argomenti GET presenti in un dict.

Attualmente sto usando sha1(repr(sorted(my_dict.items())))( sha1()è un metodo pratico che utilizza l' hashlib internamente) ma sono curioso di sapere se esiste un modo migliore.


4
questo potrebbe non funzionare con dict nidificato. la soluzione più breve consiste nell'utilizzare json.dumps (my_dict, sort_keys = True), che verrà utilizzato come valore dict.
Andrey Fedorov,

2
FYI re: dumps, stackoverflow.com/a/12739361/1082367 dice "L'output dal pickle non è garantito come canonico per ragioni simili a dettare e impostare l'ordine come non deterministico. Non utilizzare pickle o pprint o repr per hashing ".
Matthew Cornell,

ordina le chiavi dict, non gli articoli, invierei anche le chiavi alla funzione hash.
nyuwec,

2
Interessante retroscena sull'hash delle strutture di dati mutabili (come i dizionari): python.org/dev/peps/pep-0351 è stato proposto per consentire il congelamento arbitrario di oggetti, ma è stato respinto. Per ragioni logiche, vedi questo thread in python-dev: mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur

Se i tuoi dati sono in formato json e desideri eseguire l'hash semanticamente invariante, consulta github.com/schollii/sandals/blob/master/json_sem_hash.py . Funziona su strutture nidificate (ovviamente, dal momento che JSON), e non dipende da interni di dict come l'ordine preservato (che si è evoluto nel corso della vita di Python) e darà lo stesso hash se due strutture di dati sono semanticamente uguali ( come {'a': 1, 'b':2}è semanticamente uguale a {'b':2, 'a':1}). Non l'ho ancora usato su qualcosa di troppo complicato, quindi YMMV, ma feedback positivo.
Oliver,

Risposte:


110

Se il tuo dizionario non è nidificato, puoi creare un frozenset con gli elementi del dict e utilizzare hash():

hash(frozenset(my_dict.items()))

Questo è molto meno intenso dal punto di vista computazionale rispetto alla generazione della stringa JSON o della rappresentazione del dizionario.

AGGIORNAMENTO: vedere i commenti seguenti, perché questo approccio potrebbe non produrre un risultato stabile.


9
Questo non ha funzionato per me con un dizionario nidificato. Non ho provato la soluzione di seguito (troppo complicata). La soluzione dell'OP funziona perfettamente. Ho sostituito sha1 con hash per salvare un'importazione.
Spatola il

9
@Ceaser Non funzionerà perché la tupla implica l'ordinamento ma gli articoli di dict non sono ordinati. frozenset è meglio.
Antimonio,

28
Fai attenzione all'hash integrato se qualcosa deve essere coerente su macchine diverse. Le implementazioni di Python su piattaforme cloud come Heroku e GAE restituiranno valori diversi per hash () su istanze diverse che lo rendono inutile per tutto ciò che deve essere condiviso tra due o più "macchine" (dynos nel caso di heroku)
Ben Roberts

6
Potrebbe essere interessante che la hash()funzione non produca un output stabile. Ciò significa che, dato lo stesso input, restituisce risultati diversi con istanze diverse dello stesso interprete python. A me sembra che ogni volta che viene avviato l'interprete venga generata una sorta di valore seed.
Hermann Schachner,

7
previsto. il seme viene introdotto per motivi di sicurezza per quanto ricordo di aggiungere un qualche tipo di randomizzazione della memoria. Quindi non puoi aspettarti che l'hash sia lo stesso tra due processi Python
Nikokrock,

137

L'utilizzo sorted(d.items())non è sufficiente per farci avere un repr stabile. Alcuni dei valori in dpotrebbero essere anche dizionari e le loro chiavi usciranno comunque in un ordine arbitrario. Finché tutte le chiavi sono stringhe, preferisco usare:

json.dumps(d, sort_keys=True)

Detto questo, se gli hash devono essere stabili su macchine diverse o versioni di Python, non sono sicuro che questo sia a prova di proiettile. Potresti voler aggiungere gli argomenti separatorse ensure_asciiper proteggerti da eventuali modifiche alle impostazioni predefinite lì. Apprezzerei i commenti.


6
Questo è solo paranoico, ma JSON consente alla maggior parte dei personaggi di apparire in stringhe senza alcuna fuga letterale, quindi l'encoder può fare alcune scelte se fuggire dai personaggi o semplicemente passarli. Il rischio quindi è che versioni diverse (o versioni future) dell'encoder possano fare scelte di escape diverse per impostazione predefinita, e quindi il tuo programma calcolerà valori di hash diversi per lo stesso dizionario in ambienti diversi. L' ensure_asciiargomento proteggerebbe da questo problema del tutto ipotetico.
Jack O'Connor,

4
Ho testato le prestazioni di questo con diversi set di dati, è molto più veloce di make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
@charlax ujson non garantisce l'ordine delle coppie di dict, quindi non è sicuro farlo
arthurprs

11
Questa soluzione funziona solo finché tutte le chiavi sono stringhe, ad esempio json.dumps ({'a': {(0, 5): 5, 1: 3}}) non riesce.
kadee,

5
@LorenzoBelli, puoi superarlo aggiungendo default=stral dumpscomando. Sembra funzionare bene.
mlissner

63

EDIT : Se tutte le tue chiavi sono stringhe , prima di continuare a leggere questa risposta, vedi la soluzione significativamente più semplice (e più veloce) di Jack O'Connor (che funziona anche con i dizionari annidati).

Sebbene sia stata accettata una risposta, il titolo della domanda è "Hashing a Python Dictionary" e la risposta è incompleta per quanto riguarda quel titolo. (Per quanto riguarda il corpo della domanda, la risposta è completa.)

Dizionari nidificati

Se si cerca Stack Overflow su come eseguire l'hashing di un dizionario, si potrebbe inciampare su questa domanda appropriatamente intitolata e rimanere insoddisfatti se si sta tentando di eseguire l'hashing di moltiplicare i dizionari nidificati. La risposta sopra non funzionerà in questo caso e dovrai implementare una sorta di meccanismo ricorsivo per recuperare l'hash.

Ecco uno di questi meccanismi:

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: hash di oggetti e classi

La hash()funzione funziona benissimo quando si eseguono hash di classi o istanze. Tuttavia, ecco un problema che ho riscontrato con l'hash, per quanto riguarda gli oggetti:

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

L'hash è lo stesso, anche dopo che ho modificato foo. Questo perché l'identità di foo non è cambiata, quindi l'hash è lo stesso. Se vuoi che foo abbia l'hash in modo diverso a seconda della sua definizione attuale, la soluzione è quella di eseguire l'hash di tutto ciò che sta realmente cambiando. In questo caso, l' __dict__attributo:

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

Ahimè, quando provi a fare la stessa cosa con la classe stessa:

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

La __dict__proprietà class non è un dizionario normale:

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

Ecco un meccanismo simile al precedente che gestirà le classi in modo appropriato:

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

Puoi usarlo per restituire una tupla di hash di quanti elementi desideri:

# -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: tutto il codice sopra riportato presuppone Python 3.x. Non ho testato nelle versioni precedenti, anche se presumo make_hash()funzionerà, diciamo, 2.7.2. Per quanto riguarda rendendo il lavoro esempi, io non so che

func.__code__ 

dovrebbe essere sostituito con

func.func_code

isinstance prende una sequenza per il secondo argomento, quindi isinstance (o, (set, tuple, list)) funzionerebbe.
Xealot,

grazie per avermi fatto capire che frozenset potrebbe costantemente hash parametri di querystring :)
Xealot

1
Gli articoli devono essere ordinati per creare lo stesso hash se l'ordine degli articoli di dict è diverso ma i valori chiave non sono -> return hash (tuple (frozenset (ordinati (new_o.items ()))))
Bas Koopmans

Bello! Ho anche aggiunto una chiamata a hashliste e tuple. Altrimenti prende le mie liste di numeri interi che risultano essere valori nel mio dizionario e restituisce liste di hash, che non è quello che voglio.
osa,

Un frozenset è una collezione UNORDERED, quindi non c'è nulla da guadagnare ordinando i suoi input. Elenchi e tuple invece sono raccolte ORDERED ("sequenze"), e quindi il valore di hash dovrebbe essere influenzato dall'ordine degli oggetti all'interno. Non dovresti ordinarli!
RobM,

14

Ecco una soluzione più chiara.

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

Se cambi if isinstance(o,list):a if isinstance(obj, (set, tuple, list)):allora questa funzione può funzionare su qualsiasi oggetto.
Peter Schorn,

10

Il codice seguente evita di usare la funzione hash () di Python perché non fornirà hash coerenti tra i riavvii di Python (vedere la funzione hash in Python 3.3 restituisce risultati diversi tra le sessioni ). make_hashable()convertirà l'oggetto in tuple nidificate e make_hash_sha256()convertirà anche repr()in un hash SHA256 con codifica 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=

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))). Questa non è proprio la soluzione che sto cercando, ma è un bel intermedio. Sto pensando di aggiungere type(o).__name__all'inizio di ciascuna delle tuple per forzare la differenziazione.
Poik,

Se vuoi ordinare anche l'elenco:tuple(sorted((make_hashable(e) for e in o)))
Suraj,

make_hash_sha256 () - bello!
jtlz2

1
@Suraj Non dovresti ordinare l'elenco prima dell'hashing perché gli elenchi che hanno il loro contenuto in ordini diversi non sono assolutamente la stessa cosa. Se l'ordine degli articoli non ha importanza, il problema è che si sta utilizzando una struttura dati errata. Dovresti utilizzare un set anziché un elenco.
scottclowe,

@scottclowe È vero. Grazie per aver aggiunto quel punto. Esistono 2 scenari in cui si desidera comunque un elenco (senza esigenze di ordinamento specifiche): 1. Elenco di articoli ripetuti. 2. Quando devi usare direttamente un JSON. Poiché JSON non supporta la rappresentazione "set".
Suraj,

5

Aggiornato dalla risposta 2013 ...

Nessuna delle risposte sopra mi sembra affidabile. Il motivo è l'uso di items (). Per quanto ne so, questo viene fuori in un ordine dipendente dalla macchina.

Che ne dici di questo invece?

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

Perché pensi sia importante che dict.itemsnon restituisca un elenco ordinato in modo prevedibile? frozensetse ne occupa
glarrain l'

2
Un set, per definizione, non è ordinato. Pertanto l'ordine in cui vengono aggiunti gli oggetti è irrilevante. Devi capire che la funzione integrata hashnon si preoccupa di come vengono stampati i contenuti di Frozenset o qualcosa del genere. Provalo in diverse macchine e versioni di Python e vedrai.
Glarrain,

Perché usi la chiamata hash () extra in value = hash ('% s ::% s'% (value, type (value))) ??
RuiDo,

4

Per mantenere l'ordine chiave, invece che hash(str(dictionary))o hash(json.dumps(dictionary))io preferirei soluzione rapida-and-dirty:

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

Funzionerà anche per tipi come DateTimee altro che non sono serializzabili JSON.


3
Chi garantisce che pformat o json utilizzino sempre lo stesso ordine?
ThiefMaster il

1
@ThiefMaster, "Modificato nella versione 2.5: i dizionari sono ordinati per chiave prima che il display sia calcolato; prima della 2.5, un dizionario era ordinato solo se il suo display richiedeva più di una riga, sebbene non fosse documentato." ( Docs.python. org / 2 / library / pprint.html )
Arel,

2
Questo non mi sembra valido. I moduli pprint e pformat sono compresi dagli autori per scopi di visualizzazione e non per serializzazione. Per questo motivo, non dovresti sentirti al sicuro supponendo che pformat restituirà sempre un risultato che sembra funzionare.
David Sanders,

3

È possibile utilizzare il frozendictmodulo di terze parti per bloccare il dict e renderlo hash.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Per la gestione di oggetti nidificati, è possibile utilizzare:

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 vuoi supportare più tipi, usa 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

Questo non funziona, ad esempio, per uno dictdegli DataFrameoggetti.
James Hirschorn,

@JamesHirschorn: aggiornato per fallire rumorosamente
Eric

Meglio! Ho aggiunto la seguente elifclausola per farlo funzionare con DataFrames: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) modificherò la risposta e vedrò se la accetti ...
James Hirschorn

1
OK. Vedo che stavo chiedendo qualcosa di più di "hashable", che garantisce solo che gli oggetti uguali abbiano lo stesso hash. Sto lavorando a una versione che darà lo stesso valore tra le esecuzioni e indipendente dalla versione di Python, ecc.
James Hirschorn

1
hashla randomizzazione è una funzionalità di sicurezza deliberata abilitata per impostazione predefinita in Python 3.7.
Eric

1

È possibile utilizzare la libreria di mappe per farlo. In particolare, maps.FrozenMap

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

Per installare maps, basta fare:

pip install maps

Gestisce anche il dictcaso nidificato :

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

Disclaimer: sono l'autore della mapsbiblioteca.


La libreria non ordina l'elenco all'interno di un dict. E quindi questo potrebbe produrre hash diversi. Dovrebbe esserci un'opzione per ordinare anche un elenco. Un frozenset dovrebbe aiutare, ma mi chiedo come gestiresti il ​​caso con un dict nidificato contenente un elenco di dadi. Come dict non sono lavabili.
Suraj,

1
@Suraj: si fa maniglia struttura annidata via .recurse. Vedi maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . Ordinare negli elenchi è semanticamente significativo, se si desidera l'indipendenza degli ordini è possibile convertire gli elenchi in set prima di chiamare .recurse. È inoltre possibile utilizzare il list_fnparametro per .recurseutilizzare una struttura dati hash diversa rispetto a tuple(.eg frozenset)
Pedro Cattori,

0

Un modo per affrontare il problema è creare una tupla degli elementi del dizionario:

hash(tuple(my_dict.items()))

-8

Lo faccio così:

hash(str(my_dict))

1
Qualcuno può spiegare cosa c'è di così sbagliato in questo metodo?
mhristache,

7
I dizionari @maximi non sono stabili in termini di ordine, quindi hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(mentre potrebbe funzionare per alcuni dizionari, non è garantito che funzioni su tutti).
Vlad Frolov
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.