No entiendo cómo se realiza un bucle sobre un diccionario o un conjunto en Python por orden 'arbitrario'.
Quiero decir, es un lenguaje de programación, así que todo en el lenguaje debe estar 100% determinado, ¿correcto? Python debe tener algún tipo de algoritmo que decida qué parte del diccionario o conjunto se elige, primero, segundo, etc.
¿Qué me estoy perdiendo?
python
dictionary
set
python-internals
Edgar Aroutiounian
fuente
fuente
Respuestas:
El orden no es arbitrario, sino que depende del historial de inserción y eliminación del diccionario o conjunto, así como de la implementación específica de Python. Para el resto de esta respuesta, para 'diccionario', también puede leer 'set'; los conjuntos se implementan como diccionarios con solo claves y sin valores.
Las claves son hash, y los valores hash se asignan a las ranuras en una tabla dinámica (puede crecer o reducirse según las necesidades). Y ese proceso de mapeo puede conducir a colisiones, lo que significa que una clave tendrá que ubicarse en un siguiente espacio en función de lo que ya existe.
Al enumerar los bucles de contenido en las ranuras, las claves se enumeran en el orden en que residen actualmente en la tabla.
Tome las llaves
'foo'
y'bar'
, por ejemplo, y supongamos que el tamaño de la tabla es de 8 ranuras. En Python 2.7,hash('foo')
es-4177197833195190597
,hash('bar')
es327024216814240868
. Módulo 8, eso significa que estas dos teclas se ubican en las ranuras 3 y 4 y luego:Esto informa su orden de listado:
Todas las ranuras, excepto 3 y 4, están vacías, al recorrer la tabla se enumeran primero las ranuras 3 y luego las ranuras 4, por lo que
'foo'
se enumeran antes'bar'
.bar
ybaz
, sin embargo, tienen valores hash que están exactamente separados por 8 y, por lo tanto, se asignan exactamente a la misma ranura4
:Su orden ahora depende de qué clave se introdujo primero; la segunda clave deberá moverse a la siguiente ranura:
El orden de la tabla difiere aquí, porque una u otra clave se asignó primero.
El nombre técnico de la estructura subyacente utilizada por CPython (la implementación de Python más utilizada) es una tabla hash , una que utiliza direccionamiento abierto. Si tiene curiosidad y entiende C bastante bien, eche un vistazo a la implementación de C para ver todos los detalles (bien documentados). También puede ver esta presentación de Pycon 2010 de Brandon Rhodes sobre cómo
dict
funciona CPython , o recoger una copia de Beautiful Code , que incluye un capítulo sobre la implementación escrito por Andrew Kuchling.Tenga en cuenta que a partir de Python 3.3, también se usa una semilla aleatoria de hash, lo que hace que las colisiones de hash sean impredecibles para evitar ciertos tipos de denegación de servicio (donde un atacante hace que un servidor de Python no responda al causar colisiones de hash masivas). Esto significa que el orden de un diccionario o conjunto dado también depende de la semilla aleatoria hash para la invocación actual de Python.
Otras implementaciones son libres de usar una estructura diferente para los diccionarios, siempre que satisfagan la interfaz documentada de Python para ellos, pero creo que todas las implementaciones hasta ahora usan una variación de la tabla hash.
CPython 3.6 presenta una nueva
dict
implementación que mantiene el orden de inserción, y es más rápido y más eficiente en el arranque de memoria. En lugar de mantener una tabla dispersa grande donde cada fila hace referencia al valor hash almacenado y los objetos clave y de valor, la nueva implementación agrega una matriz hash más pequeña que solo hace referencia a índices en una tabla 'densa' separada (una que solo contiene tantas filas) ya que hay pares clave-valor reales), y es la tabla densa que enumera los elementos contenidos en orden. Vea la propuesta a Python-Dev para más detalles . Tenga en cuenta que en Python 3.6 esto se considera un detalle de implementación, Python-the-language no especifica que otras implementaciones tengan que mantener el orden. Esto cambió en Python 3.7, donde este detalle se elevó para ser una especificación de lenguaje ; para que cualquier implementación sea correctamente compatible con Python 3.7 o posterior, debe copiar este comportamiento de preservación del orden. Y para ser explícito: este cambio no se aplica a los conjuntos, ya que los conjuntos ya tienen una estructura hash 'pequeña'.Python 2.7 y posteriores también proporcionan una
OrderedDict
clase , una subclasedict
que agrega una estructura de datos adicional para registrar el orden de las claves. Al precio de cierta velocidad y memoria extra, esta clase recuerda en qué orden insertó las llaves; listar claves, valores o elementos lo hará en ese orden. Utiliza una lista doblemente vinculada almacenada en un diccionario adicional para mantener el orden actualizado de manera eficiente. Vea la publicación de Raymond Hettinger que describe la idea .OrderedDict
Los objetos tienen otras ventajas, como ser reordenables .Si desea un conjunto ordenado, puede instalar el
oset
paquete ; Funciona en Python 2.5 y versiones posteriores.fuente
__hash__
y__eq__
(y nada más) es prácticamente una garantía de idioma, no un detalle de implementación.dictobject.c
) y terminan con muchas menos comparaciones de las que un BTree necesita para encontrar el correcto subárbolEsto es más una respuesta a Python 3.41 Un conjunto antes de que se cerrara como duplicado.
Los otros tienen razón: no confíe en el orden. Ni siquiera finjas que hay uno.
Dicho esto, hay una cosa en la que puede confiar:
Es decir, el orden es estable .
Comprender por qué hay un orden percibido requiere comprender algunas cosas:
Que Python usa conjuntos hash ,
Cómo se almacena el hash set de CPython en la memoria y
Cómo se numeran los números
Desde la parte superior:
Un conjunto de hash es un método para almacenar datos aleatorios con tiempos de búsqueda realmente rápidos.
Tiene una matriz de respaldo:
Ignoraremos el objeto ficticio especial, que existe solo para hacer que las eliminaciones sean más fáciles de manejar, porque no eliminaremos de estos conjuntos.
Para tener una búsqueda realmente rápida, haces algo de magia para calcular un hash de un objeto. La única regla es que dos objetos que son iguales tienen el mismo hash. (Pero si dos objetos tienen el mismo hash, pueden ser desiguales).
Luego realiza un índice tomando el módulo por la longitud de la matriz:
Esto hace que sea realmente rápido acceder a elementos.
Hashes sólo son la mayor parte de la historia, como
hash(n) % len(storage)
yhash(m) % len(storage)
pueden resultar en el mismo número. En ese caso, varias estrategias diferentes pueden intentar resolver el conflicto. CPython usa "sondeo lineal" 9 veces antes de hacer cosas complicadas, por lo que buscará a la izquierda de la ranura hasta 9 lugares antes de buscar en otro lado.Los conjuntos de hash de CPython se almacenan así:
Un conjunto de hash no puede tener más de 2/3 de capacidad . Si hay 20 elementos y la matriz de respaldo tiene 30 elementos de largo, el almacén de respaldo cambiará de tamaño para ser más grande. Esto se debe a que se producen colisiones con mayor frecuencia en pequeñas tiendas de respaldo, y las colisiones ralentizan todo.
La tienda de respaldo cambia de tamaño en potencias de 4, comenzando en 8, excepto para conjuntos grandes (elementos de 50k) que cambian de tamaño en potencias de dos: (8, 32, 128, ...).
Por lo tanto, cuando crea una matriz, el almacén de respaldo es de longitud 8. Cuando está lleno 5 y agrega un elemento, contendrá brevemente 6 elementos.
6 > ²⁄₃·8
así que esto provoca un cambio de tamaño, y la tienda de respaldo se cuadruplica al tamaño 32.Finalmente,
hash(n)
solo devuelve losn
números (excepto-1
que es especial).Entonces, veamos el primero:
len(v_set)
es 10, por lo que la tienda de respaldo es al menos 15 (+1) después de que se hayan agregado todos los artículos . El poder relevante de 2 es 32. Entonces, la tienda de respaldo es:Tenemos
así que estos se insertan como:
Entonces esperaríamos una orden como
con el 1 o 33 que no está al principio en otro lugar. Esto usará un sondeo lineal, por lo que tendremos:
o
Es posible que espere que el 33 sea el desplazado porque el 1 ya estaba allí, pero debido al cambio de tamaño que ocurre a medida que se construye el conjunto, este no es realmente el caso. Cada vez que se reconstruye el conjunto, los elementos ya agregados se reordenan de manera efectiva.
Ahora puedes ver por qué
Podría estar en orden. Hay 14 elementos, por lo que la tienda de respaldo es al menos 21 + 1, lo que significa 32:
1 a 13 hash en los primeros 13 espacios. 20 va en la ranura 20.
55 va en la ranura
hash(55) % 32
que es 23:Si elegimos 50 en su lugar, esperaríamos
Y he aquí y he aquí:
pop
se implementa simplemente por el aspecto de las cosas: atraviesa la lista y aparece la primera.Todo esto es detalle de implementación.
fuente
"Arbitrario" no es lo mismo que "no determinado".
Lo que dicen es que no hay propiedades útiles del orden de iteración del diccionario que estén "en la interfaz pública". Es casi seguro que hay muchas propiedades del orden de iteración que están completamente determinadas por el código que implementa actualmente la iteración del diccionario, pero los autores no te las prometen como algo que puedes usar. Esto les da más libertad para cambiar estas propiedades entre versiones de Python (o incluso en diferentes condiciones de funcionamiento, o completamente al azar en tiempo de ejecución) sin preocuparse de que su programa se rompa.
Por lo tanto, si escribe un programa que depende de cualquier propiedad en el orden del diccionario, entonces está "rompiendo el contrato" de usar el tipo de diccionario, y los desarrolladores de Python no prometen que esto siempre funcionará, incluso si parece funcionar. por ahora cuando lo pruebes. Básicamente es el equivalente a confiar en un "comportamiento indefinido" en C.
fuente
d.items()
es esencialmente idéntico azip(d.keys(), d.values())
. Sin embargo, si se agregan elementos al diccionario, todas las apuestas están canceladas. El orden podría cambiar por completo (si la tabla hash tuviera que ser redimensionada), aunque la mayoría de las veces solo encontraría el nuevo elemento apareciendo en algún lugar arbitrario en la secuencia.Las otras respuestas a esta pregunta son excelentes y están bien escritas. El OP pregunta "cómo", que yo interpreto como "cómo se salen con la suya" o "por qué".
La documentación de Python dice que los diccionarios no están ordenados porque el diccionario de Python implementa la matriz asociativa de tipos de datos abstractos . Como ellos dicen
En otras palabras, un estudiante de informática no puede asumir que se ordena una matriz asociativa. Lo mismo es cierto para series en matemáticas
y ciencias de la computación
La implementación de un diccionario utilizando una tabla hash es un detalle de implementación interesante porque tiene las mismas propiedades que las matrices asociativas en lo que respecta al orden.
fuente
Python usa la tabla hash para almacenar los diccionarios, por lo que no hay orden en los diccionarios u otros objetos iterables que usan la tabla hash.
Pero con respecto a los índices de elementos en un objeto hash, python calcula los índices en función del siguiente código dentro
hashtable.c
:Por lo tanto, como el valor hash de los enteros es el propio entero *, el índice se basa en el número (
ht->num_buckets - 1
es una constante), por lo que el índice calculado por Bitwise-and entre(ht->num_buckets - 1)
y el número en sí * (se espera que -1, que es hash, sea -2 ) y para otros objetos con su valor hash.considere el siguiente ejemplo con
set
ese uso de tabla hash:Para el número
33
tenemos:Que en realidad es:
Nota en este caso
(ht->num_buckets - 1)
es8-1=7
o0b111
.Y para
1919
:Y para
333
:Para más detalles sobre la función hash de Python, es bueno leer las siguientes citas del código fuente de Python :
* La función hash para la clase
int
:fuente
Comenzando con Python 3.7 (y ya en CPython 3.6 ), los elementos del diccionario permanecen en el orden en que fueron insertados .
fuente