Comparación: statement de importación vs función __import__

Como seguimiento de la pregunta Uso de __import__() en casos normales , realizo algunas pruebas y encontré resultados sorprendentes.

Aquí estoy comparando el tiempo de ejecución de una statement de import clásica y una llamada a la __import__ incorporada __import__ . Para este propósito, uso el siguiente script en modo interactivo:

 import timeit def test(module): t1 = timeit.timeit("import {}".format(module)) t2 = timeit.timeit("{0} = __import__('{0}')".format(module)) print("import statement: ", t1) print("__import__ function:", t2) print("t(statement) {} t(function)".format("<" if t1 ")) 

Como en la pregunta vinculada, aquí está la comparación al importar sys , junto con algunos otros módulos estándar:

 >>> test('sys') import statement: 0.319865173171288 __import__ function: 0.38428380458522987 t(statement) >> test('math') import statement: 0.10262547545597034 __import__ function: 0.16307580163101054 t(statement) >> test('os') import statement: 0.10251490255312312 __import__ function: 0.16240755669640627 t(statement) >> test('threading') import statement: 0.11349136644972191 __import__ function: 0.1673617034957573 t(statement) < t(function) 

Hasta ahora, bien, la import es más rápida que __import__() . Esto tiene sentido para mí, porque como escribí en la publicación vinculada, me parece lógico que la instrucción IMPORT_NAME esté optimizada en comparación con CALL_FUNCTION , cuando esta última resulta en una llamada a __import__ .

Pero cuando se trata de módulos menos estándar, los resultados se invierten:

 >>> test('numpy') import statement: 0.18907936340054476 __import__ function: 0.15840019037769792 t(statement) > t(function) >>> test('tkinter') import statement: 0.3798560809537861 __import__ function: 0.15899962771786136 t(statement) > t(function) >>> test("pygame") import statement: 0.6624641952621317 __import__ function: 0.16268579177259568 t(statement) > t(function) 

¿Cuál es la razón detrás de esta diferencia en los tiempos de ejecución? ¿Cuál es la razón real por la que la statement de import es más rápida en los módulos estándar? Por otro lado, ¿por qué la función __import__ es más rápida con otros módulos?

Las pruebas conducen con Python 3.6.

timeit mide el tiempo total de ejecución, pero la primera importación de un módulo, ya sea a través de import o __import__ , es más lenta que las posteriores, porque es la única que realiza la inicialización del módulo. Tiene que buscar en el sistema de archivos los archivos del módulo, cargar el código fuente del módulo (el más lento) o el bytecode creado anteriormente (lento pero un poco más rápido que analizar los archivos .py ) o la biblioteca compartida (para las extensiones C), ejecutar el código de inicialización, y almacenar el objeto de módulo en sys.modules . Las importaciones posteriores pueden omitir todo eso y recuperar el objeto de módulo de sys.modules .

Si invierte el orden los resultados serán diferentes:

 import timeit def test(module): t2 = timeit.timeit("{0} = __import__('{0}')".format(module)) t1 = timeit.timeit("import {}".format(module)) print("import statement: ", t1) print("__import__ function:", t2) print("t(statement) {} t(function)".format("<" if t1 < t2 else ">")) test('numpy') import statement: 0.4611093703134608 __import__ function: 1.275512785926014 t(statement) < t(function) 

La mejor manera de obtener resultados no sesgados es importarlo una vez y luego hacer los tiempos:

 import timeit def test(module): exec("import {}".format(module)) t2 = timeit.timeit("{0} = __import__('{0}')".format(module)) t1 = timeit.timeit("import {}".format(module)) print("import statement: ", t1) print("__import__ function:", t2) print("t(statement) {} t(function)".format("<" if t1 < t2 else ">")) test('numpy') import statement: 0.4826306561727307 __import__ function: 0.9192819125911029 t(statement) < t(function) 

Entonces, sí, la import siempre es más rápida que __import__ .

Recuerde que todos los módulos se almacenan en caché en sys.modules después de la primera importación, por lo que el tiempo …

De todos modos, mis resultados se ven así:

 #!/bin/bash itest() { echo -n "import $1: " python3 -m timeit "import $1" echo -n "__import__('$1'): " python3 -m timeit "__import__('$1')" } itest "sys" itest "math" itest "six" itest "PIL" 
  • import sys : 0.481
  • __import__('sys') : 0.586
  • import math : 0.163
  • __import__('math') : 0.247
  • import six : 0.157
  • __import__('six') : 0.273
  • import PIL : 0.162
  • __import__('PIL') : 0.265

introduzca la descripción de la imagen aquí

¿Cuál es la razón detrás de esta diferencia en los tiempos de ejecución?

La statement de importación tiene un camino bastante sencillo para pasar. Conduce a IMPORT_NAME que llama a import_name e importa el módulo dado (si no se ha __import__ el nombre __import__ ):

 dis('import math') 1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (None) 4 IMPORT_NAME 0 (math) 6 STORE_NAME 0 (math) 8 LOAD_CONST 1 (None) 10 RETURN_VALUE 

__import__ , por otro lado, sigue los pasos de llamada a funciones genéricas que todas las funciones realizan a través de CALL_FUNCTION :

 dis('__import__(math)') 1 0 LOAD_NAME 0 (__import__) 2 LOAD_NAME 1 (math) 4 CALL_FUNCTION 1 6 RETURN_VALUE 

Claro, está integrado y es más rápido que las funciones normales de py, pero sigue siendo más lento que la statement de import con import_name .

Por eso, la diferencia de tiempo entre ellos es constante. Al usar el fragmento de código @MSeifert (que corrigió los tiempos injustos 🙂 y agregar otra impresión, puede ver esto:

 import timeit def test(module): exec("import {}".format(module)) t2 = timeit.timeit("{0} = __import__('{0}')".format(module)) t1 = timeit.timeit("import {}".format(module)) print("import statement: ", t1) print("__import__ function:", t2) print("t(statement) {} t(function)".format("<" if t1 < t2 else ">")) print('Diff: {}'.format(t2-t1)) for m in sys.builtin_module_names: test(m) 

En mi máquina, hay una diferencia constante de alrededor de 0,17 entre ellos (con una ligera variación que generalmente se espera)

* Vale la pena señalar que estos no son exactamente equivalentes. __import__ no hace ningún enlace de nombre como lo atestigua el bytecode.