¿Alguien sabe cómo se implementa el tipo de diccionario incorporado para python? Tengo entendido que es una especie de tabla hash, pero no he podido encontrar ningún tipo de respuesta definitiva.
python
data-structures
dictionary
ricree
fuente
fuente
Respuestas:
Aquí está todo sobre los dictados de Python que pude reunir (probablemente más de lo que a nadie le gustaría saber, pero la respuesta es exhaustiva).
dict
usa direccionamiento abierto para resolver colisiones de hash (explicado a continuación) (ver dictobject.c: 296-297 ).O(1)
búsqueda por índice).La siguiente figura es una representación lógica de una tabla hash de Python. En la figura a continuación,
0, 1, ..., i, ...
a la izquierda hay índices de las ranuras en la tabla hash (son solo para fines ilustrativos y, obviamente, no se almacenan junto con la tabla).Cuando se inicializa un nuevo dict comienza con 8 ranuras . (ver dictobject.h: 49 )
i
, que se basa en el hash de la clave. CPython utiliza inicialmentei = hash(key) & mask
(dondemask = PyDictMINSIZE - 1
, pero eso no es realmente importante). Solo tenga en cuenta que la ranura iniciali
, que está marcada, depende del hash de la clave.<hash|key|value>
). ¿Pero qué pasa si ese espacio está ocupado? Lo más probable es que otra entrada tenga el mismo hash (¡colisión de hash!)==
comparación no lais
comparación) de la entrada en la ranura con el hash y la clave de la entrada actual que se va a insertar ( dictobject.c : 337,344-345 ) respectivamente. Si ambos coinciden, entonces cree que la entrada ya existe, se da por vencida y pasa a la siguiente entrada que se insertará. Si el hash o la clave no coinciden, comienza a sondear .i+1, i+2, ...
y usar el primero disponible (eso es sondeo lineal). Pero por razones explicadas maravillosamente en los comentarios (ver dictobject.c: 33-126 ), CPython usa sondeo aleatorio . En el sondeo aleatorio, el siguiente espacio se elige en un orden pseudoaleatorio. La entrada se agrega a la primera ranura vacía. Para esta discusión, el algoritmo real utilizado para elegir el siguiente espacio no es realmente importante (ver dictobject.c: 33-126 para el algoritmo de sondeo). Lo importante es que las ranuras se analicen hasta que se encuentre la primera ranura vacía.dict
se cambiará el tamaño si está dos tercios lleno. Esto evita ralentizar las búsquedas. (ver dictobject.h: 64-65 )NOTA: Investigué sobre la implementación de Python Dict en respuesta a mi propia pregunta sobre cómo varias entradas en un dict pueden tener los mismos valores hash. Publiqué una versión ligeramente editada de la respuesta aquí porque toda la investigación también es muy relevante para esta pregunta.
fuente
Aquí está el curso corto:
El aspecto ordenado no es oficial a partir de Python 3.6 (para dar a otras implementaciones la oportunidad de mantenerse al día), pero oficial en Python 3.7 .
Los diccionarios de Python son tablas de hash
Durante mucho tiempo, funcionó exactamente así. Python preasignaría 8 filas vacías y usaría el hash para determinar dónde pegar el par clave-valor. Por ejemplo, si el hash para la clave terminó en 001, lo pegaría en el índice 1 (es decir, el segundo) (como en el ejemplo a continuación).
Cada fila ocupa 24 bytes en una arquitectura de 64 bits, 12 en una de 32 bits. (Tenga en cuenta que los encabezados de columna son solo etiquetas para nuestros propósitos aquí; en realidad no existen en la memoria).
Si el hash terminó igual que el hash de una clave preexistente, esto es una colisión, y luego pegaría el par clave-valor en una ubicación diferente.
Después de almacenar 5 valores clave, al agregar otro par clave-valor, la probabilidad de colisiones hash es demasiado grande, por lo que el diccionario duplica su tamaño. En un proceso de 64 bits, antes del cambio de tamaño, tenemos 72 bytes vacíos, y después, estamos desperdiciando 240 bytes debido a las 10 filas vacías.
Esto ocupa mucho espacio, pero el tiempo de búsqueda es bastante constante. El algoritmo de comparación de claves es calcular el hash, ir a la ubicación esperada, comparar la identificación de la clave; si son el mismo objeto, son iguales. Si no, entonces comparar los valores hash, si son no lo mismo, no son iguales. De lo contrario, finalmente comparamos las claves para la igualdad y, si son iguales, devolvemos el valor. La comparación final para la igualdad puede ser bastante lenta, pero las comprobaciones anteriores generalmente reducen la comparación final, lo que hace que las búsquedas sean muy rápidas.
Las colisiones ralentizan las cosas, y un atacante podría usar teóricamente colisiones hash para realizar un ataque de denegación de servicio, por lo que aleatorizamos la inicialización de la función hash de modo que calcule diferentes hash para cada nuevo proceso de Python.
El espacio desaprovechado descrito anteriormente nos ha llevado a modificar la implementación de los diccionarios, con una nueva y emocionante característica que los diccionarios ahora están ordenados por inserción.
Las nuevas tablas de hash compactas
Comenzamos, en cambio, preasignando una matriz para el índice de la inserción.
Como nuestro primer par clave-valor va en la segunda ranura, indexamos así:
Y nuestra tabla solo se llena por orden de inserción:
Entonces, cuando buscamos una clave, usamos el hash para verificar la posición que esperamos (en este caso, vamos directamente al índice 1 de la matriz), luego vamos a ese índice en la tabla hash (por ejemplo, índice 0 ), compruebe que las claves son iguales (utilizando el mismo algoritmo descrito anteriormente) y, de ser así, devuelva el valor.
Mantenemos un tiempo de búsqueda constante, con pérdidas de velocidad menores en algunos casos y ganancias en otros, con las ventajas de que ahorramos bastante espacio sobre la implementación preexistente y conservamos el orden de inserción. El único espacio desperdiciado son los bytes nulos en la matriz de índice.
Raymond Hettinger introdujo esto en python-dev en diciembre de 2012. Finalmente entró en CPython en Python 3.6 . Ordenar por inserción se consideró un detalle de implementación para 3.6 para permitir que otras implementaciones de Python tengan la oportunidad de ponerse al día.
Claves compartidas
Otra optimización para ahorrar espacio es una implementación que comparte claves. Por lo tanto, en lugar de tener diccionarios redundantes que ocupan todo ese espacio, tenemos diccionarios que reutilizan las claves compartidas y los hashes de las claves. Puedes pensarlo así:
Para una máquina de 64 bits, esto podría ahorrar hasta 16 bytes por clave por diccionario adicional.
Claves compartidas para objetos personalizados y alternativas
Estos dictos de clave compartida están destinados a ser utilizados para objetos personalizados '
__dict__
. Para obtener este comportamiento, creo que debe terminar de llenar su__dict__
antes de crear una instancia de su próximo objeto ( consulte PEP 412 ). Esto significa que debe asignar todos sus atributos en__init__
o__new__
, de lo contrario, es posible que no obtenga sus ahorros de espacio.Sin embargo, si conoce todos sus atributos en el momento en que
__init__
se ejecuta, también podría proporcionar__slots__
su objeto y garantizar que__dict__
no se crea en absoluto (si no está disponible en los padres), o incluso permitir__dict__
pero garantizar que sus atributos previstos sean almacenado en ranuras de todos modos. Para más información__slots__
, mira mi respuesta aquí .Ver también:
**kwargs
una función.fuente
find_empty_slot
: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - y comenzando en la línea 134 hay una prosa que lo describe.Los diccionarios de Python usan direccionamiento abierto ( referencia dentro del código hermoso )
¡NÓTESE BIEN! El direccionamiento abierto , también conocido como hashing cerrado , no debe confundirse, como se señaló en Wikipedia, con su hashing abierto opuesto .
El direccionamiento abierto significa que el dict usa ranuras de matriz, y cuando la posición primaria de un objeto se toma en la dict, se busca el lugar del objeto en un índice diferente en la misma matriz, usando un esquema de "perturbación", donde el valor hash del objeto juega un papel importante. .
fuente