¿Cuál es una forma correcta y buena de implementar __hash __ ()?


150

¿Cuál es una forma correcta y buena de implementar __hash__()?

Estoy hablando de la función que devuelve un código hash que luego se usa para insertar objetos en tablas hash, también conocidos como diccionarios.

Como __hash__()devuelve un entero y se usa para "agrupar" objetos en tablas hash, supongo que los valores del entero devuelto deben distribuirse uniformemente para los datos comunes (para minimizar las colisiones). ¿Qué es una buena práctica para obtener esos valores? ¿Las colisiones son un problema? En mi caso, tengo una clase pequeña que actúa como una clase contenedor que contiene algunas entradas, algunos flotadores y una cadena.

Respuestas:


185

Una manera fácil y correcta de implementar __hash__()es usar una tupla clave. No será tan rápido como un hash especializado, pero si lo necesita, probablemente debería implementar el tipo en C.

Aquí hay un ejemplo del uso de una clave para hash e igualdad:

class A:
    def __key(self):
        return (self.attr_a, self.attr_b, self.attr_c)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        if isinstance(other, A):
            return self.__key() == other.__key()
        return NotImplemented

Además, la documentación de__hash__ tiene más información, que puede ser valiosa en algunas circunstancias particulares.


1
Aparte de la sobrecarga menor de factorizar la __keyfunción, esto es casi tan rápido como cualquier hash puede ser. Claro, si se sabe que los atributos son enteros, y no hay demasiados, supongo que potencialmente podría correr un poco más rápido con un poco de hash casero, pero probablemente no estaría tan bien distribuido. hash((self.attr_a, self.attr_b, self.attr_c))va a ser sorprendentemente rápido (y correcto ), ya que la creación de pequeños tuples está especialmente optimizada, y empuja el trabajo de obtener y combinar hashes en C incorporados, que generalmente es más rápido que el código de nivel de Python.
ShadowRanger

Digamos que un objeto de la clase A se está utilizando como clave para un diccionario y si un atributo de la clase A cambia, su valor hash también cambiará. ¿No crearía eso un problema?
Sr. Matrix

1
Como la respuesta de @ loved.by.Jesus a continuación menciona, el método hash no debe definirse / anularse para un objeto mutable (definido de manera predeterminada y usa id para igualdad y comparación).
Sr. Matrix

@Miguel, me encontré con el problema exacto , lo que sucede es que el diccionario vuelve Noneuna vez que cambia la clave. La forma en que lo resolví fue almacenando la identificación del objeto como una clave en lugar de solo el objeto.
Jaswant P

@JaswantP Python usa de manera predeterminada la identificación del objeto como clave para cualquier objeto que se pueda cambiar.
Mr Matrix

22

John Millikin propuso una solución similar a esta:

class A(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        return (isinstance(othr, type(self))
                and (self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))

    def __hash__(self):
        return hash((self._a, self._b, self._c))

El problema con esta solución es que el hash(A(a, b, c)) == hash((a, b, c)). En otras palabras, el hash choca con el de la tupla de sus miembros clave. Tal vez esto no importa muy a menudo en la práctica?

Actualización: los documentos de Python ahora recomiendan usar una tupla como en el ejemplo anterior. Tenga en cuenta que la documentación indica

La única propiedad requerida es que los objetos que comparan igual tienen el mismo valor hash

Tenga en cuenta que lo contrario no es cierto. Los objetos que no se comparan igual pueden tener el mismo valor hash. Tal colisión de hash no hará que un objeto reemplace a otro cuando se use como una clave dict o elemento de ajuste , siempre que los objetos no se comparen igual .

Solución desactualizada / mala

La documentación de Python__hash__ sugiere combinar los hashes de los subcomponentes usando algo como XOR , lo que nos da esto:

class B(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        if isinstance(othr, type(self)):
            return ((self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))
        return NotImplemented

    def __hash__(self):
        return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
                hash((self._a, self._b, self._c)))

Actualización: como señala Blckknght, cambiar el orden de a, byc podría causar problemas. Agregué un adicional ^ hash((self._a, self._b, self._c))para capturar el orden de los valores que se están dividiendo. Este final ^ hash(...)se puede eliminar si los valores que se combinan no se pueden reorganizar (por ejemplo, si tienen diferentes tipos y, por lo tanto, el valor de _anunca se asignará a _bo _c, etc.).


55
Por lo general, no desea hacer un XOR directo de los atributos juntos, ya que eso le dará colisiones si cambia el orden de los valores. Es decir, hash(A(1, 2, 3))será igual a hash(A(3, 1, 2))(y ambos serán hash iguales a cualquier otra Ainstancia con una permutación de 1, 2y 3como sus valores). Si desea evitar que su instancia tenga el mismo hash que una tupla de sus argumentos, simplemente cree un valor centinela (como variable de clase o como global) e inclúyalo en la tupla que se va a hash: return hash ((_ sentinel , self._a, self._b, self._c))
Blckknght

1
Su uso de isinstancepodría ser problemático, ya que un objeto de una subclase de type(self)ahora puede ser igual a un objeto de type(self). Así que es posible que la adición de una Cary una Forda una set()puede resultar en un único objeto insertado, dependiendo de la orden de inserción. Además, puede encontrarse con una situación en la que a == bes Verdadero pero b == aFalso.
MaratC

1
Si usted es de subclases B, es posible que desee cambiar eso aisinstance(othr, B)
millerdev

77
Un pensamiento: la tupla clave podría incluir el tipo de clase, lo que impediría otras clases con el mismo conjunto de atributos clave se muestre a ser igual: hash((type(self), self._a, self._b, self._c)).
Ben Mosher

2
Además del punto sobre el uso en Blugar de type(self), a menudo también se considera una mejor práctica regresar NotImplementedcuando se encuentra con un tipo inesperado en __eq__lugar de False. Eso permite que otros tipos definidos por el usuario implementen un sistema __eq__que sepa By pueda comparar igual, si así lo desean.
Mark Amery

16

Paul Larson de Microsoft Research estudió una amplia variedad de funciones hash. Él me dijo eso

for c in some_string:
    hash = 101 * hash  +  ord(c)

funcionó sorprendentemente bien para una amplia variedad de cuerdas. Descubrí que técnicas polinómicas similares funcionan bien para calcular un hash de subcampos dispares.


8
Al parecer Java lo hace de la misma manera pero usando 31 en lugar de 101
user229898

3
¿Cuál es la razón detrás del uso de estos números? ¿Hay alguna razón para elegir 101 o 31?
bigblind

1
Aquí hay una explicación para los multiplicadores principales: stackoverflow.com/questions/3613102/… . 101 parece funcionar particularmente bien, basado en los experimentos de Paul Larson.
George V. Reilly

44
Python utiliza (hash * 1000003) XOR ord(c)para cadenas con multiplicación envolvente de 32 bits. [Cita ]
tylerl

44
Incluso si esto es cierto, no tiene ningún uso práctico en este contexto porque los tipos de cadena Python incorporados ya proporcionan un __hash__método; No necesitamos rodar el nuestro. La pregunta es cómo implementar __hash__una clase típica definida por el usuario (con un montón de propiedades que apuntan a tipos integrados o tal vez a otras clases definidas por el usuario), que esta respuesta no aborda en absoluto.
Mark Amery

3

Puedo intentar responder la segunda parte de tu pregunta.

Las colisiones probablemente serán el resultado del código hash en sí mismo, sino de la asignación del código hash a un índice en una colección. Entonces, por ejemplo, su función hash podría devolver valores aleatorios de 1 a 10000, pero si su tabla hash solo tiene 32 entradas, obtendrá colisiones al insertarlas.

Además, pensaría que las colisiones serían resueltas internamente por la colección, y existen muchos métodos para resolver las colisiones. Lo más simple (y lo peor) es, dada una entrada para insertar en el índice i, agregar 1 a i hasta que encuentre un lugar vacío e insertar allí. La recuperación entonces funciona de la misma manera. Esto da como resultado recuperaciones ineficientes para algunas entradas, ya que podría tener una entrada que requiera recorrer toda la colección para encontrarla.

Otros métodos de resolución de colisiones reducen el tiempo de recuperación al mover las entradas en la tabla hash cuando se inserta un elemento para distribuir las cosas. Esto aumenta el tiempo de inserción pero supone que lee más de lo que inserta. También hay métodos que intentan ramificar diferentes entradas en colisión para que las entradas se agrupen en un lugar en particular.

Además, si necesita cambiar el tamaño de la colección, deberá volver a mostrar todo o usar un método de hashing dinámico.

En resumen, dependiendo de lo que esté usando el código hash, es posible que deba implementar su propio método de resolución de colisiones. Si no los está almacenando en una colección, probablemente pueda salirse con la suya con una función hash que solo genera códigos hash en un rango muy amplio. Si es así, puede asegurarse de que su contenedor sea más grande de lo que debe ser (cuanto más grande mejor, por supuesto) dependiendo de sus problemas de memoria.

Aquí hay algunos enlaces si le interesa más:

hashing combinado en wikipedia

Wikipedia también tiene un resumen de varios métodos de resolución de colisiones:

Además, " Organización y procesamiento de archivos " de Tharp cubre una gran cantidad de métodos de resolución de colisiones. OMI es una gran referencia para algoritmos de hash.


1

Una muy buena explicación sobre cuándo y cómo implementar la __hash__función en el sitio web programiz :

Solo una captura de pantalla para proporcionar una descripción general: (Consultado el 13/12/2019)

Captura de pantalla de https://www.programiz.com/python-programming/methods/built-in/hash 2019-12-13

En cuanto a una implementación personal del método, el sitio mencionado anteriormente proporciona un ejemplo que coincide con la respuesta de millerdev .

class Person:
def __init__(self, age, name):
    self.age = age
    self.name = name

def __eq__(self, other):
    return self.age == other.age and self.name == other.name

def __hash__(self):
    print('The hash is:')
    return hash((self.age, self.name))

person = Person(23, 'Adam')
print(hash(person))

0

Depende del tamaño del valor hash que devuelva. Es una lógica simple que si necesita devolver un 32bit int basado en el hash de cuatro entradas de 32bit, obtendrá colisiones.

Yo favorecería las operaciones bit. Como el siguiente pseudocódigo C:

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

Tal sistema también podría funcionar para flotantes, si simplemente los toma como su valor de bit en lugar de representar realmente un valor de punto flotante, tal vez sea mejor.

Para las cadenas, tengo poca / ninguna idea.


Sé que habrá colisiones. Pero no tengo idea de cómo se manejan estos. Además, mis valores de atributo en combinación están muy poco distribuidos, así que estaba buscando una solución inteligente. Y de alguna manera esperaba que hubiera una mejor práctica en alguna parte.
user229898
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.