Aparentemente list(a) no se sobreasigna, [x for x in a] se sobreasigna en algunos puntos, y [*a] se sobreasigna todo el tiempo .
Aquí están los tamaños n de 0 a 12 y los tamaños resultantes en bytes para los tres métodos:
0 56 56 56 1 64 88 88 2 72 88 96 3 80 88 104 4 88 88 112 5 96 120 120 6 104 120 128 7 112 120 136 8 120 120 152 9 128 184 184 10 136 184 192 11 144 184 200 12 152 184 208Calculado así, reproducible en repl.it , usando Python 3.8 :
from sys import getsizeof for n in range(13): a = [None] * n print(n, getsizeof(list(a)), getsizeof([x for x in a]), getsizeof([*a])) ¿Entonces, cómo funciona esto? ¿Cómo sobreasigna [*a] ? En realidad, ¿qué mecanismo utiliza para crear la lista de resultados a partir de la entrada dada? ¿Utiliza un iterador sobre a y usa algo como list.append ? ¿Dónde está el código fuente?
( Colaboración con los datos y el código que produjo las imágenes).
Acercando a n más pequeño:
Alejar a n más grande:
[*a] está haciendo internamente el equivalente en C de :
list vacíanewlist.extend(a)list devoluciones.Entonces, si expande su prueba a:
from sys import getsizeof for n in range(13): a = [None] * n l = [] l.extend(a) print(n, getsizeof(list(a)), getsizeof([x for x in a]), getsizeof([*a]), getsizeof(l)) verá los resultados de getsizeof([*a]) y l = []; l.extend(a); getsizeof(l) son iguales.
Esto suele ser lo correcto; cuando se extend , por lo general se espera agregar más más tarde, y de manera similar para el desempaquetado generalizado, se supone que se agregarán varias cosas una tras otra. [*a] no es el caso normal; Python asume que se agregan varios elementos o iterables a la list ( [*a, b, c, *d] ), por lo que la sobreasignación ahorra trabajo en el caso común.
Por el contrario, una list construida a partir de un solo iterable de tamaño predeterminado (con list() ) puede no crecer ni reducirse durante el uso, y la sobreasignación es prematura hasta que se demuestre lo contrario; Python solucionó recientemente un error que hacía que el constructor sobreasignara incluso para entradas con tamaño conocido .
En cuanto a las comprensiones de list , son efectivamente equivalentes a los append repetidos, por lo que está viendo el resultado final del patrón de crecimiento de sobreasignación normal al agregar un elemento a la vez.
Para ser claros, nada de esto es una garantía de idioma. Así es como lo implementa CPython. La especificación del lenguaje Python generalmente no se preocupa por los patrones de crecimiento específicos en la list (aparte de garantizar la amortización de O(1) append s y pop s desde el final). Como se señaló en los comentarios, la implementación específica cambia nuevamente en 3.9; si bien no afectará a [*a] , podría afectar otros casos en los que lo que solía ser "crear una tuple temporal de elementos individuales y luego extend con la tuple " ahora se convierte en múltiples aplicaciones de LIST_APPEND , que pueden cambiar cuando se produce la sobreasignación y qué números entran en el cálculo.
Imagen completa de lo que sucede, basándose en las otras respuestas y comentarios (especialmente la respuesta de ShadowRanger , que también explica por qué se hace así).
El desmontaje muestra que se usa BUILD_LIST_UNPACK :
>>> import dis >>> dis.dis('[*a]') 1 0 LOAD_NAME 0 (a) 2 BUILD_LIST_UNPACK 1 4 RETURN_VALUE Eso se maneja en ceval.c , que construye una lista vacía y la extiende (con a ):
case TARGET(BUILD_LIST_UNPACK): { ... PyObject *sum = PyList_New(0); ... none_val = _PyList_Extend((PyListObject *)sum, PEEK(i)); _PyList_Extend usa list_extend :
_PyList_Extend(PyListObject *self, PyObject *iterable) { return list_extend(self, iterable); } Que llama a list_resize con la suma de los tamaños :
list_extend(PyListObject *self, PyObject *iterable) ... n = PySequence_Fast_GET_SIZE(iterable); ... m = Py_SIZE(self); ... if (list_resize(self, m + n) < 0) {Y eso sobreasigna de la siguiente manera:
list_resize(PyListObject *self, Py_ssize_t newsize) { ... new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);Comprobemos eso. Calcule el número esperado de puntos con la fórmula anterior y calcule el tamaño de bytes esperado multiplicándolo por 8 (ya que aquí estoy usando Python de 64 bits) y agregando el tamaño de bytes de una lista vacía (es decir, la sobrecarga constante de un objeto de lista) :
from sys import getsizeof for n in range(13): a = [None] * n expected_spots = n + (n >> 3) + (3 if n < 9 else 6) expected_bytesize = getsizeof([]) + expected_spots * 8 real_bytesize = getsizeof([*a]) print(n, expected_bytesize, real_bytesize, real_bytesize == expected_bytesize)Producción:
0 80 56 False 1 88 88 True 2 96 96 True 3 104 104 True 4 112 112 True 5 120 120 True 6 128 128 True 7 136 136 True 8 152 152 True 9 184 184 True 10 192 192 True 11 200 200 True 12 208 208 True Coincide excepto por n = 0 , que list_extend en realidad abrevia , por lo que en realidad también coincide:
if (n == 0) { ... Py_RETURN_NONE; } ... if (list_resize(self, m + n) < 0) {Estos serán los detalles de implementación del intérprete de CPython y, por lo tanto, es posible que no sean coherentes entre otros intérpretes.
Dicho esto, puede ver dónde entran los comportamientos de comprensión y list(a) aquí:
https://github.com/python/cpython/blob/master/Objects/listobject.c#L36
Específicamente para la comprensión:
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ... ... new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6); Justo debajo de esas líneas, está list_preallocate_exact que se usa cuando se llama a list(a) .