Dict hashable di Python


92

Come esercizio, e principalmente per il mio divertimento, sto implementando un parser packrat di backtracking. L'ispirazione per questo è che mi piacerebbe avere un'idea migliore di come le macro igieniche funzionerebbero in un linguaggio simile ad algol (come in contrasto con i dialetti lisp privi di sintassi in cui li trovi normalmente). Per questo motivo, diversi passaggi attraverso l'input potrebbero visualizzare grammatiche diverse, quindi i risultati dell'analisi memorizzati nella cache non sono validi, a meno che non archivi anche la versione corrente della grammatica insieme ai risultati dell'analisi memorizzati nella cache. ( EDIT : una conseguenza di questo utilizzo delle raccolte di valori-chiave è che dovrebbero essere immutabili, ma non intendo esporre l'interfaccia per consentire la modifica, quindi le raccolte modificabili o immutabili vanno bene)

Il problema è che i dict di Python non possono apparire come chiavi per altri dict. Anche l'uso di una tupla (come farei comunque) non aiuta.

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

Immagino che debbano essere tuple fino in fondo. Ora la libreria standard di Python fornisce approssimativamente ciò di cui avrei bisogno, collections.namedtupleha una sintassi molto diversa, ma può essere utilizzata come chiave. continuando dalla sessione precedente:

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

Ok. Ma devo creare una classe per ogni possibile combinazione di chiavi nella regola che vorrei usare, il che non è poi così male, perché ogni regola di analisi sa esattamente quali parametri utilizza, in modo che quella classe possa essere definita allo stesso tempo come la funzione che analizza la regola.

Modifica: un ulteriore problema con namedtuples è che sono strettamente posizionali. Due tuple che sembrano dovrebbero essere diverse possono infatti essere le stesse:

>>> 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: Come ottengo dicti messaggi che possono essere usati come chiavi per altri messaggi dict?

Dopo aver modificato un po 'le risposte, ecco la soluzione più completa che sto usando. Nota che questo fa un po 'di lavoro extra per rendere i dict risultanti vagamente immutabili per scopi pratici. Ovviamente è ancora abbastanza facile aggirarlo chiamando, dict.__setitem__(instance, key, value)ma qui siamo tutti adulti.

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

Il hashdictdeve essere immutabile, almeno dopo aver avviato l'hashing, quindi perché non memorizzare nella cache i valori keye hashcome attributi hashdictdell'oggetto? Ho modificato __key()e __hash__(), e testato per confermare che è molto più veloce. SO non consente il codice formattato nei commenti, quindi lo collegherò qui: sam.aiki.info/hashdict.py
Sam Watkins

Risposte:


68

Ecco il modo più semplice per creare un dizionario modificabile. Ricorda solo di non modificarli dopo averli incorporati in un altro dizionario per ovvie ragioni.

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

7
Ciò non garantisce in modo netto la coerenza di eq e hash mentre la mia risposta precedente lo fa attraverso l'uso del metodo __key (in pratica entrambi gli approcci dovrebbero funzionare, sebbene questo potrebbe essere rallentato creando un elenco itermediat non necessario - risolvibile da s / elementi / iteritems / - assumendo Python 2. * come non dici ;-).
Alex Martelli

5
Probabilmente sarebbe meglio usare solo un frozenset piuttosto che una tupla con l'ordinamento. Non solo sarebbe più veloce, ma non puoi presumere che le chiavi del dizionario siano confrontabili.
asmeurer

1
Sembra che ci dovrebbe essere un modo per evitare una funzione hash che è O(n*log(n))dove si ntrova il numero di dictvoci. Qualcuno sa se la frozensetfunzione hash di Python viene eseguita in tempo lineare?
Tom Karzes,

2
@HelloGoodbye Un dict può anche essere creato come questo dict(key1=value1, key2=value2,...)o questo dict([(key1, value1), (key2, value2),...)]). Lo stesso vale per questo. La creazione che hai pubblicato è chiamata letterale
smido

2
@smido: grazie. Ho anche scoperto che puoi semplicemente lanciare un letterale, ie hashabledict({key_a: val_a, key_b: val_b, ...}).
Ciao arrivederci

62

Gli hashables dovrebbero essere immutabili - non imponendolo ma FIDANDOSI di non modificare un dict dopo il suo primo utilizzo come chiave, il seguente approccio funzionerebbe:

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

Se hai bisogno di modificare i tuoi dettami e ANCORA vuoi usarli come chiavi, la complessità esplode centinaia di volte - per non dire che non può essere fatto, ma aspetterò fino a un'indicazione MOLTO specifica prima di entrare in QUELLA palude incredibile! -)


Certamente non voglio modificare i dict una volta che sono stati preparati. Ciò farebbe crollare il resto dell'algoritmo di Packrad.
SingleNegationElimination

Quindi la sottoclasse che ho suggerito funzionerà - nota come aggira il problema "posizionale" ( prima di aver modificato la tua domanda per evidenziarla ;-) con il sortedtasto in __ ;-).
Alex Martelli

Il comportamento dipendente dalla posizione di namedtuple mi ha sorpreso. Ci stavo giocando, pensando che potesse essere ancora un modo più semplice per risolvere il problema, ma questo ha praticamente deluso tutte le mie speranze (e richiederà un rollback :()
SingleNegationElimination

Diciamo che ho un dict e voglio trasmetterlo a un hashabledict. Come potrei farlo?
jononomo


32

Tutto ciò che serve per rendere i dizionari utilizzabili per il tuo scopo è aggiungere un metodo __hash__:

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

Nota, la conversione frozenset funzionerà per tutti i dizionari (cioè non richiede che le chiavi siano ordinabili). Allo stesso modo, non ci sono restrizioni sui valori del dizionario.

Se ci sono molti dizionari con chiavi identiche ma con valori distinti, è necessario che l'hash tenga conto dei valori. Il modo più veloce per farlo è:

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

Questo è più veloce che frozenset(self.iteritems())per due motivi. Innanzitutto, il frozenset(self)passaggio riutilizza i valori hash memorizzati nel dizionario, salvando chiamate non necessarie a hash(key). In secondo luogo, l'utilizzo di itervalues accederà direttamente ai valori ed eviterà le numerose chiamate di allocatori di memoria che utilizzano by items per formare nuove molte tuple chiave / valore in memoria ogni volta che si esegue una ricerca.


@ RaymondHettinger Correggimi se sbaglio, ma pensavo di dictnon memorizzare nella cache i valori hash delle sue chiavi, sebbene le singole classi (come str) possano e scelgano di memorizzare nella cache i propri hash. Almeno quando ho creato un dictcon le mie istanze di classe personalizzate usate come chiavi, i loro __hash__metodi sono stati chiamati ad ogni operazione di accesso (python 3.4). Che io sia corretto o meno, non sono sicuro di come hash(frozenset(self))riutilizzare i valori hash precalcolati, a meno che non siano memorizzati nella cache all'interno delle chiavi stesse (nel qual caso, anche hash(frozenset(self.items())riutilizzarli).
max

Per quanto riguarda il tuo secondo punto sulla creazione di tuple (chiave / valore), ho pensato che i metodi .items () restituissero una vista piuttosto che un elenco di tuple e che la creazione di quella vista non implicasse la copia delle chiavi e dei valori sottostanti. (Python 3.4 di nuovo.) Detto questo, vedo il vantaggio di eseguire l'hashing solo delle chiavi se la maggior parte degli input ha chiavi diverse, perché (1) è piuttosto costoso eseguire l'hash dei valori e (2) è piuttosto restrittivo richiedere che i valori siano hash
max

6
Questo ha anche la possibilità di creare lo stesso hash per due dizionari diversi. Considera {'one': 1, 'two': 2}e{'one': 2, 'two': 1}
AgDude

Mike Graham nel suo commento afferma che Deriving dict per qualsiasi altro motivo, ma definire __missing__è una cattiva idea. Cosa ne pensi?
Piotr Dobrogost

1
La sottoclasse da dict è stata ben definita a partire da Python 2.2. Vedi collections.OrderedDict e collections.Counter per esempi dalla libreria standard Python. L'altro commento si basa sulla convinzione infondata che solo le sottoclassi di MutableMapping siano ben definite.
Raymond Hettinger

23

Le risposte fornite vanno bene, ma potrebbero essere migliorate utilizzando frozenset(...)invece di tuple(sorted(...))generare l'hash:

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

Il vantaggio in termini di prestazioni dipende dal contenuto del dizionario, ma nella maggior parte dei casi che ho testato, l'hashing con frozensetè almeno 2 volte più veloce (principalmente perché non ha bisogno di ordinare).


1
Nota, non è necessario includere sia le chiavi che i valori. Questa soluzione sarebbe molto più veloce come: hash(frozenset(d)).
Raymond Hettinger

10
@RaymondHettinger: hash(frozenset(d))produce hash identici per 2 dict con chiavi simili ma valori diversi!
Oben Sonne

4
Non è un problema. È compito di __eq__ distinguere tra dict di valori diversi. Il compito di __hash__ è semplicemente quello di ridurre lo spazio di ricerca.
Raymond Hettinger

5
Questo è vero per il concetto teorico di hash e mappature, ma non pratico per le cache con dizionari come ricerche: non è raro che dizionari con chiavi simili ma valori diversi vengano passati a una funzione memorizzata nella cache. In quel caso la cache si trasforma praticamente in una lista invece che in una mappatura se solo le chiavi sono usate per costruire un hash.
Oben Sonne

3
Nel caso speciale di dict con chiavi indentiche e valori distinti, faresti meglio a memorizzare un hash basato su frozenset(d.itervalues()). Nei casi in cui i dict hanno chiavi distinte, frozenset(d)è molto più veloce e non impone restrizioni sull'habilità delle chiavi. Infine, ricorda che il metodo dict .__ eq__ verificherà la presenza di coppie chiave / valore uguali molto più rapidamente che qualsiasi cosa possa calcolare l'hash per tutte le tuple di coppie chiave / valore. Anche l'uso di tuple chiave / valore è problematico perché elimina gli hash memorizzati per tutte le chiavi (ecco perché frozenset(d)è così veloce).
Raymond Hettinger

11

Un'implementazione ragionevolmente pulita e semplice è

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

Perché è così ragionevole, pulito e diretto? Cioè per favore spiega le differenze con altre risposte, ad esempio la necessità di __iter__e __len__.
Karl Richter

1
@KarlRichter, non ho mai detto che fosse ragionevole, solo ragionevolmente pulito. ;)
Mike Graham

@KarlRichter, definisco __iter__e __len__perché devo, visto che sto derivando collections.Mapping; come usarlo collections.Mappingè trattato abbastanza bene nella documentazione del modulo collezioni. Altre persone non ne sentono il bisogno poiché derivano dict. Derivare dictper qualsiasi altra ragione, ma per definire __missing__è una cattiva idea. Le specifiche di dict non dicono come funziona dict in un caso del genere, e in realtà questo finirà per avere tonnellate di metodi non virtuali che sono meno utili in generale e in questo caso particolare avranno metodi rudimentali con comportamenti irrilevanti.
Mike Graham

7

Continuo a tornare su questo argomento ... Ecco un'altra variazione. Sono a disagio con la sottoclasse dictper aggiungere un __hash__metodo; Praticamente non c'è via di scampo dal problema che i dict sono mutabili e fidarsi che non cambieranno sembra un'idea debole. Quindi ho invece cercato di costruire una mappatura basata su un tipo incorporato che è di per sé immutabile. sebbene tuplesia una scelta ovvia, l'accesso ai valori in esso implica un ordinamento e una bisetta; non è un problema, ma sembra che non stia sfruttando gran parte della potenza del tipo su cui è costruito.

Cosa succede se inceppi le coppie chiave e valore in una frozenset? Cosa richiederebbe, come funzionerebbe?

Parte 1, hai bisogno di un modo per codificare gli elementi in modo tale che un frozenset li tratti principalmente dalle loro chiavi; Creerò una piccola sottoclasse per questo.

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

Questo da solo ti mette a distanza di una mappatura immutabile:

>>> 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! Sfortunatamente, quando usi gli operatori di gruppo e gli elementi sono uguali ma non lo stesso oggetto; quale va a finire nel valore di ritorno non è definito , dovremo eseguire altre rotazioni.

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

Tuttavia, cercare i valori in questo modo è complicato e, peggio, crea molti insiemi intermedi; non va bene! Creeremo una coppia chiave-valore "falsa" per aggirare il problema:

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

Il che si traduce nel meno problematico:

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

Questa è tutta la magia profonda; il resto sta avvolgendo tutto in qualcosa che ha un'interfaccia come un dict. Dal momento che stiamo creando sottoclassi da frozenset, che ha un'interfaccia molto diversa, ci sono molti metodi; otteniamo un piccolo aiuto da collections.Mapping, ma la maggior parte del lavoro sta sovrascrivendo i frozensetmetodi per le versioni che funzionano come dict, invece:

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)

che, in definitiva, risponde alla mia stessa domanda:

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

La risposta accettata da @Unknown, così come la risposta da @AlexMartelli funzionano perfettamente, ma solo con i seguenti vincoli:

  1. I valori del dizionario devono essere modificabili. Ad esempio, hash(hashabledict({'a':[1,2]}))rilancerà TypeError.
  2. Le chiavi devono supportare l'operazione di confronto. Ad esempio, hash(hashabledict({'a':'a', 1:1}))rilancerà TypeError.
  3. L'operatore di confronto sui tasti impone l'ordinamento totale. Ad esempio, se le due chiavi in ​​un dizionario sono frozenset((1,2,3))e frozenset((4,5,6)), il confronto non è uguale in entrambe le direzioni. Pertanto, l'ordinamento degli elementi di un dizionario con tali chiavi può risultare in un ordine arbitrario e pertanto violerà la regola che gli oggetti uguali devono avere lo stesso valore hash.

La risposta molto più rapida di @ObenSonne solleva i vincoli 2 e 3, ma è ancora vincolata dal vincolo 1 (i valori devono essere modificabili).

La risposta più veloce di @RaymondHettinger solleva tutti e 3 i vincoli perché non include .values()nel calcolo dell'hash. Tuttavia, le sue prestazioni sono buone solo se:

  1. La maggior parte dei dizionari (non uguali) che devono essere sottoposti a hashing non sono identici .keys().

Se questa condizione non è soddisfatta, la funzione hash sarà comunque valida, ma potrebbe causare troppe collisioni. Ad esempio, nel caso estremo in cui tutti i dizionari sono generati da un modello di sito web (nomi di campo come chiavi, input dell'utente come valori), le chiavi saranno sempre le stesse e la funzione hash restituirà lo stesso valore per tutti gli input . Di conseguenza, una tabella hash che si basa su una tale funzione hash diventerà lenta come un elenco durante il recupero di un elemento ( O(N)invece di O(1)).

Penso che la seguente soluzione funzionerà ragionevolmente bene anche se tutti e 4 i vincoli che ho elencato sopra vengono violati. Ha un ulteriore vantaggio che può eseguire l'hashing non solo dei dizionari, ma di tutti i contenitori, anche se hanno contenitori mutabili annidati.

Apprezzerei molto qualsiasi feedback su questo, dato che finora l'ho provato solo leggermente.

# 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

Potresti anche voler aggiungere questi due metodi per far funzionare il protocollo di decapaggio v2 con le istanze hashdict. Altrimenti cPickle proverà a usare hashdict .____ setitem____ risultando in un TypeError. È interessante notare che con le altre due versioni del protocollo il tuo codice funziona perfettamente.

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

-2

Se non metti numeri nel dizionario e non perdi mai le variabili che contengono i dizionari, puoi farlo:

cache[id(rule)] = "whatever"

poiché id () è unico per ogni dizionario

MODIFICARE:

Oh scusa, sì in quel caso quello che hanno detto gli altri ragazzi sarebbe stato meglio. Penso che potresti anche serializzare i tuoi dizionari come una stringa, come

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

Se hai bisogno di recuperare i tuoi dizionari dalle chiavi, allora dovresti fare qualcosa di più brutto come

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

Immagino che il vantaggio di questo sia che non dovresti scrivere tanto codice.


Hmmm, no; questo non è quello che sto cercando: cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cacheessere in grado di creare dinamicamente chiavi è importante per quando voglio utilizzare i dict come chiavi in ​​primo luogo.
SingleNegationElimination

1
Serializzare i dict potrebbe essere ok, hai un consiglio su un modo per serializzarli? è quello che sto cercando.
SingleNegationElimination
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.