Recientemente comparé las velocidades de procesamiento de []
y me list()
sorprendió descubrir que []
funciona más de tres veces más rápido que list()
. Me encontré con la misma prueba con {}
y dict()
y los resultados fueron prácticamente idénticos: []
y {}
ambos tomaron alrededor 0.128sec / millón de ciclos, mientras que list()
y dict()
tomó aproximadamente 0.428sec / millón de ciclos de cada uno.
¿Por qué es esto? Hacer []
y {}
(y, probablemente, ()
y ''
, también) inmediatamente pasar de nuevo a copias de alguna vacío stock literal, mientras que sus homólogos explícitamente nombradas-( list()
, dict()
, tuple()
, str()
) van totalmente sobre la creación de un objeto, ya sea o no que en realidad tienen elementos?
No tengo idea de cómo difieren estos dos métodos, pero me encantaría descubrirlo. No pude encontrar una respuesta en los documentos o en SO, y la búsqueda de paréntesis vacíos resultó ser más problemático de lo que esperaba.
Obtuve mis resultados de tiempo llamando timeit.timeit("[]")
y timeit.timeit("list()")
, timeit.timeit("{}")
y timeit.timeit("dict()")
, para comparar listas y diccionarios, respectivamente. Estoy ejecutando Python 2.7.9.
Recientemente descubrí " ¿Por qué si es Verdadero más lento que si 1? " Que compara el rendimiento de if True
to if 1
y parece tocar un escenario literal versus global similar; quizás valga la pena considerar también.
fuente
()
y''
son especiales, ya que no solo están vacíos, son inmutables y, como tal, es una victoria fácil hacerlos solteros; ni siquiera construyen nuevos objetos, solo cargan el singleton para eltuple
/ vacíostr
. Técnicamente es un detalle de implementación, pero me cuesta imaginar por qué no almacenarían en caché el vacíotuple
/str
por razones de rendimiento. Por lo tanto, su intuición acerca de[]
y{}
devolver un literal de acciones estaba mal, pero se aplica a()
y''
.{}
más rápido que llamarset()
?Respuestas:
Porque
[]
y{}
son sintaxis literal . Python puede crear bytecode solo para crear la lista o los objetos del diccionario:list()
ydict()
son objetos separados. Sus nombres deben resolverse, la pila debe estar involucrada para enviar los argumentos, el marco debe almacenarse para recuperarlo más tarde y debe realizarse una llamada. Todo eso lleva más tiempo.Para el caso vacío, eso significa que tiene al menos un
LOAD_NAME
(que tiene que buscar a través del espacio de nombres global, así como el__builtin__
módulo ) seguido de unCALL_FUNCTION
, que tiene que preservar el marco actual:Puede cronometrar la búsqueda de nombres por separado con
timeit
:La discrepancia de tiempo probablemente es una colisión de hash de diccionario. Resta esos tiempos de los tiempos para llamar a esos objetos, y compara el resultado con los tiempos para usar literales:
Entonces, tener que llamar al objeto lleva unos
1.00 - 0.31 - 0.30 == 0.39
segundos adicionales por cada 10 millones de llamadas.Puede evitar el costo de búsqueda global aliasing los nombres globales como locales (usando una
timeit
configuración, todo lo que se une a un nombre es un local):pero nunca puedes superar ese
CALL_FUNCTION
costo.fuente
list()
requiere una búsqueda global y una llamada a función, pero se[]
compila en una sola instrucción. Ver:fuente
Porque
list
es una función para convertir digamos una cadena en un objeto de lista, mientras que[]
se usa para crear una lista desde el principio. Pruebe esto (puede tener más sentido para usted):Mientras
Te da una lista real que contiene todo lo que pones en ella.
fuente
[]
es más rápido quelist()
, no por qué['wham bam']
es más rápido quelist('wham bam')
.[]
/list()
es exactamente lo mismo que['wham']
/list('wham')
porque tienen las mismas diferencias variables1000/10
al igual que100/1
en matemáticas. En teoría, podría eliminarwham bam
y el hecho seguiría siendo el mismo, quelist()
intenta convertir algo llamando a un nombre de función, mientras[]
que simplemente convertirá la variable. Las llamadas a funciones son diferentes, sí, esto es solo una descripción lógica del problema, ya que, por ejemplo, un mapa de red de una empresa también es lógico de una solución / problema. Vota como quieras.Las respuestas aquí son geniales, al punto y cubren completamente esta pregunta. Dejaré un paso más abajo del código de bytes para aquellos interesados. Estoy usando el repositorio más reciente de CPython; las versiones anteriores se comportan de manera similar a este respecto, pero pueden existir ligeros cambios.
Aquí hay un desglose de la ejecución para cada uno de estos,
BUILD_LIST
por[]
yCALL_FUNCTION
paralist()
.La
BUILD_LIST
instrucción:Deberías ver el horror:
Terriblemente enrevesada, lo sé. Así de simple es:
PyList_New
(esto asigna principalmente la memoria para un nuevo objeto de lista),oparg
señalando el número de argumentos en la pila. Directo al grano.if (list==NULL)
.PyList_SET_ITEM
(una macro).¡No es de extrañar que sea rápido! Está hecho a medida para crear nuevas listas, nada más :-)
La
CALL_FUNCTION
instrucción:Esto es lo primero que ve cuando mira el manejo del código
CALL_FUNCTION
:Parece bastante inofensivo, ¿verdad? Bueno, no, desafortunadamente no, no
call_function
es un tipo directo que llamará a la función de inmediato, no puede. En su lugar, toma el objeto de la pila, toma todos los argumentos de la pila y luego cambia según el tipo de objeto; Es una:PyCFunction_Type
? No, se tratalist
,list
no es de tipoPyCFunction
PyMethodType
? No, ver anterior.PyFunctionType
? No, ver anterior.Estamos llamando al
list
tipo, el argumento pasado acall_function
esPyList_Type
. CPython ahora tiene que llamar a una función genérica para manejar cualquier objeto invocable llamado_PyObject_FastCallKeywords
, yay más llamadas a funciones.Esta función nuevamente realiza algunas comprobaciones para ciertos tipos de funciones (que no puedo entender por qué) y luego, después de crear un dict para kwargs si es necesario , pasa a llamar
_PyObject_FastCallDict
._PyObject_FastCallDict
finalmente nos lleva a alguna parte! Después de realizar incluso más comprobaciones , toma latp_call
ranuratype
de latype
que hemos pasado, es decir, tomatype.tp_call
. Luego procede a crear una tupla a partir de los argumentos pasados_PyStack_AsTuple
y, finalmente, ¡ finalmente se puede hacer una llamada !tp_call
, que coincidetype.__call__
toma el control y finalmente crea el objeto de lista. Llama a las listas__new__
que le correspondenPyType_GenericNew
y le asigna memoriaPyType_GenericAlloc
: Esta es en realidad la parte en la que se pone al díaPyList_New
, finalmente . Todo lo anterior es necesario para manejar objetos de forma genérica.Al final,
type_call
llamalist.__init__
e inicializa la lista con los argumentos disponibles, luego regresamos por donde vinimos. :-)Finalmente, recuerda
LOAD_NAME
, ese es otro tipo que contribuye aquí.Es fácil ver que, cuando se trata de nuestra entrada, Python generalmente tiene que saltar a través de los aros para descubrir realmente la
C
función adecuada para hacer el trabajo. No tiene la cortesía de llamarlo de inmediato porque es dinámico, alguien podría enmascararlist
( y mucha gente lo hace ) y se debe tomar otro camino.Aquí es donde
list()
pierde mucho: la exploración de Python debe hacer para descubrir qué diablos debería hacer.La sintaxis literal, por otro lado, significa exactamente una cosa; no se puede cambiar y siempre se comporta de manera predeterminada.
Nota al pie: Todos los nombres de funciones están sujetos a cambios de una versión a otra. El punto sigue en pie y lo más probable es que permanezca en cualquier versión futura, es la búsqueda dinámica que ralentiza las cosas.
fuente
La razón más importante es que Python se trata
list()
como una función definida por el usuario, lo que significa que puede interceptarla aliasando algo máslist
y hacer algo diferente (como usar su propia lista subclasificada o tal vez una deque).Inmediatamente crea una nueva instancia de una lista integrada con
[]
.Mi explicación busca darte la intuición para esto.
Explicación
[]
se conoce comúnmente como sintaxis literal.En la gramática, esto se conoce como "visualización de lista". De los documentos :
En resumen, esto significa que
list
se crea un objeto de tipo incorporado .No hay forma de eludir esto, lo que significa que Python puede hacerlo tan rápido como sea posible.
Por otro lado,
list()
se puede interceptar creando un generador incorporadolist
utilizando el constructor de la lista integrada.Por ejemplo, supongamos que queremos que nuestras listas se creen ruidosamente:
Luego podríamos interceptar el nombre
list
en el ámbito global del nivel del módulo, y luego cuando creamos unlist
, en realidad creamos nuestra lista de subtipos:Del mismo modo, podríamos eliminarlo del espacio de nombres global
y ponerlo en el espacio de nombres incorporado:
Y ahora:
Y tenga en cuenta que la visualización de la lista crea una lista incondicionalmente:
Probablemente solo hagamos esto temporalmente, así que deshazcamos nuestros cambios: primero elimine el nuevo
List
objeto de los archivos incorporados:Oh, no, perdimos el rastro del original.
No se preocupe, todavía podemos obtenerlo
list
: es el tipo de una lista literal:Entonces...
Como hemos visto, podemos sobrescribir
list
, pero no podemos interceptar la creación del tipo literal. Cuando usamoslist
tenemos que hacer las búsquedas para ver si hay algo allí.Luego tenemos que llamar a cualquier llamada que hayamos buscado. De la gramática:
Podemos ver que hace lo mismo con cualquier nombre, no solo con la lista:
Para
[]
no hay llamada de función en el nivel de código de bytes Python:Simplemente va directamente a la construcción de la lista sin búsquedas ni llamadas a nivel de bytecode.
Conclusión
Hemos demostrado que
list
puede ser interceptado con código de usuario utilizando las reglas de alcance, y quelist()
busca un invocable y luego lo llama.Mientras que
[]
es una visualización de lista, o un literal, y por lo tanto evita la búsqueda de nombre y la llamada a función.fuente
list
y el compilador de Python no puede estar seguro de si realmente devolverá una lista vacía.