Python-Hash-Diktate


91

Als Übung und hauptsächlich zu meiner eigenen Unterhaltung implementiere ich einen Backtracking-Packrat-Parser. Die Inspiration dafür ist, dass ich eine bessere Vorstellung davon haben möchte, wie hygienische Makros in einer algolähnlichen Sprache funktionieren würden (im Gegensatz zu den syntaxfreien Lisp-Dialekten, in denen sie normalerweise vorkommen). Aus diesem Grund werden bei unterschiedlichen Durchläufen durch die Eingabe möglicherweise unterschiedliche Grammatiken angezeigt, sodass zwischengespeicherte Analyseergebnisse ungültig sind, es sei denn, ich speichere auch die aktuelle Version der Grammatik zusammen mit den zwischengespeicherten Analyseergebnissen. ( BEARBEITEN : Eine Konsequenz dieser Verwendung von Schlüsselwertsammlungen ist, dass sie unveränderlich sein sollten, aber ich beabsichtige nicht, die Schnittstelle verfügbar zu machen, damit sie geändert werden können, sodass entweder veränderbare oder unveränderliche Sammlungen in Ordnung sind.)

Das Problem ist, dass Python-Dikte nicht als Schlüssel für andere Dikte angezeigt werden können. Selbst die Verwendung eines Tupels (wie ich es sowieso tun würde) hilft nicht.

>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>> 

Ich denke, es müssen ganz unten Tupel sein. Jetzt bietet die Python-Standardbibliothek ungefähr das, was ich brauche, collections.namedtuplehat eine ganz andere Syntax, kann aber als Schlüssel verwendet werden. Fortsetzung der obigen Sitzung:

>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}

OK. Aber ich muss eine Klasse für jede mögliche Kombination von Schlüsseln in der Regel erstellen, die ich verwenden möchte, was nicht so schlimm ist, da jede Analyseregel genau weiß, welche Parameter sie verwendet, damit die Klasse gleichzeitig definiert werden kann als die Funktion, die die Regel analysiert.

Bearbeiten: Ein zusätzliches Problem mit namedtuples ist, dass sie streng positionell sind. Zwei Tupel, die so aussehen, als ob sie unterschiedlich sein sollten, können tatsächlich gleich sein:

>>> you = namedtuple("foo",["bar","baz"])
>>> me = namedtuple("foo",["bar","quux"])
>>> you(bar=1,baz=2) == me(bar=1,quux=2)
True
>>> bob = namedtuple("foo",["baz","bar"])
>>> you(bar=1,baz=2) == bob(bar=1,baz=2)
False

tl'dr: Wie bekomme ich dicts, die als Schlüssel für andere verwendet werden können ?dict s verwendet werden können?

Nachdem ich die Antworten ein wenig gehackt habe, ist hier die vollständigere Lösung, die ich verwende. Beachten Sie, dass dies ein wenig zusätzliche Arbeit leistet, um die resultierenden Diktate für praktische Zwecke vage unveränderlich zu machen. Natürlich ist es immer noch recht einfach, sich durch einen Anruf darum zu kümmern, dict.__setitem__(instance, key, value)aber wir sind alle Erwachsene hier.

class hashdict(dict):
    """
    hashable dict implementation, suitable for use as a key into
    other dicts.

        >>> h1 = hashdict({"apples": 1, "bananas":2})
        >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
        >>> h1+h2
        hashdict(apples=1, bananas=3, mangoes=5)
        >>> d1 = {}
        >>> d1[h1] = "salad"
        >>> d1[h1]
        'salad'
        >>> d1[h2]
        Traceback (most recent call last):
        ...
        KeyError: hashdict(bananas=3, mangoes=5)

    based on answers from
       http://stackoverflow.com/questions/1151658/python-hashable-dicts

    """
    def __key(self):
        return tuple(sorted(self.items()))
    def __repr__(self):
        return "{0}({1})".format(self.__class__.__name__,
            ", ".join("{0}={1}".format(
                    str(i[0]),repr(i[1])) for i in self.__key()))

    def __hash__(self):
        return hash(self.__key())
    def __setitem__(self, key, value):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def __delitem__(self, key):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def clear(self):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def pop(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def popitem(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def setdefault(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    def update(self, *args, **kwargs):
        raise TypeError("{0} does not support item assignment"
                         .format(self.__class__.__name__))
    # update is not ok because it mutates the object
    # __add__ is ok because it creates a new object
    # while the new object is under construction, it's ok to mutate it
    def __add__(self, right):
        result = hashdict(self)
        dict.update(result, right)
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Das hashdictmuss unveränderlich sein, zumindest nachdem Sie mit dem Hashing begonnen haben. Warum also nicht die Werte keyund hashals Attribute des hashdictObjekts zwischenspeichern? Ich habe __key()und modifiziert und __hash__()getestet, um zu bestätigen, dass es viel schneller ist. SO erlaubt keinen formatierten Code in Kommentaren, also werde ich ihn hier verlinken
Sam Watkins

Antworten:


67

Hier ist der einfache Weg, um ein Hash-Wörterbuch zu erstellen. Denken Sie daran, sie nach dem Einbetten in ein anderes Wörterbuch aus offensichtlichen Gründen nicht zu mutieren.

class hashabledict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.items())))

7
Dies stellt die Konsistenz von Gleichung und Hash nicht scharf sicher, während meine frühere Antwort die Verwendung der __key-Methode verwendet (in der Praxis sollte jeder Ansatz funktionieren, obwohl dieser Ansatz möglicherweise verlangsamt wird, indem eine nicht benötigte Zwischenliste erstellt wird, die durch s / items korrigiert werden kann / iteritems / - vorausgesetzt Python 2. * wie du nicht sagst ;-).
Alex Martelli

5
Es wäre wahrscheinlich besser, nur ein Frozenset zu verwenden, als ein Tupel mit Sortierung. Dies wäre nicht nur schneller, sondern Sie können auch nicht davon ausgehen, dass die Wörterbuchschlüssel vergleichbar sind.
Asmeurer

1
Es scheint, dass es eine Möglichkeit geben sollte, eine Hash-Funktion zu vermeiden, O(n*log(n))bei der ndie Anzahl der dictEinträge angegeben ist. Weiß jemand, ob Pythons frozensetHash-Funktion in linearer Zeit ausgeführt wird?
Tom Karzes

2
@HelloGoodbye Ein Diktat kann auch so dict(key1=value1, key2=value2,...)oder so erstellt werden dict([(key1, value1), (key2, value2),...)]). Gleiches gilt für diesen. Die Kreation, die Sie gepostet haben, heißt wörtlich
smido

2
@smido: Danke. Ich fand auch, dass man nur ein Literal sprechen kann, dh hashabledict({key_a: val_a, key_b: val_b, ...}).
HelloGoodbye

62

Hashables sollten unveränderlich sein - dies nicht erzwingen, sondern darauf vertrauen, dass Sie ein Diktat nach seiner ersten Verwendung als Schlüssel nicht mutieren. Der folgende Ansatz würde funktionieren:

class hashabledict(dict):
  def __key(self):
    return tuple((k,self[k]) for k in sorted(self))
  def __hash__(self):
    return hash(self.__key())
  def __eq__(self, other):
    return self.__key() == other.__key()

Wenn Sie Ihre Diktate mutieren müssen und sie NOCH als Schlüssel verwenden möchten, explodiert die Komplexität hundertfach - um nicht zu sagen, dass dies nicht möglich ist, aber ich werde bis zu einem SEHR spezifischen Hinweis warten, bevor ich in DIESEN unglaublichen Morast gerate! -)


Ich möchte die Diktate auf keinen Fall mutieren, wenn sie einmal vorbereitet sind. Das würde den Rest des Packrad-Algorithmus auseinanderfallen lassen.
SingleNegationElimination

Dann funktioniert die von mir vorgeschlagene Unterklasse - beachten Sie, wie sie das "Positions" -Problem umgeht ( bevor Sie Ihre Frage bearbeitet haben, um darauf hinzuweisen ;-) mit dem sortedSchlüssel in __ ;-).
Alex Martelli

Das positionsabhängige Verhalten von namedtuple überraschte mich zum Teufel. Ich hatte damit gespielt und dachte, es könnte immer noch ein einfacher Weg sein, das Problem zu lösen, aber das hat meine Hoffnungen ziemlich
zunichte gemacht

Nehmen wir an, ich habe ein Diktat und möchte es einem Hashabledict übertragen. Wie würde ich das machen?
Jononomo


32

Alles, was benötigt wird, um Wörterbücher für Ihren Zweck nutzbar zu machen, ist das Hinzufügen einer __hash__ -Methode:

class Hashabledict(dict):
    def __hash__(self):
        return hash(frozenset(self))

Beachten Sie, dass die Frozenset- Konvertierung für alle Wörterbücher funktioniert (dh die Schlüssel müssen nicht sortierbar sein). Ebenso gibt es keine Einschränkung für die Wörterbuchwerte.

Wenn es viele Wörterbücher mit identischen Schlüsseln, aber unterschiedlichen Werten gibt, muss der Hash die Werte berücksichtigen. Der schnellste Weg dazu ist:

class Hashabledict(dict):
    def __hash__(self):
        return hash((frozenset(self), frozenset(self.itervalues())))

Dies ist schneller als frozenset(self.iteritems())aus zwei Gründen. Zunächst verwendet der frozenset(self)Schritt die im Wörterbuch gespeicherten Hash-Werte erneut und speichert unnötige Aufrufe von hash(key). Zweitens greift die Verwendung von itervalues direkt auf die Werte zu und vermeidet die vielen Speicherzuweisungsaufrufe, die von Elementen verwendet werden , um bei jeder Suche viele neue Schlüssel- / Wertetupel im Speicher zu bilden.


@RaymondHettinger Korrigieren Sie mich, wenn ich falsch liege, aber ich dachte dict, dass die Hash-Werte der Schlüssel nicht zwischengespeichert werden - obwohl einzelne Klassen (wie str) ihre Hashes zwischenspeichern können und dies auch tun. Zumindest beim Erstellen einer dictmit meinen benutzerdefinierten Klasseninstanzen als Schlüssel verwendeten Instanz wurden deren __hash__Methoden bei jeder Zugriffsoperation aufgerufen (Python 3.4). Unabhängig davon, ob ich richtig bin oder nicht, bin ich mir nicht sicher, wie hash(frozenset(self))die vorberechneten Hashwerte wiederverwendet werden können, es sei denn, sie werden in den Schlüsseln selbst zwischengespeichert (in diesem Fall werden hash(frozenset(self.items())sie auch wiederverwendet).
Max

In Bezug auf Ihren zweiten Punkt zur Erstellung von (Schlüssel / Wert-) Tupeln dachte ich, dass .items () -Methoden eher eine Ansicht als eine Liste von Tupeln zurückgeben und dass die Erstellung dieser Ansicht nicht das Kopieren der zugrunde liegenden Schlüssel und Werte umfasst. (Wieder Python 3.4.) Trotzdem sehe ich den Vorteil, nur die Schlüssel zu hashen, wenn die meisten Eingaben unterschiedliche Schlüssel haben - weil (1) das Hashing von Werten ziemlich teuer ist und (2) es ziemlich restriktiv ist, zu verlangen, dass Werte hashbar sind
Max

6
Dies hat auch die Möglichkeit, denselben Hash für zwei verschiedene Wörterbücher zu erstellen. Betrachten Sie {'one': 1, 'two': 2}und{'one': 2, 'two': 1}
AgDude

Mike Graham erklärt in seinem Kommentar , dass es eine schlechte Idee ist, ein Diktat aus einem anderen Grund als der Definition __missing__abzuleiten. Was denken Sie?
Piotr Dobrogost

1
Die Unterklasse von dict ist seit Python 2.2 gut definiert. Beispiele aus der Python-Standardbibliothek finden Sie unter collection.OrderedDict und collection.Counter. Der andere Kommentar basiert auf der unbegründeten Annahme, dass nur Unterklassen von MutableMapping gut definiert sind.
Raymond Hettinger

23

Die gegebenen Antworten sind in Ordnung, aber sie könnten verbessert werden, indem der Hash frozenset(...)anstelle von tuple(sorted(...))generiert wird:

>>> import timeit
>>> timeit.timeit('hash(tuple(sorted(d.iteritems())))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
4.7758948802947998
>>> timeit.timeit('hash(frozenset(d.iteritems()))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')")
1.8153600692749023

Der Leistungsvorteil hängt vom Inhalt des Wörterbuchs ab, aber in den meisten Fällen, die ich getestet habe, ist das Hashing mit frozensetmindestens zweimal schneller (hauptsächlich, weil es nicht sortiert werden muss).


1
Beachten Sie, dass nicht sowohl die Schlüssel als auch die Werte angegeben werden müssen. Diese Lösung wäre viel schneller als : hash(frozenset(d)).
Raymond Hettinger

10
@RaymondHettinger: hash(frozenset(d))führt zu identischen Hashes für 2 Diktate mit ähnlichen Schlüsseln, aber unterschiedlichen Werten!
Oben Sonne

4
Das ist kein Problem. Es ist die Aufgabe von __eq__, zwischen Diktaten mit unterschiedlichen Werten zu unterscheiden. Die Aufgabe von __hash__ besteht lediglich darin, den Suchraum zu verkleinern.
Raymond Hettinger

5
Dies gilt für das theoretische Konzept von Hashes und Mappings, ist jedoch für Caches mit Wörterbüchern als Lookups nicht praktikabel. Es ist nicht ungewöhnlich, dass Wörterbücher mit ähnlichen Schlüsseln, aber unterschiedlichen Werten an eine zwischengespeicherte Funktion übergeben werden. In diesem Fall wird der Cache praktisch zu einer Liste anstelle einer Zuordnung, wenn nur die Schlüssel zum Erstellen eines Hashs verwendet werden.
Oben Sonne

3
Im speziellen Fall von Diktaten mit indentischen Schlüsseln und unterschiedlichen Werten ist es besser, nur einen Hash basierend auf zu speichern frozenset(d.itervalues()). In Fällen, in denen Diktate unterschiedliche Schlüssel haben, frozenset(d)ist dies viel schneller und schränkt die Hashfähigkeit von Schlüsseln nicht ein. Denken Sie zum Schluss daran, dass die Methode dict .__ eq__ viel schneller nach gleichen Schlüssel / Wert-Paaren sucht, als der Hash für alle Schlüssel / Wert-Paar-Tupel berechnet werden kann. Die Verwendung von Schlüssel- / Wertetupeln ist ebenfalls problematisch, da die gespeicherten Hashes für alle Schlüssel weggeworfen werden (deshalb frozenset(d)ist dies so schnell).
Raymond Hettinger

11

Eine einigermaßen saubere, unkomplizierte Implementierung ist

import collections

class FrozenDict(collections.Mapping):
    """Don't forget the docstrings!!"""

    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __getitem__(self, key):
        return self._d[key]

    def __hash__(self):
        return hash(tuple(sorted(self._d.iteritems())))

Warum ist es so vernünftig, sauber und unkompliziert? Dh bitte erklären Sie die Unterschiede zu anderen Antworten, zB die Notwendigkeit von __iter__und __len__.
Karl Richter

1
@KarlRichter, ich habe nie gesagt, dass es vernünftig ist, nur einigermaßen sauber. ;)
Mike Graham

@KarlRichter, ich definiere __iter__und __len__weil ich muss, da ich ableite collections.Mapping; Wie man es benutzt, collections.Mappingwird in der Dokumentation des Sammlungsmoduls ziemlich gut behandelt. Andere Menschen haben kein Bedürfnis danach, da sie ableiten dict. Aus dicteinem anderen Grund abzuleiten als zu definieren, __missing__ist eine schlechte Idee. Die Diktatspezifikation sagt nicht aus, wie Diktat in einem solchen Fall funktioniert, und in Wirklichkeit wird dies Tonnen von nicht virtuellen Methoden enthalten, die im Allgemeinen weniger nützlich sind und in diesem speziellen Fall Restmethoden mit irrelevantem Verhalten haben.
Mike Graham

7

Ich komme immer wieder auf dieses Thema zurück ... Hier ist eine weitere Variante. Es ist mir unangenehm, eine Unterklasse dicthinzuzufügen, um eine __hash__Methode hinzuzufügen . Es gibt praktisch kein Entrinnen vor dem Problem, dass Diktate veränderlich sind, und das Vertrauen, dass sie sich nicht ändern, scheint eine schwache Idee zu sein. Also habe ich stattdessen versucht, ein Mapping basierend auf einem eingebauten Typ zu erstellen, der selbst unveränderlich ist. obwohltuple eine naheliegende Wahl ist, impliziert der Zugriff auf Werte eine Sortierung und eine Halbierung. Kein Problem, aber es scheint nicht viel von der Kraft des Typs zu nutzen, auf dem es aufgebaut ist.

Was ist, wenn Sie einen Schlüssel blockieren und Wertepaare in a frozenset ? Was würde das erfordern, wie würde es funktionieren?

Teil 1, Sie benötigen eine Möglichkeit, die Elemente so zu codieren, dass ein Frozenset sie hauptsächlich anhand ihrer Schlüssel behandelt. Ich werde eine kleine Unterklasse dafür machen.

import collections
class pair(collections.namedtuple('pair_base', 'key value')):
    def __hash__(self):
        return hash((self.key, None))
    def __eq__(self, other):
        if type(self) != type(other):
            return NotImplemented
        return self.key == other.key
    def __repr__(self):
        return repr((self.key, self.value))

Das allein bringt Sie in die Nähe einer unveränderlichen Zuordnung:

>>> frozenset(pair(k, v) for k, v in enumerate('abcd'))
frozenset([(0, 'a'), (2, 'c'), (1, 'b'), (3, 'd')])
>>> pairs = frozenset(pair(k, v) for k, v in enumerate('abcd'))
>>> pair(2, None) in pairs
True
>>> pair(5, None) in pairs
False
>>> goal = frozenset((pair(2, None),))
>>> pairs & goal
frozenset([(2, None)])

D'oh! Wenn Sie die Set-Operatoren verwenden, sind die Elemente leider gleich, aber nicht dasselbe Objekt. Was im Rückgabewert endet, ist undefiniert . Wir müssen noch einige Drehungen durchlaufen.

>>> pairs - (pairs - goal)
frozenset([(2, 'c')])
>>> iter(pairs - (pairs - goal)).next().value
'c'

Das Nachschlagen von Werten auf diese Weise ist jedoch umständlich und führt schlimmer noch zu vielen Zwischensätzen. das geht nicht! Wir werden ein 'falsches' Schlüssel-Wert-Paar erstellen, um es zu umgehen:

class Thief(object):
    def __init__(self, key):
        self.key = key
    def __hash__(self):
        return hash(pair(self.key, None))
    def __eq__(self, other):
        self.value = other.value
        return pair(self.key, None) == other

Was zu weniger problematischen Ergebnissen führt:

>>> thief = Thief(2)
>>> thief in pairs
True
>>> thief.value
'c'

Das ist die ganze tiefe Magie; Der Rest packt alles in etwas ein, das eine Schnittstelle wie ein Diktat hat. Da wir eine Unterklasse von haben frozenset, die eine ganz andere Oberfläche hat, gibt es ziemlich viele Methoden; Wir bekommen ein wenig Hilfe von collections.Mapping, aber der größte Teil der Arbeit überschreibt frozensetstattdessen die Methoden für Versionen, die wie Diktate funktionieren:

class FrozenDict(frozenset, collections.Mapping):
    def __new__(cls, seq=()):
        return frozenset.__new__(cls, (pair(k, v) for k, v in seq))
    def __getitem__(self, key):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        raise KeyError(key)
    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            return dict(self.iteritems()) == other
        if len(self) != len(other):
            return False
        for key, value in self.iteritems():
            try:
                if value != other[key]:
                    return False
            except KeyError:
                return False
        return True
    def __hash__(self):
        return hash(frozenset(self.iteritems()))
    def get(self, key, default=None):
        thief = Thief(key)
        if frozenset.__contains__(self, thief):
            return thief.value
        return default
    def __iter__(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def iteritems(self):
        for item in frozenset.__iter__(self):
            yield (item.key, item.value)
    def iterkeys(self):
        for item in frozenset.__iter__(self):
            yield item.key
    def itervalues(self):
        for item in frozenset.__iter__(self):
            yield item.value
    def __contains__(self, key):
        return frozenset.__contains__(self, pair(key, None))
    has_key = __contains__
    def __repr__(self):
        return type(self).__name__ + (', '.join(repr(item) for item in self.iteritems())).join('()')
    @classmethod
    def fromkeys(cls, keys, value=None):
        return cls((key, value) for key in keys)

was letztendlich meine eigene Frage beantwortet:

>>> myDict = {}
>>> myDict[FrozenDict(enumerate('ab'))] = 5
>>> FrozenDict(enumerate('ab')) in myDict
True
>>> FrozenDict(enumerate('bc')) in myDict
False
>>> FrozenDict(enumerate('ab', 3)) in myDict
False
>>> myDict[FrozenDict(enumerate('ab'))]
5

5

Die akzeptierte Antwort von @Unknown sowie die Antwort von @AlexMartelli funktionieren einwandfrei, jedoch nur unter den folgenden Einschränkungen:

  1. Die Werte des Wörterbuchs müssen hashbar sein. Zum Beispiel hash(hashabledict({'a':[1,2]}))wird erhöhenTypeError .
  2. Schlüssel müssen den Vergleichsvorgang unterstützen. Zum Beispiel hash(hashabledict({'a':'a', 1:1}))wird erhöhen TypeError.
  3. Der Vergleichsoperator für Schlüssel legt die Gesamtbestellung fest. Wenn beispielsweise die beiden Schlüssel in einem Wörterbuch frozenset((1,2,3))und sind frozenset((4,5,6)), werden sie in beide Richtungen ungleich verglichen. Das Sortieren der Elemente eines Wörterbuchs mit solchen Schlüsseln kann daher zu einer beliebigen Reihenfolge führen und verstößt daher gegen die Regel, dass gleiche Objekte denselben Hashwert haben müssen.

Die viel schnellere Antwort von @ObenSonne hebt die Einschränkungen 2 und 3 auf, ist jedoch weiterhin an die Einschränkung 1 gebunden (Werte müssen hashbar sein).

Die schnellere und dennoch antwortende Antwort von @RaymondHettinger hebt alle drei Einschränkungen auf, da sie nicht .values()in die Hash-Berechnung einbezogen wird. Die Leistung ist jedoch nur dann gut, wenn:

  1. Die meisten (ungleichen) Wörterbücher, die gehasht werden müssen, sind nicht identisch .keys().

Wenn diese Bedingung nicht erfüllt ist, ist die Hash-Funktion weiterhin gültig, kann jedoch zu viele Kollisionen verursachen. Im Extremfall, in dem alle Wörterbücher aus einer Website-Vorlage generiert werden (Feldnamen als Schlüssel, Benutzereingabe als Werte), sind die Schlüssel immer gleich und die Hash-Funktion gibt für alle Eingaben den gleichen Wert zurück . Infolgedessen wird eine Hashtabelle, die auf einer solchen Hash-Funktion basiert, beim Abrufen eines Elements ( O(N)anstelle von O(1)) so langsam wie eine Liste .

Ich denke, die folgende Lösung wird einigermaßen gut funktionieren, selbst wenn alle 4 oben aufgeführten Einschränkungen verletzt werden. Es hat den zusätzlichen Vorteil, dass es nicht nur Wörterbücher, sondern auch alle Container hashen kann, selbst wenn sie veränderbare veränderbare Container haben.

Ich würde mich über jedes Feedback sehr freuen, da ich dies bisher nur leicht getestet habe.

# python 3.4
import collections
import operator
import sys
import itertools
import reprlib

# a wrapper to make an object hashable, while preserving equality
class AutoHash:
    # for each known container type, we can optionally provide a tuple
    # specifying: type, transform, aggregator
    # even immutable types need to be included, since their items
    # may make them unhashable

    # transformation may be used to enforce the desired iteration
    # the result of a transformation must be an iterable
    # default: no change; for dictionaries, we use .items() to see values

    # usually transformation choice only affects efficiency, not correctness

    # aggregator is the function that combines all items into one object
    # default: frozenset; for ordered containers, we can use tuple

    # aggregator choice affects both efficiency and correctness
    # e.g., using a tuple aggregator for a set is incorrect,
    # since identical sets may end up with different hash values
    # frozenset is safe since at worst it just causes more collisions
    # unfortunately, no collections.ABC class is available that helps
    # distinguish ordered from unordered containers
    # so we need to just list them out manually as needed

    type_info = collections.namedtuple(
        'type_info',
        'type transformation aggregator')

    ident = lambda x: x
    # order matters; first match is used to handle a datatype
    known_types = (
        # dict also handles defaultdict
        type_info(dict, lambda d: d.items(), frozenset), 
        # no need to include set and frozenset, since they are fine with defaults
        type_info(collections.OrderedDict, ident, tuple),
        type_info(list, ident, tuple),
        type_info(tuple, ident, tuple),
        type_info(collections.deque, ident, tuple),
        type_info(collections.Iterable, ident, frozenset) # other iterables
    )

    # hash_func can be set to replace the built-in hash function
    # cache can be turned on; if it is, cycles will be detected,
    # otherwise cycles in a data structure will cause failure
    def __init__(self, data, hash_func=hash, cache=False, verbose=False):
        self._data=data
        self.hash_func=hash_func
        self.verbose=verbose
        self.cache=cache
        # cache objects' hashes for performance and to deal with cycles
        if self.cache:
            self.seen={}

    def hash_ex(self, o):
        # note: isinstance(o, Hashable) won't check inner types
        try:
            if self.verbose:
                print(type(o),
                    reprlib.repr(o),
                    self.hash_func(o),
                    file=sys.stderr)
            return self.hash_func(o)
        except TypeError:
            pass

        # we let built-in hash decide if the hash value is worth caching
        # so we don't cache the built-in hash results
        if self.cache and id(o) in self.seen:
            return self.seen[id(o)][0] # found in cache

        # check if o can be handled by decomposing it into components
        for typ, transformation, aggregator in AutoHash.known_types:
            if isinstance(o, typ):
                # another option is:
                # result = reduce(operator.xor, map(_hash_ex, handler(o)))
                # but collisions are more likely with xor than with frozenset
                # e.g. hash_ex([1,2,3,4])==0 with xor

                try:
                    # try to frozenset the actual components, it's faster
                    h = self.hash_func(aggregator(transformation(o)))
                except TypeError:
                    # components not hashable with built-in;
                    # apply our extended hash function to them
                    h = self.hash_func(aggregator(map(self.hash_ex, transformation(o))))
                if self.cache:
                    # storing the object too, otherwise memory location will be reused
                    self.seen[id(o)] = (h, o)
                if self.verbose:
                    print(type(o), reprlib.repr(o), h, file=sys.stderr)
                return h

        raise TypeError('Object {} of type {} not hashable'.format(repr(o), type(o)))

    def __hash__(self):
        return self.hash_ex(self._data)

    def __eq__(self, other):
        # short circuit to save time
        if self is other:
            return True

        # 1) type(self) a proper subclass of type(other) => self.__eq__ will be called first
        # 2) any other situation => lhs.__eq__ will be called first

        # case 1. one side is a subclass of the other, and AutoHash.__eq__ is not overridden in either
        # => the subclass instance's __eq__ is called first, and we should compare self._data and other._data
        # case 2. neither side is a subclass of the other; self is lhs
        # => we can't compare to another type; we should let the other side decide what to do, return NotImplemented
        # case 3. neither side is a subclass of the other; self is rhs
        # => we can't compare to another type, and the other side already tried and failed;
        # we should return False, but NotImplemented will have the same effect
        # any other case: we won't reach the __eq__ code in this class, no need to worry about it

        if isinstance(self, type(other)): # identifies case 1
            return self._data == other._data
        else: # identifies cases 2 and 3
            return NotImplemented

d1 = {'a':[1,2], 2:{3:4}}
print(hash(AutoHash(d1, cache=True, verbose=True)))

d = AutoHash(dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars'),cache=True, verbose=True)
print(hash(d))

2

Möglicherweise möchten Sie auch diese beiden Methoden hinzufügen, damit das v2-Beizprotokoll mit Hashdict-Instanzen funktioniert. Andernfalls versucht cPickle, hashdict .____ setitem____ zu verwenden, was zu einem TypeError führt. Interessanterweise funktioniert Ihr Code mit den beiden anderen Versionen des Protokolls einwandfrei.

def __setstate__(self, objstate):
    for k,v in objstate.items():
        dict.__setitem__(self,k,v)
def __reduce__(self):
    return (hashdict, (), dict(self),)

-2

Wenn Sie keine Zahlen in das Wörterbuch aufnehmen und die Variablen mit Ihren Wörterbüchern nie verlieren, können Sie Folgendes tun:

cache[id(rule)] = "whatever"

da id () für jedes Wörterbuch eindeutig ist

BEARBEITEN:

Oh sorry, ja in diesem Fall wäre das, was die anderen sagten, besser. Ich denke, Sie könnten Ihre Wörterbücher auch als Zeichenfolge serialisieren

cache[ 'foo:bar' ] = 'baz'

Wenn Sie Ihre Wörterbücher jedoch von den Schlüsseln wiederherstellen müssen, müssen Sie etwas Hässlicheres tun

cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )

Ich denke, der Vorteil davon ist, dass Sie nicht so viel Code schreiben müssten.


Hmmm, nein; das ist nicht was ich suche:cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache Fähigkeit, Schlüssel dynamisch zu erstellen, ist wichtig, wenn ich überhaupt Diktate als Schlüssel verwenden möchte.
SingleNegationElimination

1
Die Serialisierung der Diktate ist möglicherweise in Ordnung. Haben Sie eine Empfehlung für eine Serialisierung? das ist was ich suche.
SingleNegationElimination
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.