una - ¿Las listas de comprensión son sintácticas de azúcar para `list(expresión del generador)` en Python 3?
metodos de listas en python (4)
Ambas formas crean y llaman a una función anónima. Sin embargo, el formulario de list(...)
crea una función de generador y pasa el generador-iterador devuelto a la list
, mientras que con el formulario, la [...]
función anónima construye la lista directamente con los LIST_APPEND
.
El siguiente código obtiene la salida de descompilación de las funciones anónimas para una comprensión de ejemplo y su correspondiente genexp-pass-to- list
:
import dis
def f():
[x for x in []]
def g():
list(x for x in [])
dis.dis(f.__code__.co_consts[1])
dis.dis(g.__code__.co_consts[1])
La salida para la comprensión es
4 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (x)
12 LOAD_FAST 1 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
La salida para el genexp es
7 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 11 (to 17)
6 STORE_FAST 1 (x)
9 LOAD_FAST 1 (x)
12 YIELD_VALUE
13 POP_TOP
14 JUMP_ABSOLUTE 3
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
En Python 3, ¿es una comprensión de lista simplemente azúcar sintáctica para una expresión generadora incorporada a la función de list
?
Por ejemplo, es el siguiente código:
squares = [x**2 for x in range(1000)]
en realidad convertido en el fondo en el siguiente?
squares = list(x**2 for x in range(1000))
Sé que la salida es idéntica, y Python 3 corrige los sorprendentes efectos secundarios de los espacios de nombres circundantes que las listas de comprensión tenían, pero en términos de lo que el intérprete de CPython hace bajo el capó, es el primero convertido al último, o hay alguna diferencia ¿En cómo se ejecuta el código?
Fondo
Encontré esta reclamación de equivalencia en la sección de comentarios de esta pregunta , y una búsqueda rápida en Google mostró la misma reclamación que se hizo here .
También se mencionó esto en la documentación de Novedades en Python 3.0 , pero la redacción es algo vaga:
También tenga en cuenta que las comprensiones de lista tienen diferentes semánticas: están más cerca del azúcar sintáctica para una expresión generadora dentro de un constructor de lista (), y en particular las variables de control de bucle ya no se filtran en el ámbito circundante.
Ambos funcionan de manera diferente, la versión de comprensión de lista toma la ventaja del LIST_APPEND
byte especial LIST_APPEND
que llama a PyList_Append directamente por nosotros. Por lo tanto, evita la búsqueda de atributos para list.append
y función call a nivel de Python.
>>> def func_lc():
[x**2 for x in y]
...
>>> dis.dis(func_lc)
2 0 LOAD_CONST 1 (<code object <listcomp> at 0x10d3c6780, file "<ipython-input-42-ead395105775>", line 2>)
3 LOAD_CONST 2 (''func_lc.<locals>.<listcomp>'')
6 MAKE_FUNCTION 0
9 LOAD_GLOBAL 0 (y)
12 GET_ITER
13 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
16 POP_TOP
17 LOAD_CONST 0 (None)
20 RETURN_VALUE
>>> lc_object = list(dis.get_instructions(func_lc))[0].argval
>>> lc_object
<code object <listcomp> at 0x10d3c6780, file "<ipython-input-42-ead395105775>", line 2>
>>> dis.dis(lc_object)
2 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 16 (to 25)
9 STORE_FAST 1 (x)
12 LOAD_FAST 1 (x)
15 LOAD_CONST 0 (2)
18 BINARY_POWER
19 LIST_APPEND 2
22 JUMP_ABSOLUTE 6
>> 25 RETURN_VALUE
Por otro lado, la versión list()
simplemente pasa el objeto generador al método __init__
la lista, que luego llama internamente a su método extend
. Como el objeto no es una lista o tupla, CPython obtiene primero su iterador y luego simplemente agrega los elementos a la lista hasta que se agote el iterador :
>>> def func_ge():
list(x**2 for x in y)
...
>>> dis.dis(func_ge)
2 0 LOAD_GLOBAL 0 (list)
3 LOAD_CONST 1 (<code object <genexpr> at 0x10cde6ae0, file "<ipython-input-41-f9a53483f10a>", line 2>)
6 LOAD_CONST 2 (''func_ge.<locals>.<genexpr>'')
9 MAKE_FUNCTION 0
12 LOAD_GLOBAL 1 (y)
15 GET_ITER
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
22 POP_TOP
23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> ge_object = list(dis.get_instructions(func_ge))[1].argval
>>> ge_object
<code object <genexpr> at 0x10cde6ae0, file "<ipython-input-41-f9a53483f10a>", line 2>
>>> dis.dis(ge_object)
2 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 15 (to 21)
6 STORE_FAST 1 (x)
9 LOAD_FAST 1 (x)
12 LOAD_CONST 0 (2)
15 BINARY_POWER
16 YIELD_VALUE
17 POP_TOP
18 JUMP_ABSOLUTE 3
>> 21 LOAD_CONST 1 (None)
24 RETURN_VALUE
>>>
Comparaciones de tiempo:
>>> %timeit [x**2 for x in range(10**6)]
1 loops, best of 3: 453 ms per loop
>>> %timeit list(x**2 for x in range(10**6))
1 loops, best of 3: 478 ms per loop
>>> %%timeit
out = []
for x in range(10**6):
out.append(x**2)
...
1 loops, best of 3: 510 ms per loop
Los bucles normales son ligeramente lentos debido a la lenta búsqueda de atributos. Lo caché y otra vez.
>>> %%timeit
out = [];append=out.append
for x in range(10**6):
append(x**2)
...
1 loops, best of 3: 467 ms per loop
Aparte del hecho de que la comprensión de la lista ya no filtra las variables, una diferencia más es que algo como esto ya no es válido:
>>> [x**2 for x in 1, 2, 3] # Python 2
[1, 4, 9]
>>> [x**2 for x in 1, 2, 3] # Python 3
File "<ipython-input-69-bea9540dd1d6>", line 1
[x**2 for x in 1, 2, 3]
^
SyntaxError: invalid syntax
>>> [x**2 for x in (1, 2, 3)] # Add parenthesis
[1, 4, 9]
>>> for x in 1, 2, 3: # Python 3: For normal loops it still works
print(x**2)
...
1
4
9
En realidad, puede demostrar que los dos pueden tener diferentes resultados para demostrar que son inherentemente diferentes:
>>> list(next(iter([])) if x > 3 else x for x in range(10))
[0, 1, 2, 3]
>>> [next(iter([])) if x > 3 else x for x in range(10)]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <listcomp>
StopIteration
La expresión dentro de la comprensión no se trata como un generador, ya que la comprensión no maneja la StopIteration
, mientras que el constructor de list
sí lo hace.
No son lo mismo, list()
evaluará lo que se le haya dado una vez que lo que está entre paréntesis haya terminado de ejecutarse, no antes.
El []
en python es un poco mágico, le dice a python que incluya todo lo que hay dentro de él como una lista, más como una sugerencia tipográfica para el idioma.