Ein Wörterbuch zerschlagen?

156

Für Caching-Zwecke muss ich einen Cache-Schlüssel aus GET-Argumenten generieren, die in einem Diktat vorhanden sind.

Derzeit verwende ich sha1(repr(sorted(my_dict.items())))( sha1()ist eine bequeme Methode, die Hashhlib intern verwendet), aber ich bin gespannt, ob es einen besseren Weg gibt.

DiebMaster
quelle
4
Dies funktioniert möglicherweise nicht mit verschachtelten Diktaten. Die kürzeste Lösung besteht darin, stattdessen json.dumps (my_dict, sort_keys = True) zu verwenden, die in dikt-Werte zurückgeführt werden.
Andrey Fedorov
2
Zu Ihrer Information : dumps , stackoverflow.com/a/12739361/1082367 sagt: "Die Ausgabe von pickle ist aus ähnlichen Gründen nicht kanonisch, um zu diktieren und die Reihenfolge nicht deterministisch festzulegen. Verwenden Sie kein pickle oder pprint oder repr zum Hashing . "
Matthew Cornell
Sortieren Sie die Diktatschlüssel, nicht die Elemente. Ich würde die Schlüssel auch an die Hash-Funktion senden.
Nyuwec
2
Interessante Hintergrundgeschichte zum Hashing veränderlicher Datenstrukturen (wie Wörterbücher): python.org/dev/peps/pep-0351 wurde vorgeschlagen, um willkürlich einfrierende Objekte zuzulassen, wurde jedoch abgelehnt. Weitere Informationen finden
FluxLemur
Wenn Ihre Daten im JSON-Format vorliegen und Sie semantisch invariantes Hashing wünschen, überprüfen Sie github.com/schollii/sandals/blob/master/json_sem_hash.py . Es funktioniert mit verschachtelten Strukturen (natürlich seit json) und hängt nicht von diktalen Interna wie der erhaltenen Ordnung ab (die sich über die Lebensdauer von Python entwickelt hat) und gibt den gleichen Hash aus, wenn zwei Datenstrukturen semantisch gleich sind ( wie {'a': 1, 'b':2}ist semantisch das gleiche wie {'b':2, 'a':1}). Ich habe es noch nicht für etwas zu Kompliziertes verwendet, also YMMV, aber Feedback ist willkommen.
Oliver

Antworten:

110

Wenn Ihr Wörterbuch nicht verschachtelt ist, können Sie ein Frozenset mit den Elementen des Diktats erstellen und Folgendes verwenden hash():

hash(frozenset(my_dict.items()))

Dies ist viel weniger rechenintensiv als das Generieren der JSON-Zeichenfolge oder der Darstellung des Wörterbuchs.

UPDATE: Bitte beachten Sie die folgenden Kommentare, warum dieser Ansatz möglicherweise kein stabiles Ergebnis liefert.

Imran
quelle
9
Bei einem verschachtelten Wörterbuch hat dies bei mir nicht funktioniert. Ich habe die folgende Lösung nicht ausprobiert (zu kompliziert). Die OP-Lösung funktioniert einwandfrei. Ich habe sha1 durch Hash ersetzt, um einen Import zu speichern.
Spatel
9
@Ceaser Das funktioniert nicht, weil Tupel das Bestellen impliziert, aber diktierte Elemente ungeordnet sind. Frozenset ist besser.
Antimon
28
Achten Sie auf den integrierten Hash, wenn etwas auf verschiedenen Computern konsistent sein muss. Implementierungen von Python auf Cloud-Plattformen wie Heroku und GAE geben unterschiedliche Werte für hash () auf verschiedenen Instanzen zurück, sodass es für alles unbrauchbar ist, was zwischen zwei oder mehr "Maschinen" (Dynos im Fall von Heroku) geteilt werden muss
Ben Roberts
6
Es könnte interessant sein, dass die hash()Funktion keine stabile Ausgabe erzeugt. Dies bedeutet, dass bei gleicher Eingabe unterschiedliche Ergebnisse mit unterschiedlichen Instanzen desselben Python-Interpreters zurückgegeben werden. Für mich sieht es so aus, als würde bei jedem Start des Interpreters ein Startwert generiert.
Hermann Schachner
7
erwartet. Der Startwert wird aus Sicherheitsgründen eingeführt, soweit ich mich erinnere, eine Art Speicher-Randomisierung hinzuzufügen. Sie können also nicht erwarten, dass der Hash zwischen zwei Python-Prozessen gleich ist
Nikokrock
137

Die Verwendung sorted(d.items())reicht nicht aus, um uns einen stabilen Repräsentanten zu verschaffen. Einige der Werte in dkönnten auch Wörterbücher sein, und ihre Schlüssel werden weiterhin in einer beliebigen Reihenfolge ausgegeben. Solange alle Schlüssel Zeichenfolgen sind, bevorzuge ich:

json.dumps(d, sort_keys=True)

Das heißt, wenn die Hashes auf verschiedenen Computern oder Python-Versionen stabil sein müssen, bin ich mir nicht sicher, ob dies kugelsicher ist. Möglicherweise möchten Sie die Argumente separatorsund hinzufügen ensure_ascii, um sich vor Änderungen an den dortigen Standardeinstellungen zu schützen. Ich würde mich über Kommentare freuen.

Jack O'Connor
quelle
6
Dies ist nur paranoid, aber JSON ermöglicht es den meisten Zeichen, in Zeichenfolgen angezeigt zu werden, ohne dass ein wörtliches Escapezeichen entsteht, sodass der Encoder einige Entscheidungen darüber treffen kann, ob Zeichen maskiert oder nur durchlaufen werden sollen. Das Risiko besteht dann darin, dass verschiedene Versionen (oder zukünftige Versionen) des Encoders standardmäßig unterschiedliche Escape-Entscheidungen treffen und Ihr Programm dann unterschiedliche Hash-Werte für dasselbe Wörterbuch in verschiedenen Umgebungen berechnet. Das ensure_asciiArgument würde vor diesem völlig hypothetischen Problem schützen.
Jack O'Connor
4
Ich habe die Leistung mit einem anderen Datensatz getestet, es ist viel viel schneller als make_hash. gist.github.com/charlax/b8731de51d2ea86c6eb9
charlax
3
@charlax ujson garantiert nicht die Reihenfolge der Diktatpaare, daher ist es nicht sicher, dies zu tun
arthurprs
11
Diese Lösung funktioniert nur, solange alle Schlüssel Zeichenfolgen sind, z. B. schlägt json.dumps ({'a': {(0, 5): 5, 1: 3}}) fehl.
Kadee
5
@ LorenzoBelli, Sie können dies überwinden, indem Sie default=strdem dumpsBefehl hinzufügen . Scheint gut zu funktionieren.
mlissner
63

BEARBEITEN : Wenn alle Ihre Schlüssel Zeichenfolgen sind , lesen Sie bitte Jack O'Connors wesentlich einfachere (und schnellere) Lösung (die auch für das Hashing verschachtelter Wörterbücher funktioniert ) , bevor Sie diese Antwort weiter lesen .

Obwohl eine Antwort akzeptiert wurde, lautet der Titel der Frage "Hashing a Python Dictionary", und die Antwort ist in Bezug auf diesen Titel unvollständig. (In Bezug auf den Hauptteil der Frage ist die Antwort vollständig.)

Verschachtelte Wörterbücher

Wenn man im Stapelüberlauf nach dem Hash eines Wörterbuchs sucht, kann man auf diese treffend betitelte Frage stoßen und unbefriedigt bleiben, wenn man versucht, verschachtelte Wörterbücher zu hashen. Die obige Antwort funktioniert in diesem Fall nicht und Sie müssen einen rekursiven Mechanismus implementieren, um den Hash abzurufen.

Hier ist ein solcher Mechanismus:

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: Objekte und Klassen zerschlagen

Die hash()Funktion funktioniert hervorragend, wenn Sie Klassen oder Instanzen hashen. Hier ist jedoch ein Problem, das ich mit Hash in Bezug auf Objekte gefunden habe:

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

Der Hash ist der gleiche, auch nachdem ich foo geändert habe. Dies liegt daran, dass sich die Identität von foo nicht geändert hat, sodass der Hash derselbe ist. Wenn Sie möchten, dass foo je nach aktueller Definition unterschiedlich hasht, besteht die Lösung darin, alles zu hashen, was sich tatsächlich ändert. In diesem Fall ist das __dict__Attribut:

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

Leider, wenn Sie versuchen, dasselbe mit der Klasse selbst zu tun:

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

Die class __dict__-Eigenschaft ist kein normales Wörterbuch:

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

Hier ist ein ähnlicher Mechanismus wie zuvor, der Klassen angemessen behandelt:

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

Sie können dies verwenden, um ein Hash-Tupel mit beliebig vielen Elementen zurückzugeben:

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

HINWEIS: Der gesamte obige Code setzt Python 3.x voraus. In früheren Versionen nicht getestet, obwohl ich davon make_hash()ausgehe, dass dies beispielsweise in 2.7.2 funktioniert. Soweit die Beispiele funktionieren, weiß ich das

func.__code__ 

sollte durch ersetzt werden

func.func_code
Jomido
quelle
isinstance verwendet eine Sequenz für das zweite Argument, sodass isinstance (o, (set, tuple, list)) funktionieren würde.
Xealot
Vielen Dank, dass Sie mir klar gemacht haben, dass Frozenset durchgehend Hash-Querystring-Parameter haben kann :)
Xealot
1
Die Elemente müssen sortiert werden, um denselben Hash zu erstellen, wenn die Reihenfolge der diktierten Elemente unterschiedlich ist, die Schlüsselwerte jedoch nicht -> Hash zurückgeben (tuple (frozenset (sortiert (new_o.items ())))
Bas Koopmans
Nett! Ich habe auch einen Anruf zu hashListen und Tupeln hinzugefügt . Andernfalls werden meine Ganzzahllisten verwendet, die zufällig Werte in meinem Wörterbuch sind, und es werden Listen mit Hashes zurückgegeben, was nicht das ist, was ich möchte.
osa
Ein Frozenset ist eine UNORDERED-Sammlung, daher gibt es nichts zu gewinnen, wenn Sie seine Eingaben sortieren. Listen und Tupel hingegen sind ORDERED-Sammlungen ("Sequenzen"). Daher sollte der Hash-Wert von der Reihenfolge der darin enthaltenen Elemente beeinflusst werden. Sie sollten sie nicht sortieren!
RobM
14

Hier ist eine klarere Lösung.

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
quelle
Wenn Sie zu wechseln if isinstance(o,list):, kann if isinstance(obj, (set, tuple, list)):diese Funktion für jedes Objekt verwendet werden.
Peter Schorn
10

Der folgende Code vermeidet die Verwendung der Python-Funktion hash (), da keine Hashes bereitgestellt werden, die bei Neustarts von Python konsistent sind (siehe Hash-Funktion in Python 3.3 gibt unterschiedliche Ergebnisse zwischen Sitzungen zurück ). make_hashable()konvertiert das Objekt in verschachtelte Tupel und make_hash_sha256()konvertiert den repr()in einen base64-codierten SHA256-Hash.

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
quelle
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))). Dies ist nicht ganz die Lösung, nach der ich suche, aber es ist eine schöne Zwischenstufe. Ich denke darüber type(o).__name__nach, den Anfang jedes Tupels zu ergänzen, um die Differenzierung zu erzwingen.
Poik
Wenn Sie die Liste auch sortieren möchten:tuple(sorted((make_hashable(e) for e in o)))
Suraj
make_hash_sha256 () - schön!
jtlz2
1
@Suraj Sie sollten die Liste nicht vor dem Hashing sortieren wollen, da Listen, deren Inhalt in unterschiedlicher Reihenfolge vorliegt, definitiv nicht dasselbe sind. Wenn die Reihenfolge der Elemente keine Rolle spielt, besteht das Problem darin, dass Sie die falsche Datenstruktur verwenden. Sie sollten ein Set anstelle einer Liste verwenden.
Scottclowe
@scottclowe Das ist sehr wahr. Vielen Dank, dass Sie diesen Punkt hinzugefügt haben. Es gibt zwei Szenarien, in denen Sie immer noch eine Liste wünschen (ohne spezielle Bestellanforderungen): 1. Liste der sich wiederholenden Elemente. 2. Wenn Sie einen JSON direkt verwenden müssen. Da JSON die "Set" -Darstellung nicht unterstützt.
Suraj
5

Aktualisiert von 2013 Antwort ...

Keine der oben genannten Antworten scheint mir zuverlässig zu sein. Der Grund ist die Verwendung von items (). Soweit ich weiß, erfolgt dies in maschinenabhängiger Reihenfolge.

Wie wäre es stattdessen damit?

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
quelle
Warum ist es Ihrer Meinung nach wichtig, dass dict.itemskeine vorhersehbar geordnete Liste zurückgegeben wird? frozensetkümmert sich darum
Glarrain
2
Eine Menge ist per Definition ungeordnet. Daher ist die Reihenfolge, in der Objekte hinzugefügt werden, irrelevant. Sie müssen sich hashdarüber im Klaren sein , dass es der integrierten Funktion egal ist, wie der Frozenset-Inhalt gedruckt wird oder so etwas. Testen Sie es in mehreren Maschinen und Python-Versionen und Sie werden sehen.
Glarrain
Warum verwenden Sie den zusätzlichen Aufruf von hash () in value = hash ('% s ::% s'% (Wert, Typ (Wert)))?
RuiDo
4

Um die Schlüsselreihenfolge beizubehalten, würde ich anstelle von hash(str(dictionary))oder hash(json.dumps(dictionary))eine schnelle und schmutzige Lösung bevorzugen:

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

Es funktioniert auch für Typen wie DateTimeund mehr, die nicht JSON-serialisierbar sind.

shirk3y
quelle
3
Wer garantiert, dass pformat oder json immer dieselbe Reihenfolge verwenden?
ThiefMaster
1
@ThiefMaster, "In Version 2.5 geändert: Wörterbücher werden nach Schlüssel sortiert, bevor die Anzeige berechnet wird. Vor 2.5 wurde ein Wörterbuch nur sortiert, wenn für die Anzeige mehr als eine Zeile erforderlich war, obwohl dies nicht dokumentiert war." ( Docs.python. org / 2 / library / pprint.html )
Arel
2
Das scheint mir nicht gültig zu sein. Die pprint-Module und das pformat werden von den Autoren als Anzeigezwecke und nicht als Serialisierung verstanden. Aus diesem Grund sollten Sie sich nicht sicher fühlen, wenn Sie davon ausgehen, dass pformat immer ein Ergebnis zurückgibt, das zufällig funktioniert.
David Sanders
3

Sie können das frozendictModul eines Drittanbieters verwenden , um Ihr Diktat einzufrieren und es hashbar zu machen.

from frozendict import frozendict
my_dict = frozendict(my_dict)

Für den Umgang mit verschachtelten Objekten können Sie Folgendes verwenden:

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

Wenn Sie weitere Typen unterstützen möchten, verwenden Sie 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
quelle
Das funktioniert nicht, zum Beispiel für einen dictvon DataFrameObjekten.
James Hirschorn
@ JamesHirschorn: Aktualisiert, um laut zu scheitern
Eric
Besser! Ich habe die folgende elifKlausel hinzugefügt , damit es mit DataFrames funktioniert : elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) Ich werde die Antwort bearbeiten und sehen, ob Sie sie akzeptieren ...
James Hirschorn
1
OK. Ich habe nach etwas mehr als "Hashable" gefragt, was nur garantiert, dass Objekte, die gleich sind, denselben Hash haben. Ich arbeite an einer Version, die zwischen den Läufen den gleichen Wert
liefert
1
hashRandomisierung ist eine absichtliche Sicherheitsfunktion, die in Python 3.7 standardmäßig aktiviert ist.
Eric
1

Sie können dazu die Kartenbibliothek verwenden. Insbesondere maps.FrozenMap

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

Zum Installieren mapseinfach:

pip install maps

Es behandelt auch den verschachtelten dictFall:

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

Haftungsausschluss: Ich bin der Autor der mapsBibliothek.

Pedro Cattori
quelle
Die Bibliothek sortiert die Liste nicht innerhalb eines Diktats. Und daher könnte dies zu unterschiedlichem Hash führen. Es sollte auch eine Option zum Sortieren einer Liste geben. Ein Frozenset sollte helfen, aber ich frage mich, wie Sie mit einem verschachtelten Diktat umgehen würden, das eine Liste von Diktaten enthält. Als Dikt sind nicht zerlegbar.
Suraj
1
@Suraj: es tut Griff verschachtelte Struktur über .recurse. Siehe maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse . Das Bestellen in Listen ist semantisch sinnvoll. Wenn Sie die Unabhängigkeit der Reihenfolge wünschen, können Sie Ihre Listen vor dem Aufruf in Sets konvertieren .recurse. Sie können den list_fnParameter auch verwenden .recurse, um eine andere hashable Datenstruktur als tuple(.eg frozenset) zu verwenden
Pedro Cattori
0

Eine Möglichkeit, sich dem Problem zu nähern, besteht darin, ein Tupel aus den Elementen des Wörterbuchs zu erstellen:

hash(tuple(my_dict.items()))
Anonym
quelle
-8

So mach ich es:

hash(str(my_dict))
Kichererbsen
quelle
1
Kann jemand erklären, was an dieser Methode so falsch ist?
Mhristache
7
@maximi Wörterbücher sind in Bezug auf die Reihenfolge nicht stabil, daher hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(obwohl es für einige Wörterbücher funktioniert, kann nicht garantiert werden, dass es für alle funktioniert).
Vlad Frolov