“Comprensiones de tuplas” y el operador estrella desempaquetar / desempaquetar *

Acabo de leer la pregunta ¿Por qué no hay comprensión de tuplas en Python?

En los comentarios de la respuesta aceptada , se afirma que no hay verdaderas “comprensiones de tuplas”. En cambio, nuestra opción actual es usar una expresión generadora y pasar el objeto generador resultante al constructor de tuplas:

tuple(thing for thing in things) 

Alternativamente, podemos crear una lista usando una comprensión de lista y luego pasar la lista al constructor de tuplas:

 tuple([thing for thing in things]) 

Por último, y al contrario de la respuesta aceptada, una respuesta más reciente indicó que las comprensiones de tuplas son realmente una cosa (desde Python 3.5) utilizando la siguiente syntax:

 *(thing for thing in things), 

Actualizar:

Como era de esperar, la lista de comprensión es mucho más rápida. Sin embargo, no entiendo por qué el primero es más rápido que el tercero. ¿Alguna idea?

 >>> from timeit import timeit >>> a = 'tuple(i for i in range(10000))' >>> b = 'tuple([i for i in range(10000)])' >>> c = '*(i for i in range(10000)),' >>> print('A:', timeit(a, number=1000000)) >>> print('B:', timeit(b, number=1000000)) >>> print('C:', timeit(c, number=1000000)) A: 438.98362647295824 B: 271.7554752581845 C: 455.59842588083677 

Para mí, parece que el segundo ejemplo es también uno donde se crea primero un objeto generador. ¿Es esto correcto?

Sí, tiene razón, verifique el código de bytes de CPython:

 >>> import dis >>> dis.dis("*(thing for thing in thing),") 1 0 LOAD_CONST 0 ( at 0x7f56e9347ed0, file "", line 1>) 2 LOAD_CONST 1 ('') 4 MAKE_FUNCTION 0 6 LOAD_NAME 0 (thing) 8 GET_ITER 10 CALL_FUNCTION 1 12 BUILD_TUPLE_UNPACK 1 14 POP_TOP 16 LOAD_CONST 2 (None) 18 RETURN_VALUE 

¿Hay alguna diferencia entre estas expresiones en términos de lo que sucede detrás de la escena? En términos de rendimiento? Supongo que el primero y el tercero podrían tener problemas de latencia, mientras que el segundo podría tener problemas de memoria (como se explica en los comentarios vinculados).

Mis tiempos sugieren que el primer 1 es un poco más rápido, probablemente porque el desempaquetado es más costoso a través de BUILD_TUPLE_UNPACK que la llamada tuple() :

 >>> from timeit import timeit >>> def f1(): tuple(thing for thing in range(100000)) ... >>> def f2(): *(thing for thing in range(100000)), ... >>> timeit(lambda: f1(), number=100) 0.5535585517063737 >>> timeit(lambda: f2(), number=100) 0.6043887557461858 

Comparando el primero y el último, ¿cuál es más pythonico?

El primero me parece mucho más legible, y también funcionará en diferentes versiones de Python.