¿Las declaraciones de importación siempre deben estar en la parte superior de un módulo?

StackOverflow https://stackoverflow.com/questions/128478

Pregunta

PEP 08 declara:

  

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 excepcionales, ¿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()
¿Fue útil?

Solución

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.

Entonces, si te importa la eficiencia, coloca las importaciones en la parte superior. Solo muévalos a una función si su perfil muestra ayuda (usted hizo el perfil para ver cuál es la mejor forma de 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 utilizan bibliotecas diferentes, no las rompa si no hay una biblioteca opcional instalada.
  • En el __init__.py de un complemento, que se puede importar pero no se usa realmente. Algunos ejemplos son los complementos de Bazaar, que utilizan el marco de carga lenta de bzrlib .

Otros consejos

Poner la declaración de importación dentro de una función puede evitar las 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 la función, y ese módulo ya se importará, por lo que no hay un bucle infinito. Lea aquí para obtener más información, effbot.org/zone/import-confusion.htm

He adoptado la práctica de colocar todas las importaciones en las funciones que las utilizan, en lugar de situarse 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 minimizar 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 otra parte. He medido esto en mi aplicación y me ha parecido insignificante para mis propósitos.

También es bueno poder ver todas las dependencias de los módulos desde el principio 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 todos modos para asegurarme de que tengo 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 si __name __ == '__ main__' comprueba 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 se ha importado.

La mayoría de las veces, esto sería útil para la claridad y sensatez, pero no siempre es así. A continuación, se incluyen algunos ejemplos de circunstancias en las que las importaciones de módulos pueden vivir en otro lugar.

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 deba importar condicionalmente algún 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 hay 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, " importa cada llamada " El enfoque es en realidad menos eficiente. Vea this link para un modelo de la carga y descarga. se aproxima haciendo una " importación diferida " ;.

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 " datetime " módulo mientras que el segundo no fallará hasta que se llame el 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.

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 estar seguro para capturar ImportError excepciones, y manejarlas de una manera razonable.

No me preocuparía la eficiencia de cargar el módulo desde el principio. La memoria ocupada por el módulo no será muy grande (suponiendo que sea lo suficientemente modular) y el costo de inicio será insignificante.

En la mayoría de los casos, desea cargar los módulos en la parte superior del archivo fuente. Para alguien que lee tu 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(x)

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 la eficiencia de cargar el módulo desde el principio. La memoria ocupada por el módulo no será muy grande (suponiendo que sea lo suficientemente modular) y el costo de inicio será insignificante.

Es una compensación, que solo el programador puede decidir hacer.

El caso 1 ahorra algo de memoria y tiempo de inicio al no importar el módulo datetime (y hacer la inicialización que necesite) 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 guarda un poco de tiempo de ejecución y latencia al importar datetime de antemano para que not_often_called () regrese más rápidamente cuando se llame a , y también al 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.

Generalmente, sigo el PEP, a excepción de cosas como las pruebas unitarias y las que no quiero que siempre se carguen porque , no se van a utilizar, excepto para el código de prueba.

Aquí hay un ejemplo donde todas las importaciones están en la parte superior (esta es la única vez que necesito 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 John Millikin dijo.)

Esto es como muchas otras optimizaciones: sacrificas algo de legibilidad por velocidad. Como mencionó John, si ha hecho su tarea de creación de perfiles y encontró que esto es un cambio lo suficientemente útil como 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

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 legibilidad y diría que es mejor tener la declaración de importación en el alcance del módulo.

Solo para completar la respuesta de Moe y la pregunta original:

Cuando tenemos que lidiar con dependencias circulares, podemos hacer algunos " trucos " ;. Suponiendo que estamos trabajando con los módulos a.py y b. py que contienen x () y b y () , respectivamente. Entonces:

  1. Podemos mover uno de los de las importaciones al final del módulo.
  2. Podemos mover uno de los de las importaciones dentro de la función o método que realmente requiere la importación (esto no siempre es posible, ya que puede usarlo desde varios lugares).
  3. Podemos cambiar uno de los dos de importaciones para que sea una importación que parezca: importar un

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, al hacer esto " trucos " incluir un comentario, siempre es bienvenido! :)

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 aparece en la API de Python de Apache Spark, donde debe 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.

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 importantes para importar (por ejemplo, la importación de plotly incluso intenta conectarse a Internet y descargar algo que se 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.

Me sorprendió no ver los números de costos reales de los controles de carga repetidos ya publicados, aunque hay muchas explicaciones buenas sobre 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 se llama mucho a la (s) función (es), recibes un golpe repetido aunque mucho más pequeño (para comprobar que se ha cargado , no para volver a cargarlo). 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 use búsquedas local variable ligeramente más rápidas para identificar el nombre más adelante (http://stackoverflow.com/questions/477096/python-import-coding-style/4789963#4789963 ).

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 que toma más que las llamadas posteriores parece ser coherente, 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

Es interesante que no haya una sola respuesta mencionada hasta ahora en el procesamiento paralelo, 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.

Puede haber una ganancia de rendimiento al importar variables / alcance local dentro de una función. Esto depende del uso de la cosa importada 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

real es 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

Me gustaría mencionar uno de mis casos, muy similares a los mencionados por @John Millikin y @ V.K. :

Importaciones opcionales

Hago análisis de datos con Jupyter Notebook, y uso el mismo cuaderno 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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top