¿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.
fuente
__key
funció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ñostuple
s 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.None
una 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.John Millikin propuso una solución similar a esta:
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
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, lo que nos da esto:__hash__
sugiere combinar los hashes de los subcomponentes usando algo como XORActualizació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_a
nunca se asignará a_b
o_c
, etc.).fuente
hash(A(1, 2, 3))
será igual ahash(A(3, 1, 2))
(y ambos serán hash iguales a cualquier otraA
instancia con una permutación de1
,2
y3
como 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))isinstance
podría ser problemático, ya que un objeto de una subclase detype(self)
ahora puede ser igual a un objeto detype(self)
. Así que es posible que la adición de unaCar
y unaFord
a unaset()
puede resultar en un único objeto insertado, dependiendo de la orden de inserción. Además, puede encontrarse con una situación en la quea == b
es Verdadero perob == a
Falso.B
, es posible que desee cambiar eso aisinstance(othr, B)
hash((type(self), self._a, self._b, self._c))
.B
lugar detype(self)
, a menudo también se considera una mejor práctica regresarNotImplemented
cuando se encuentra con un tipo inesperado en__eq__
lugar deFalse
. Eso permite que otros tipos definidos por el usuario implementen un sistema__eq__
que sepaB
y pueda comparar igual, si así lo desean.Paul Larson de Microsoft Research estudió una amplia variedad de funciones hash. Él me dijo eso
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.
fuente
(hash * 1000003) XOR ord(c)
para cadenas con multiplicación envolvente de 32 bits. [Cita ]__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.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.
fuente
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)
En cuanto a una implementación personal del método, el sitio mencionado anteriormente proporciona un ejemplo que coincide con la respuesta de millerdev .
fuente
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:
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.
fuente