¿Cuál es el __hash__ predeterminado en Python?

Muy a menudo uso cosas funky como claves para los diccionarios y, por lo tanto, me pregunto cuál es la forma correcta de hacerlo, y esto pasa por implementar buenos métodos hash para mis objetos. Soy consciente de otras preguntas hechas aquí como buena manera de implementar hachís, pero me gustaría entender cómo el valor predeterminado __hash__ funciona para objetos personalizados, y si es posible confiar en él.

He notado que los mutables son explícitamente imposibles de modificar desde hash({}) genera un error ... pero extrañamente, las clases personalizadas son hashable:

>>> class Object(object): pass
>>> o = Object()
>>> hash(o)

Entonces, ¿alguien sabe cómo funciona esta función hash predeterminada? Al entender esto, me gustaría saber:

¿Puedo confiar en este hash predeterminado si coloco objetos del mismo tipo como claves de un diccionario? p.ej :

key1 = MyObject()
key2 = MyObject()
key3 = MyObject()
{key1: 1, key2: 'blabla', key3: 456}

¿Puedo confiar en él si uso objetos de diferentes tipos como claves en un diccionario? p.ej

{int: 123, MyObject(10): 'bla', 'plo': 890}

Y en el último caso también, ¿cómo asegurarme de que mis valores hash personalizados no entren en conflicto con los valores hash integrados? p.ej :

{int: 123, MyObject(10): 'bla', MyObjectWithCustomHash(123): 890}

preguntado el 04 de julio de 12 a las 08:07

@gnibbler: ya lo tengo - vea el enlace en la pregunta -

No relacionado, pero un buen punto a tener en cuenta: "Si va a anular __hash__, anular __eq__ también." -

User-defined classes have __eq__() and __hash__() methods by default; with them, all objects compare unequal (except with themselves) and x.__hash__() returns an appropriate value such that x == y implies both that x is y and hash(x) == hash(y). docs.python.org/3/reference/datamodel.html#object.__hash__ -

6 Respuestas

En lo que puede confiar: los objetos personalizados tienen un valor predeterminado hash() que se basa de alguna manera en la identidad del objeto. es decir, cualquier objeto que use el hash predeterminado tendrá un valor constante para ese hash a lo largo de su vida y diferentes objetos pueden o no tener un valor de hash diferente.

No puede confiar en ninguna relación particular entre el valor devuelto por id() y el valor devuelto por hash(). En la implementación estándar de C de Python 2.6 y versiones anteriores eran iguales, en Python 2.7-3.2 hash(x)==id(x)/16.

Edit: originalmente escribí que en las versiones 3.2.3 y posteriores o 2.7.3 o posteriores, el valor hash puede ser aleatorio y en Python 3.3 la relación siempre será aleatoria. De hecho, la aleatorización en la actualidad solo se aplica a las cadenas de hash, por lo que, de hecho, la relación de dividir por 16 puede continuar manteniéndose por ahora, pero no confíe en ello.

Las colisiones de hash generalmente no importan: en una búsqueda de diccionario para encontrar un objeto, debe tener el mismo hash y también debe compararse igual. Las colisiones solo importan si obtiene una proporción muy alta de colisiones, como en el ataque de denegación de servicio que llevó a las versiones recientes de Python a aleatorizar el cálculo del hash.

Respondido 04 Jul 12, 09:07

El documentación establece que los objetos personalizados se basan en id() como su hash() implementación:

Detalle de implementación de CPython: esta es la dirección del objeto en la memoria.

Si mezcla objetos personalizados con tipos incorporados como int pueden ser colisiones de hash, pero eso no es un problema si se distribuyen por igual. No investigues demasiado a menos que realmente encuentres un problema de rendimiento.

Respondido 04 Jul 12, 08:07

Entonces, ¿quiere decir que si uso solo tipos personalizados, no debería haber colisiones? - sebpiq

Correcto, la identificación es única. Lo que pasa con otros tipos es que no necesariamente usan id() pero a menudo un valor hash más razonable; por ejemplo, los ints usan solo su valor como su valor hash. - meter

Asi que : {int: 123, MyObject(): 465, MyType: 890} debe ser seguro, ¿verdad? - sebpiq

Además... permítanme decir una vez más que a pesar de lo que dice el documento, en Python 2.7 id(custom_obj) != hash(custom_obj) - sebpiq

Son "seguros" aunque pueden ser colisiones hash. Es solo una preocupación de rendimiento. A su pregunta, no, pueden ocurrir colisiones hash si sus claves no son solo instancias de objetos personalizados. - usuario816328

En Python 3, la siguiente función se usa en subclases de object Contra el id() del objeto (de pyhash.c)

Py_hash_t
_Py_HashPointer(void *p)
{
    Py_hash_t x;
    size_t y = (size_t)p;
    /* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid
       excessive hash collisions for dicts and sets */
    y = (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4));
    x = (Py_hash_t)y;
    if (x == -1)
        x = -2;
    return x;
}

SIZEOF_VOID_P es 8 para Python de 64 bits y 4 para Python de 32 bits.

>>> class test: pass
...
>>> a = test()
>>> id(a)
4325845928
>>> hash(a)
-9223372036584410438

Puedes ver que el hash se calcula a partir de id(a) usando la fórmula (id(a) >> 4) | (id(a) << (8 * SIZEOF_VOID_P - 4)), donde las operaciones bit a bit se realizan en C enteros con signo. Por ejemplo, para el a definido arriba:

>>> import numpy
>>> y = numpy.array([4325845928], dtype='int64')
>>> SIZEOF_VOID_P = 8
>>> (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4))
array([-9223372036584410438])

Tenga en cuenta que estoy usando numpy.array(dtype='int64') para que las operaciones bit a bit actúen de la misma manera que lo harían en C (si realiza la misma operación en Python, obtiene un comportamiento diferente porque no se desbordan). Ver https://stackoverflow.com/a/5994397/161801.

contestado el 23 de mayo de 17 a las 13:05

Conforme a Duncan- En Python 3.3 ni siquiera habrá una relación fija entre id() y hash(). - Piotr Dobrogost

@PiotrDobrogost hay una relación fija. Es (id(x) >> 4) | (id(x) << (8 * SIZEOF_VOID_P - 4)). El código que pegué aquí está tomado de la fuente de Python 3. d (la entrada al _Py_HashPointer función) es la dirección de memoria del objeto, es decir, su id(). correr SIZEOF_VOID_P = 8; y = numpy.array([4325845928], dtype='int64'); print((y >> 4) | (y << (8 * SIZEOF_VOID_P - 4))). El resultado es -9223372036584410438, que corresponde al ejemplo que mostré arriba. - asmeurer

Creo que Duncan se refería a la aleatorización de hash introducida en Python 3.3. Sin embargo, actualmente solo está activo para cadenas y el código que muestra es probablemente para el caso general. - Piotr Dobrogost

size_t no está firmado en C (no firmado), por lo que, como señala el comentario, esta es una operación de rollo: David Munro

El hash predeterminado para las clases definidas por el usuario es simplemente devolver su id. Esto da un comportamiento que suele ser útil; el uso de una instancia de una clase definida por el usuario como clave de diccionario permitirá recuperar el valor asociado cuando exactamente el mismo objeto se proporciona de nuevo para buscar el valor. p.ej:

>>> class Foo(object):
    def __init__(self, foo):
        self.foo = foo


>>> f = Foo(10)
>>> d = {f: 10}
>>> d[f]
10

Esto coincide con la igualdad predeterminada de las clases definidas por el usuario:

>>> g = Foo(10)
>>> f == g
False
>>> d[g]

Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    d[g]
KeyError: <__main__.Foo object at 0x0000000002D69390>

Tenga en cuenta que aunque f y g tienen los mismos valores para sus atributos, no son iguales y buscan g in d no encuentra el valor almacenado en f. Además, incluso si cambiamos el valor de f.foo, buscando f in d todavía encuentra el valor:

>>> f.foo = 11
>>> d[f]
10

La suposición es que las instancias de alguna nueva clase arbitraria deben tratarse como no equivalentes, a menos que el programador declare específicamente las condiciones para que dos instancias sean tratadas como equivalentes definiendo __eq__ y __hash__.

Y esto prácticamente funciona; si defino un Car clase, probablemente considero que dos autos con atributos idénticos representan dos autos diferentes. Si tengo un diccionario que mapea autos a propietarios registrados, no quiero encontrar a Alice cuando busco el auto de Bob, ¡incluso si Alice y Bob tienen autos idénticos! OTOH, si defino una clase para representar códigos postales, probablemente quiera considerar dos objetos diferentes con el mismo código como representaciones intercambiables de "lo mismo", y en este caso si tuviera un diccionario que mapeara códigos postales a estados , claramente me gustaría poder encontrar el mismo estado con dos objetos diferentes que representen el mismo código postal.

Me refiero a esto como la diferencia entre "tipos de valor" y "tipos de objeto". Los tipos de valor representan algún valor, y es el propuesta de Me importa, no la identidad de cada objeto individual. Dos formas diferentes de obtener el mismo valor son igualmente buenas, y el "contrato" del código que pasa alrededor de los tipos de valor generalmente solo promete darle un objeto con algún valor, sin especificar qué objeto en particular es. Para los tipos de objetos OTOH, cada instancia individual tiene su propia identidad, incluso si contiene exactamente los mismos datos que otra instancia. El "contrato" del código que pasa alrededor de los tipos de objetos generalmente promete realizar un seguimiento de los objetos individuales exactos.

Entonces, ¿por qué las clases mutables integradas no usan su id como su hash? Es porque son todos contenedores, y generalmente consideramos que los contenedores son principalmente como tipos de valor, con su valor determinado por los elementos contenidos:

>>> [1, 2, 3] == [1, 2, 3]
True
>>> {f: 10} == {f: 10}
True

Pero mudable Los contenedores tienen un valor que es transitorio. Alguna lista dada actualmente tiene el valor [1, 2, 3], pero se puede mutar para que tenga el valor [4, 5, 6]. Si pudiera usar listas como claves de diccionario, entonces tendríamos que tomar una decisión sobre si la búsqueda debe usar el valor (actual) de la lista o su identidad. De cualquier manera, podemos estar (muy) sorprendidos cuando el valor de un objeto que se usa actualmente como clave de diccionario cambia al mutarlo. El uso de objetos como claves de diccionario solo funciona bien cuando el valor del objeto is su identidad, o cuando la identidad de un objeto es irrelevante para su valor. Entonces, la respuesta elegida por Python es declarar que los contenedores mutables no se pueden modificar.


Ahora, detalles más específicos en respuesta a sus preguntas directas:

1) Dado que este hash predeterminado en CPython (aunque aparentemente solo < 2.6, según otras respuestas/comentarios) se asigna a la dirección de memoria del objeto, entonces en CPython no hay dos objetos que usen hash predeterminado que estén activos al mismo tiempo. sus valores hash, independientemente de las clases involucradas (y si se almacena como una clave de diccionario, está en vivo). También esperaría que otras implementaciones de Python que no usan direcciones de memoria como hash aún tengan distribuciones de hash finas entre los objetos que usan el hash predeterminado. Así que sí, puedes confiar en ello.

2) Mientras no devuelva como su hash personalizado un resultado que sea exactamente el hash de algún objeto existente, debería estar relativamente bien. Tengo entendido que los contenedores basados ​​en hash de Python son relativamente tolerantes con las funciones hash subóptimas, siempre que no estén completamente degeneradas.

Respondido 04 Jul 12, 09:07

>>> class C(object):
...     pass
... 
>>> c = C()
>>> hash(c) == id(c)
True

Ver función id

Respondido 04 Jul 12, 08:07

yo obtengo False en Python 2.7 y 3.2, pero True en Python 2.6. - huón

Las versiones anteriores de CPython solo usaban el valor de id() directamente por defecto hash(), las versiones más nuevas utilizan id()/16 porque en CPython todos los ID son un múltiplo de 16 y desea establecer los bits bajos. Esto es puramente un detalle de implementación: el valor predeterminado hash() se genera a partir de id() pero exactamente cómo cambia entre lanzamientos. En Python 3.3 ni siquiera habrá una relación fija entre id() y hash(). - Duncan

@Duncan ¿Por qué dividir por 16? ¿Por qué todos los identificadores son un múltiplo de 16 y quieren establecer los bits bajos? - una pieza

Los objetos de Python están alineados con el tamaño de la palabra de la memoria, por lo que para un intérprete de 64 bits significa que cada objeto está alineado con un múltiplo de 16. La identificación es simplemente la dirección de la memoria, por lo que todas las identificaciones son un múltiplo de 16. Esto es importante porque cuando obtiene una colisión de hash, se calcula un nuevo hash a partir del anterior utilizando los 5 bits más bajos, si 4 de ellos son siempre 0, entonces hay una alta probabilidad de que obtenga otra colisión. - Duncan

>>> class C(object):
...     pass
... 
>>> c = C()
>>> hash(c) == id(c)
False
>>> hash(c) == id(c)/16
True

Dividido por 16 da Verdadero

Respondido 27 ago 15, 10:08

Duplicar una respuesta publicada 3 años antes que usted no es útil. - Alejandro Huszag

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.