¿Python optimiza una variable que solo se usa como valor de retorno?

106

¿Existe alguna diferencia fundamental entre los siguientes dos fragmentos de código? El primero asigna un valor a una variable en una función y luego devuelve esa variable. La segunda función simplemente devuelve el valor directamente.

¿Python los convierte en un código de bytes equivalente? ¿Es uno de ellos más rápido?

Caso 1 :

def func():
    a = 42
    return a

Caso 2 :

def func():
    return 42
Jayesh
fuente
5
Si usas dis.dis(..)en ambos ves que hay una diferencia , así que sí. Pero en la mayoría de las aplicaciones del mundo real , la sobrecarga de esto comparada con el retraso del procesamiento en la función no es tanto.
Willem Van Onsem
4
Hay dos posibilidades: (a) Va a llamar a esta función muchas (es decir, al menos un millón) de veces en un bucle cerrado. En ese caso, no debería llamar a una función de Python en absoluto, sino que debería vectorizar su ciclo usando algo como la biblioteca numpy. (b) No va a llamar a esta función tantas veces. En ese caso, la diferencia de velocidad entre estas funciones es demasiado pequeña para que valga la pena preocuparse.
Arthur Tacca

Respuestas:

138

No, no es así .

La compilación al código de bytes CPython solo se pasa a través de un pequeño optimizador de mirilla que está diseñado para realizar solo optimizaciones básicas (consulte test_peepholer.py en el conjunto de pruebas para obtener más información sobre estas optimizaciones).

Para ver lo que realmente va a suceder, use dis* para ver las instrucciones generadas. Para la primera función, que contiene la asignación:

from dis import dis
dis(func)
  2           0 LOAD_CONST               1 (42)
              2 STORE_FAST               0 (a)

  3           4 LOAD_FAST                0 (a)
              6 RETURN_VALUE

Mientras que, para la segunda función:

dis(func2)
  2           0 LOAD_CONST               1 (42)
              2 RETURN_VALUE

Se utilizan dos instrucciones más (rápidas) en la primera: STORE_FASTy LOAD_FAST. Estos hacen un almacenamiento rápido y capturan el valor en la fastlocalsmatriz del marco de ejecución actual. Luego, en ambos casos, RETURN_VALUEse realiza a. Entonces, el segundo es ligeramente más rápido debido a que se necesitan menos comandos para ejecutar.

En general, tenga en cuenta que el compilador CPython es conservador en las optimizaciones que realiza. No es ni intenta ser tan inteligente como otros compiladores (que, en general, también tienen mucha más información con la que trabajar). El objetivo principal del diseño, además de ser obviamente correcto, es a) mantenerlo simple yb) ser lo más rápido posible al compilarlos para que ni siquiera se dé cuenta de que existe una fase de compilación.

Al final, no deberías preocuparte por pequeños problemas como este. El beneficio de la velocidad es pequeño, constante y empequeñecido por la sobrecarga introducida por el hecho de que se interpreta Python.

* dises un pequeño módulo de Python que desensambla su código, puede usarlo para ver el código de bytes de Python que ejecutará la VM.

Nota: Como también se indicó en un comentario de @Jorn Vernee, esto es específico de la implementación CPython de Python. Otras implementaciones pueden hacer optimizaciones más agresivas si así lo desean, CPython no lo hace.

Dimitris Fasarakis Hilliard
fuente
11
No soy una persona de Python (c ++), así que no sé cómo funciona bajo el capó, pero ¿no debería optimizarse el primer caso para el segundo caso? Un compilador de C ++ decente haría esa optimización.
NathanOliver
7
@NathanOliver realmente no lo hace, Python hará lo que se le dice aquí sin siquiera intentar jugar de manera inteligente.
Dimitris Fasarakis Hilliard
80
El hecho de que la suposición inteligente y perfectamente razonable de @ NathanOliver sobre la respuesta a esta pregunta sea completamente incorrecta es, a mis ojos, una prueba de que esta no es una pregunta "autoexplicativa", "sin sentido", "estúpida" que pueda responderse. "tomando un momento para pensar", como nos quiere hacer creer TigerhawkT3. Es una pregunta válida e interesante cuya respuesta no estaba seguro a pesar de haber sido un programador profesional de Python durante años.
Mark Amery
El compilador de Python es, en el mejor de los casos, "conservador", no "muy conservador". El objetivo principal del diseño no es ser "lo más rápido posible ... para que ni siquiera se dé cuenta de que existe una fase de compilación". Eso es secundario, después de "mantenerlo simple". Una función con constantes grandes como "1 << (2 ** 34)" y "b'x '* (2 ** 32)" tarda varios segundos en compilarse y genera constantes del tamaño de GB, incluso si la función nunca se correr. La cadena grande incluso será descartada por el compilador. Se han rechazado las correcciones propuestas para estos casos porque harían que el compilador fuera demasiado complejo.
Andrew Dalke
@AndrewDalke, gracias por el comentario interno sobre esto, modifiqué la redacción para abordar los problemas que señaló.
Dimitris Fasarakis Hilliard
3

Ambos son básicamente iguales, excepto que en el primer caso el objeto 42simplemente se asigna a una variable nombrada ao, en otras palabras, los nombres (es decir a) se refieren a valores (es decir 42). No realiza ninguna asignación técnicamente, en el sentido de que nunca copia ningún dato.

Mientras returning, este enlace con nombre ase devuelve en el primer caso, mientras que el objeto 42se devuelve en el segundo caso.

Para leer más, consulte este gran artículo de Ned Batchelder

kmario23
fuente