loop - python optimize imports
¿Las declaraciones de importación siempre deben estar en la parte superior de un módulo? (18)
PEP 08 establece:
Las importaciones siempre se colocan en la parte superior del archivo, justo después de los comentarios y las cadenas de documentación del módulo, y antes de las globales y constantes del módulo.
Sin embargo, si la clase / método / función que estoy importando solo se usa en casos raros, ¿seguramente será más eficiente realizar la importación cuando sea necesario?
No es esto
class SomeClass(object):
def not_often_called(self)
from datetime import datetime
self.datetime = datetime.now()
Más eficiente que esto?
from datetime import datetime
class SomeClass(object):
def not_often_called(self)
self.datetime = datetime.now()
Además de las excelentes respuestas ya dadas, vale la pena señalar que la colocación de las importaciones no es simplemente una cuestión de estilo. A veces, un módulo tiene dependencias implícitas que deben importarse o inicializarse primero, y una importación de alto nivel podría dar lugar a violaciones del orden de ejecución requerido.
Este problema a menudo surge en la API de Python de Apache Spark, donde necesita inicializar SparkContext antes de importar cualquier paquete o módulo pyspark. Es mejor colocar las importaciones de pyspark en un ámbito donde se garantice que el SparkContext esté disponible.
Aquí hay un ejemplo donde todas las importaciones están en la parte superior (esta es la única vez que he necesitado hacer esto). Quiero poder terminar un subproceso tanto en Un * x como en Windows.
import os
# ...
try:
kill = os.kill # will raise AttributeError on Windows
from signal import SIGTERM
def terminate(process):
kill(process.pid, SIGTERM)
except (AttributeError, ImportError):
try:
from win32api import TerminateProcess # use win32api if available
def terminate(process):
TerminateProcess(int(process._handle), -1)
except ImportError:
def terminate(process):
raise NotImplementedError # define a dummy function
(En revisión: lo que dijo John Millikin .)
Curt hace un buen punto: la segunda versión es más clara y fallará en el momento de la carga en lugar de hacerlo más tarde e inesperadamente.
Normalmente no me preocupo por la eficiencia de cargar módulos, ya que (a) es bastante rápido y (b) en su mayoría solo ocurre al inicio.
Si tiene que cargar módulos pesados en momentos inesperados, probablemente tenga más sentido cargarlos dinámicamente con la función __import__
, y asegúrese de detectar ImportError
excepciones ImportError
y manejarlas de una manera razonable.
Es interesante que ni una sola respuesta mencionó el procesamiento paralelo hasta el momento, donde se puede REQUERIR que las importaciones estén en la función, cuando el código de la función serializada es lo que se está enviando a otros núcleos, por ejemplo, como en el caso de ipyparallel.
Es una compensación, que solo el programador puede decidir hacer.
El caso 1 guarda algo de memoria y tiempo de inicio al no importar el módulo datetime (y hacer la inicialización que requiera) hasta que sea necesario. Tenga en cuenta que hacer la importación ''solo cuando se llama'' también significa hacerlo ''cada vez que se llama'', por lo que cada llamada después de la primera sigue incurriendo en la sobrecarga adicional de realizar la importación.
El caso 2 ahorra tiempo de ejecución y latencia al importar fecha y hora de antemano para que not_often_called () regrese más rápidamente cuando se le llame, y también para no incurrir en la sobrecarga de una importación en cada llamada.
Además de la eficiencia, es más fácil ver las dependencias de los módulos por adelantado si las declaraciones de importación son ... por adelantado. Ocultarlos en el código puede hacer que sea más difícil encontrar fácilmente de qué módulos depende algo.
Personalmente, generalmente sigo el PEP, excepto para pruebas de unidad y que no quiero que siempre se carguen porque sé que no se van a usar, excepto el código de prueba.
Esto es como muchas otras optimizaciones: sacrificas algo de legibilidad por velocidad. Como mencionó John, si ha hecho su tarea de perfilar y ha encontrado que este es un cambio suficientemente útil y necesita la velocidad adicional, entonces hágalo. Probablemente sería bueno poner una nota con todas las demás importaciones:
from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below
He adoptado la práctica de poner todas las importaciones en las funciones que las utilizan, en lugar de en la parte superior del módulo.
El beneficio que obtengo es la capacidad de refactorizar de manera más confiable. Cuando muevo una función de un módulo a otro, sé que la función continuará funcionando con todo su legado de pruebas intactas. Si tengo mis importaciones en la parte superior del módulo, cuando muevo una función, encuentro que termino gastando mucho tiempo en completar y importar las importaciones del nuevo módulo. Un IDE de refactorización podría hacer esto irrelevante.
Hay una penalización de velocidad como se menciona en otros lugares. He medido esto en mi aplicación y me pareció insignificante para mis propósitos.
También es bueno poder ver todas las dependencias de los módulos por adelantado sin tener que recurrir a la búsqueda (por ejemplo, grep). Sin embargo, la razón por la que me importan las dependencias de los módulos es generalmente porque estoy instalando, refactorizando o moviendo un sistema completo que comprende varios archivos, no solo un solo módulo. En ese caso, voy a realizar una búsqueda global de todas formas para asegurarme de tener las dependencias a nivel de sistema. Por lo tanto, no he encontrado importaciones globales que me ayuden a entender un sistema en la práctica.
Por lo general, pongo la importación de sys
dentro de la if __name__==''__main__''
y luego paso los argumentos (como sys.argv[1:]
) a una función main()
. Esto me permite usar main
en un contexto donde sys
no ha sido importado.
La importación de módulos es bastante rápida, pero no instantánea. Esto significa que:
- Poner las importaciones en la parte superior del módulo está bien, porque es un costo trivial que solo se paga una vez.
- Poner las importaciones dentro de una función hará que las llamadas a esa función tomen más tiempo.
Así que si te importa la eficiencia, coloca las importaciones en la parte superior. Solo muévalos a una función si su perfil muestra ayuda (¿ hizo un perfil para ver cuál es el mejor para mejorar el rendimiento, verdad?)
Las mejores razones que he visto para realizar importaciones perezosas son:
- Soporte de biblioteca opcional. Si su código tiene varias rutas que usan bibliotecas diferentes, no se rompa si no se instala una biblioteca opcional.
- En el
__init__.py
de un complemento, que puede ser importado pero no utilizado realmente. Algunos ejemplos son los complementos de Bazaar, que utilizan el marco de cargabzrlib
debzrlib
.
La inicialización del módulo solo ocurre una vez, en la primera importación. Si el módulo en cuestión es de la biblioteca estándar, es probable que también lo importe de otros módulos en su programa. Para un módulo tan frecuente como datetime, también es probable que sea una dependencia para una gran cantidad de otras bibliotecas estándar. La declaración de importación costaría muy poco ya que la intialización del módulo ya habría ocurrido. Todo lo que está haciendo en este punto es vincular el objeto de módulo existente al ámbito local.
Combine esa información con el argumento de la legibilidad y diría que es mejor tener la declaración de importación en el ámbito del módulo.
La mayoría de las veces, esto sería útil para la claridad y sensatez, pero no siempre es así. A continuación se muestran algunos ejemplos de circunstancias en las que las importaciones de módulos pueden vivir en otros lugares.
En primer lugar, podría tener un módulo con una prueba unitaria del formulario:
if __name__ == ''__main__'':
import foo
aa = foo.xyz() # initiate something for the test
En segundo lugar, es posible que tenga un requisito para importar condicionalmente un módulo diferente en tiempo de ejecución.
if [condition]:
import foo as plugin_api
else:
import bar as plugin_api
xx = plugin_api.Plugin()
[...]
Probablemente haya otras situaciones en las que podría realizar importaciones en otras partes del código.
La primera variante es de hecho más eficiente que la segunda cuando la función se llama cero o una vez. Sin embargo, con la segunda y posteriores invocaciones, el enfoque de "importar cada llamada" es en realidad menos eficiente. Vea este enlace para una técnica de carga lenta que combina lo mejor de ambos enfoques haciendo una "importación lenta".
Pero hay otras razones además de la eficiencia por las que puede preferir una sobre la otra. Un enfoque es que es mucho más claro para alguien que lee el código en cuanto a las dependencias que tiene este módulo. También tienen características de falla muy diferentes: la primera fallará en el momento de la carga si no hay un módulo "datetime", mientras que la segunda no fallará hasta que se llame al método.
Nota agregada: en IronPython, las importaciones pueden ser un poco más caras que en CPython porque el código se compila básicamente a medida que se importa.
Me gustaría mencionar uno de mis casos, muy similares a los mencionados por @John Millikin y @VK:
Importaciones opcionales
Hago análisis de datos con Jupyter Notebook, y uso la misma libreta IPython como plantilla para todos los análisis. En algunas ocasiones, necesito importar Tensorflow para hacer algunas ejecuciones rápidas del modelo, pero a veces trabajo en lugares donde tensorflow no está configurado / la importación es lenta. En esos casos, encapsulo mis operaciones dependientes de Tensorflow en una función auxiliar, importo tensorflow dentro de esa función y lo vinculo a un botón.
De esta manera, podría hacer "reiniciar y ejecutar todo" sin tener que esperar a la importación, o tener que reanudar el resto de las celdas cuando falla.
Me sorprendió no ver los números de costos reales de los controles de carga repetidos ya publicados, aunque hay muchas explicaciones de qué esperar.
Si importa en la parte superior, toma el golpe de carga sin importar qué. Eso es bastante pequeño, pero generalmente en milisegundos, no en nanosegundos.
Si importa dentro de una función (es), entonces solo recibe el hit para cargar si y cuando se llama por primera vez a una de esas funciones. Como muchos han señalado, si eso no ocurre en absoluto, usted ahorra tiempo de carga. Pero si las funciones se llaman mucho, recibes un golpe repetido aunque mucho más pequeño (para comprobar que se ha cargado; no para recargar realmente). Por otro lado, como @aaronasterling señaló, también ahorras un poco porque la importación dentro de una función permite que la función utilice búsquedas de variables locales un poco más rápidas para identificar el nombre más adelante ( answer ).
Aquí están los resultados de una prueba simple que importa algunas cosas desde dentro de una función. Los tiempos informados (en Python 2.7.14 en un Intel Core i7 a 2.3 GHz) se muestran a continuación (la segunda llamada es más consistente que la llamada posterior, aunque no sé por qué).
0 foo: 14429.0924 µs
1 foo: 63.8962 µs
2 foo: 10.0136 µs
3 foo: 7.1526 µs
4 foo: 7.8678 µs
0 bar: 9.0599 µs
1 bar: 6.9141 µs
2 bar: 7.1526 µs
3 bar: 7.8678 µs
4 bar: 7.1526 µs
El código:
from __future__ import print_function
from time import time
def foo():
import collections
import re
import string
import math
import subprocess
return
def bar():
import collections
import re
import string
import math
import subprocess
return
t0 = time()
for i in xrange(5):
foo()
t1 = time()
print(" %2d foo: %12.4f /xC2/xB5s" % (i, (t1-t0)*1E6))
t0 = t1
for i in xrange(5):
bar()
t1 = time()
print(" %2d bar: %12.4f /xC2/xB5s" % (i, (t1-t0)*1E6))
t0 = t1
No aspiro a dar una respuesta completa, porque otros ya lo han hecho muy bien. Solo quiero mencionar un caso de uso cuando me parece especialmente útil para importar módulos dentro de las funciones. Mi aplicación utiliza paquetes y módulos de Python almacenados en cierta ubicación como complementos. Durante el inicio de la aplicación, la aplicación recorre todos los módulos en la ubicación y los importa, luego busca dentro de los módulos y si encuentra algunos puntos de montaje para los complementos (en mi caso, es una subclase de una determinada clase base que tiene un único ID) los registra. El número de complementos es grande (ahora docenas, pero quizás cientos en el futuro) y cada uno de ellos se usa muy raramente. Tener importaciones de bibliotecas de terceros en la parte superior de mis módulos de plugin fue un poco penoso durante el inicio de la aplicación. Especialmente algunas bibliotecas de terceros son muy pesadas para importar (por ejemplo, la importación de plotly incluso intenta conectarse a Internet y descargar algo que estaba agregando aproximadamente un segundo al inicio). Al optimizar las importaciones (llamándolos solo en las funciones donde se usan) en los complementos, logré reducir el inicio de 10 segundos a unos 2 segundos. Esa es una gran diferencia para mis usuarios.
Así que mi respuesta es no, no siempre coloque las importaciones en la parte superior de sus módulos.
No me preocuparía por la eficiencia de cargar el módulo por adelantado demasiado. La memoria ocupada por el módulo no será muy grande (suponiendo que sea lo suficientemente modular) y el costo de inicio será despreciable.
En la mayoría de los casos, desea cargar los módulos en la parte superior del archivo fuente. Para alguien que lee su código, es mucho más fácil saber qué función u objeto proviene de qué módulo.
Una buena razón para importar un módulo en otra parte del código es si se usa en una declaración de depuración.
Por ejemplo:
do_something_with_x(x0
Podría depurar esto con:
from pprint import pprint
pprint(x)
do_something_with_x(x)
Por supuesto, la otra razón para importar módulos en otra parte del código es si necesita importarlos dinámicamente. Esto se debe a que prácticamente no tienes otra opción.
No me preocuparía por la eficiencia de cargar el módulo por adelantado demasiado. La memoria ocupada por el módulo no será muy grande (suponiendo que sea lo suficientemente modular) y el costo de inicio será despreciable.
Poner la declaración de importación dentro de una función puede evitar dependencias circulares. Por ejemplo, si tiene 2 módulos, X.py e Y.py, y ambos necesitan importarse entre sí, esto causará una dependencia circular cuando importe uno de los módulos que causan un bucle infinito. Si mueve la declaración de importación en uno de los módulos, no intentará importar el otro módulo hasta que se llame a la función, y ese módulo ya se importará, por lo que no hay un bucle infinito. Lea aquí para más información - effbot.org/zone/import-confusion.htm
Puede haber una ganancia de rendimiento al importar variables / alcance local dentro de una función. Esto depende del uso de lo importado dentro de la función. Si realiza un bucle muchas veces y accede a un objeto global de módulo, importarlo como local puede ayudar.
test.py
X=10
Y=11
Z=12
def add(i):
i = i + 10
runlocal.py
from test import add, X, Y, Z
def callme():
x=X
y=Y
z=Z
ladd=add
for i in range(100000000):
ladd(i)
x+y+z
callme()
run.py
from test import add, X, Y, Z
def callme():
for i in range(100000000):
add(i)
X+Y+Z
callme()
Un tiempo en Linux muestra una pequeña ganancia.
/usr/bin/time -f "/t%E real,/t%U user,/t%S sys" python run.py
0:17.80 real, 17.77 user, 0.01 sys
/tmp/test$ /usr/bin/time -f "/t%E real,/t%U user,/t%S sys" python runlocal.py
0:14.23 real, 14.22 user, 0.01 sys
Lo real es el reloj de pared. El usuario es tiempo en el programa. sys es hora de llamadas al sistema.
https://docs.python.org/3.5/reference/executionmodel.html#resolution-of-names
Solo para completar la respuesta de Moe y la pregunta original:
Cuando tenemos que lidiar con dependencias circulares podemos hacer algunos "trucos". Asumiendo que estamos trabajando con los módulos a.py
y b.py
que contienen x()
y b y()
, respectivamente. Entonces:
- Podemos mover una de las
from imports
en la parte inferior del módulo. - Podemos mover una de las
from imports
dentro de la función o método que realmente requiere la importación (esto no siempre es posible, ya que se puede usar desde varios lugares). - Podemos cambiar uno de los dos
from imports
para que sea una importación que parezca:import a
Entonces, para concluir. Si no está tratando con dependencias circulares y haciendo algún tipo de truco para evitarlas, entonces es mejor colocar todas sus importaciones en la parte superior debido a las razones ya explicadas en otras respuestas a esta pregunta. Y, por favor, cuando hagas estos "trucos" incluye un comentario, ¡siempre es bienvenido! :)