Dichos hash de Python

94

Como ejercicio, y sobre todo para mi propia diversión, estoy implementando un analizador packrat de retroceso. La inspiración para esto es que me gustaría tener una mejor idea sobre cómo funcionarían las macros higiénicas en un lenguaje similar al algol (en oposición a los dialectos lisp sin sintaxis en los que normalmente se encuentran). Debido a esto, diferentes pases a través de la entrada pueden ver diferentes gramáticas, por lo que los resultados del análisis en caché no son válidos, a menos que también almacene la versión actual de la gramática junto con los resultados del análisis en caché. ( EDITAR : una consecuencia de este uso de colecciones de valores clave es que deberían ser inmutables, pero no tengo la intención de exponer la interfaz para permitir que se cambien, por lo que las colecciones mutables o inmutables están bien)

El problema es que los dictados de Python no pueden aparecer como claves para otros dictados. Incluso usar una tupla (como estaría haciendo de todos modos) no ayuda.

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

Supongo que tiene que ser tuplas hasta el final. Ahora, la biblioteca estándar de Python proporciona aproximadamente lo que necesitaría, collections.namedtupletiene una sintaxis muy diferente, pero se puede usar como clave. Continuando desde la sesión anterior:

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

Okay. Pero tengo que hacer una clase para cada combinación posible de claves en la regla que me gustaría usar, lo cual no es tan malo, porque cada regla de análisis sabe exactamente qué parámetros usa, por lo que esa clase se puede definir al mismo tiempo como la función que analiza la regla.

Editar: Un problema adicional con namedtuples es que son estrictamente posicionales. De hecho, dos tuplas que parecen ser diferentes pueden ser iguales:

>>> 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: ¿Cómo obtengo mensajes de correo electrónico dictque se pueden usar como claves para otros mensajes de correo electrónico dict?

Habiendo pirateado un poco las respuestas, esta es la solución más completa que estoy usando. Tenga en cuenta que esto hace un poco más de trabajo para hacer que los dictados resultantes sean vagamente inmutables para fines prácticos. Por supuesto, todavía es bastante fácil solucionarlo llamando, dict.__setitem__(instance, key, value)pero todos somos adultos aquí.

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()
SingleNegationElimination
fuente
El hashdictdebe ser inmutable, al menos después de comenzar a aplicar el hash, entonces, ¿por qué no almacenar en caché los valores keyy hashcomo atributos del hashdictobjeto? Modifiqué __key()y __hash__()probé para confirmar que es mucho más rápido. SO no permite código formateado en los comentarios, así que lo vincularé aquí: sam.aiki.info/hashdict.py
Sam Watkins

Respuestas:

71

Esta es la manera fácil de hacer un diccionario hash. Solo recuerde no modificarlos después de incrustarlos en otro diccionario por razones obvias.

class hashabledict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.items())))
Desconocido
fuente
7
Esto no asegura claramente la consistencia de eq y hash, mientras que mi respuesta anterior lo hace mediante el uso del método __key (en la práctica, cualquiera de los enfoques debería funcionar, aunque este podría ralentizarse al hacer una lista inmediata innecesaria, que se puede arreglar por s / items / iteritems / - asumiendo Python 2. * como no dices ;-).
Alex Martelli
5
Probablemente sería mejor usar un conjunto congelado en lugar de una tupla con clasificación. Esto no solo sería más rápido, sino que no puede asumir que las claves del diccionario son comparables.
asmeurer
1
Parece que debería haber una forma de evitar una función hash que es O(n*log(n))donde nestá el número de dictentradas. ¿Alguien sabe si la frozensetfunción hash de Python se ejecuta en tiempo lineal?
Tom Karzes
2
@HelloGoodbye Un dictado también se puede crear como este dict(key1=value1, key2=value2,...)o este dict([(key1, value1), (key2, value2),...)]). Lo mismo se aplica a este. La creación que publicaste se llama literal
smido
2
@smido: Gracias. También descubrí que puedes lanzar un literal, es decir hashabledict({key_a: val_a, key_b: val_b, ...}).
Hola
62

Los hashables deben ser inmutables, no imponiendo esto, pero CONFIANDO en que no mutes un dict después de su primer uso como clave, el siguiente enfoque funcionaría:

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

Si necesita mutar sus dictados y TODAVÍA quiere usarlos como teclas, la complejidad explota cien veces, por no decir que no se puede hacer, ¡pero esperaré hasta una indicación MUY específica antes de entrar en ESE increíble pantano! -)

Alex Martelli
fuente
Ciertamente no quiero modificar los dictados una vez que se hayan preparado. Eso haría que el resto del algoritmo packrad se derrumbara.
SingleNegationElimination
Entonces, la subclase que sugerí funcionará; observe cómo pasa por alto el problema "posicional" ( antes de haber editado su pregunta para señalarla ;-) con la sortedtecla __key ;-).
Alex Martelli
El comportamiento dependiente de la posición de namedtuple me sorprendió muchísimo. Había estado jugando con él, pensando que aún podría ser una forma más fácil de resolver el problema, pero eso acabó con todas mis esperanzas (y requerirá una reversión :()
SingleNegationElimination
Digamos que tengo un dict y quiero convertirlo en un hash. ¿Como podría hacerlo?
Jononomo
@JonCrowell vea estas preguntas para obtener ideas y aclaraciones: stackoverflow.com/questions/3464061/… , stackoverflow.com/questions/9112300/… , stackoverflow.com/questions/18020074/…
máximo
32

Todo lo que se necesita para que los diccionarios se puedan utilizar para su propósito es agregar un método __hash__:

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

Tenga en cuenta que la conversión frozenset funcionará para todos los diccionarios (es decir, no requiere que las claves se puedan ordenar). Del mismo modo, no hay restricción en los valores del diccionario.

Si hay muchos diccionarios con claves idénticas pero con valores distintos, es necesario que el hash tenga en cuenta los valores. La forma más rápida de hacerlo es:

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

Esto es más rápido que frozenset(self.iteritems())por dos razones. Primero, el frozenset(self)paso reutiliza los valores hash almacenados en el diccionario, ahorrando llamadas innecesarias a hash(key). En segundo lugar, el uso de itervalues accederá a los valores directamente y evitará las muchas llamadas al asignador de memoria que usan los elementos para formar nuevas tuplas clave / valor en la memoria cada vez que realice una búsqueda.

Raymond Hettinger
fuente
@RaymondHettinger Corrígeme si me equivoco, pero pensé que en dictsí mismo no almacena en caché los valores hash de sus claves, aunque las clases individuales (como str) pueden optar por almacenar en caché sus hash. Al menos cuando creé un dictcon mis instancias de clases personalizadas usadas como claves, sus __hash__métodos fueron llamados en cada operación de acceso (Python 3.4). Independientemente de si estoy en lo cierto o no, no estoy seguro de cómo hash(frozenset(self))puedo reutilizar los valores hash precalculados, a menos que estén almacenados en caché dentro de las claves (en cuyo caso, también los hash(frozenset(self.items())reutiliza).
máximo
En cuanto a su segundo punto sobre la creación de tuplas (clave / valor), pensé que los métodos .items () devuelven una vista en lugar de una lista de tuplas, y que la creación de esa vista no implica copiar las claves y valores subyacentes. (Python 3.4 de nuevo). Dicho esto, veo la ventaja de aplicar hash solo a las claves si la mayoría de las entradas tienen claves diferentes, porque (1) es bastante caro aplicar hash a los valores, y (2) es bastante restrictivo requerir que los valores sean hash
máximo
6
Esto también tiene la posibilidad de crear el mismo hash para dos diccionarios diferentes. Considere {'one': 1, 'two': 2}y{'one': 2, 'two': 1}
AgDude
Mike Graham en su comentario afirma que Derivar dictado por cualquier otra razón que no sea definir __missing__es una mala idea. ¿Qué piensas?
Piotr Dobrogost
1
La subclasificación de dict se ha definido bien desde Python 2.2. Consulte collections.OrderedDict y collections.Counter para ver ejemplos de la biblioteca estándar de Python. El otro comentario se basa en la creencia infundada de que solo las subclases de MutableMapping están bien definidas.
Raymond Hettinger
23

Las respuestas dadas están bien, pero podrían mejorarse usando en frozenset(...)lugar de tuple(sorted(...))generar el 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

La ventaja de rendimiento depende del contenido del diccionario, pero en la mayoría de los casos que he probado, el hash frozensetes al menos 2 veces más rápido (principalmente porque no es necesario ordenar).

Oben Sonne
fuente
1
Tenga en cuenta que no es necesario incluir las claves y los valores. Esta solución sería mucho más rápido a medida: hash(frozenset(d)).
Raymond Hettinger
10
@RaymondHettinger: hash(frozenset(d))da como resultado hash idénticos para 2 dictados con claves similares pero valores diferentes.
Oben Sonne
4
Eso no es un problema. Es el trabajo de __eq__ distinguir entre dictados de diferentes valores. El trabajo de __hash__ es simplemente reducir el espacio de búsqueda.
Raymond Hettinger
5
Eso es cierto para el concepto teórico de hashes y mapeos, pero no es práctico para cachés con diccionarios como búsquedas; no es raro que los diccionarios con claves similares pero valores diferentes se pasen a una función en memoria caché. En ese caso, la caché prácticamente se convierte en una lista en lugar de una asignación si solo se usan las claves para construir un hash.
Oben Sonne
3
En el caso especial de los dictados con claves idénticas y valores distintos, sería mejor almacenar un hash basado en frozenset(d.itervalues()). En los casos en que los dictados tienen claves distintas, frozenset(d)es mucho más rápido y no impone restricciones sobre la habilidad de las claves. Por último, recuerde que el método dict .__ eq__ buscará pares clave / valor iguales mucho más rápido que cualquier cosa puede calcular el hash para todas las tuplas de pares clave / valor. El uso de tuplas de clave / valor también es problemático porque elimina los hash almacenados para todas las claves (por eso frozenset(d)es tan rápido).
Raymond Hettinger
11

Una implementación razonablemente limpia y sencilla es

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())))
Mike Graham
fuente
¿Por qué es tan razonable, limpio y sencillo? Es decir, explique las diferencias con otras respuestas, por ejemplo, la necesidad de __iter__y __len__.
Karl Richter
1
@KarlRichter, nunca dije que fuera razonable, simplemente razonablemente limpio. ;)
Mike Graham
@KarlRichter, defino __iter__y __len__porque tengo que hacerlo, ya que estoy derivando collections.Mapping; cómo usarlo collections.Mappingse explica bastante bien en la documentación del módulo de colecciones. Otras personas no sienten la necesidad de hacerlo porque están derivando dict. Derivar dictpor cualquier otra razón que no sea definir __missing__es una mala idea. La especificación dict no dice cómo funciona dict en tal caso, y en realidad esto terminará teniendo toneladas de métodos no virtuales que son menos útiles en general y en este caso particular tendrán métodos vestigiales con comportamiento irrelevante.
Mike Graham
7

Sigo volviendo a este tema ... Aquí hay otra variación. Me siento incómodo con las subclases dictpara agregar un __hash__método; Prácticamente no hay escapatoria al problema de que los dict son mutables, y confiar en que no cambiarán parece una idea débil. Así que, en cambio, he mirado en construir un mapeo basado en un tipo incorporado que es en sí mismo inmutable. aunque tuplees una elección obvia, acceder a los valores en ella implica una especie y una bisección; no es un problema, pero no parece estar aprovechando gran parte del poder del tipo en el que está construido.

¿Qué pasa si atascas pares clave, valor en a frozenset? ¿Qué requeriría eso, cómo funcionaría?

Parte 1, necesita una forma de codificar los elementos de tal manera que un conjunto frozenset los trate principalmente por sus claves; Haré una pequeña subclase para eso.

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

Solo eso te pone a la distancia de un mapeo inmutable:

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

¡Oh! Desafortunadamente, cuando usa los operadores establecidos y los elementos son iguales pero no el mismo objeto; cuál termina en el valor de retorno no está definido , tendremos que pasar por algunos giros más.

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

Sin embargo, buscar valores de esta manera es engorroso y, lo que es peor, crea muchos conjuntos intermedios; eso no servirá! Crearemos un par clave-valor 'falso' para evitarlo:

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

Lo que resulta en lo menos problemático:

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

Esa es toda la magia profunda; el resto lo envuelve todo en algo que tiene una interfaz como un dictado. Dado que estamos subclasificando frozenset, que tiene una interfaz muy diferente, hay bastantes métodos; recibimos un poco de ayuda collections.Mapping, pero la mayor parte del trabajo está anulando elfrozenset métodos para las versiones que funcionan como dictados, en su lugar:

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)

que, en última instancia, responde a mi propia pregunta:

>>> 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
SingleNegationElimination
fuente
5

La respuesta aceptada por @Unknown, así como la respuesta por @AlexMartelli funcionan perfectamente bien, pero solo bajo las siguientes restricciones:

  1. Los valores del diccionario deben ser hash. Por ejemplo, hash(hashabledict({'a':[1,2]}))levantaráTypeError .
  2. Las claves deben admitir la operación de comparación. Por ejemplo, hash(hashabledict({'a':'a', 1:1}))levantaráTypeError .
  3. El operador de comparación de claves impone un orden total. Por ejemplo, si las dos claves en un diccionario son frozenset((1,2,3))y frozenset((4,5,6)), se comparan desiguales en ambas direcciones. Por lo tanto, ordenar los elementos de un diccionario con tales claves puede resultar en un orden arbitrario y, por lo tanto, violará la regla de que los objetos iguales deben tener el mismo valor hash.

La respuesta mucho más rápida de @ObenSonne elimina las restricciones 2 y 3, pero aún está limitada por la restricción 1 (los valores deben ser hash).

La respuesta más rápida aún de @RaymondHettinger elimina las 3 restricciones porque no se incluye .values()en el cálculo de hash. Sin embargo, su rendimiento es bueno solo si:

  1. La mayoría de los diccionarios (no iguales) a los que se les debe aplicar hash no tienen .keys() .

Si no se cumple esta condición, la función hash seguirá siendo válida, pero puede causar demasiadas colisiones. Por ejemplo, en el caso extremo en el que todos los diccionarios se generan a partir de una plantilla de sitio web (nombres de campo como claves, entrada del usuario como valores), las claves siempre serán las mismas y la función hash devolverá el mismo valor para todas las entradas. . Como resultado, una tabla hash que se basa en dicha función hash se volverá tan lenta como una lista al recuperar un elemento (O(N) lugar de O(1)).

Creo que la siguiente solución funcionará razonablemente bien incluso si se violan las 4 restricciones que enumeré anteriormente. Tiene la ventaja adicional de que puede aplicar hash no solo a los diccionarios, sino a cualquier contenedor, incluso si tienen contenedores mutables anidados.

Apreciaría mucho cualquier comentario sobre esto, ya que solo lo probé a la ligera hasta ahora.

# 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))
máx.
fuente
2

Es posible que también desee agregar estos dos métodos para que el protocolo de decapado v2 funcione con instancias hashdict. De lo contrario, cPickle intentará usar hashdict .____ setitem____ resultando en un TypeError. Curiosamente, con las otras dos versiones del protocolo, su código funciona bien.

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

Si no pone números en el diccionario y nunca pierde las variables que contienen sus diccionarios, puede hacer esto:

cache[id(rule)] = "whatever"

ya que id () es único para cada diccionario

EDITAR:

Oh, lo siento, sí, en ese caso lo que dijeron los otros chicos sería mejor. Creo que también podría serializar sus diccionarios como una cadena, como

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

Sin embargo, si necesita recuperar sus diccionarios de las claves, entonces tendrá que hacer algo más feo como

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

Supongo que la ventaja de esto es que no tendrías que escribir tanto código.

Garganta profunda
fuente
Hmmm, no; esto no es lo que estoy buscando: cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cacheSer capaz de crear claves dinámicamente es de importancia para cuando quiero usar dictados como claves en primer lugar.
SingleNegationElimination
1
Serializar los dictados podría estar bien, ¿tiene alguna recomendación sobre cómo serializarlos? eso es lo que estoy buscando.
SingleNegationElimination