Automatización de importación de Python

ADespues
import math import os.path import requests # 100500 other imports print(math.pi) print(os.path.join('my', 'path')) print(requests.get) 
 import smart_imports smart_imports.all() print(math.pi) print(os_path.join('my', 'path')) print(requests.get) 
Sucedió que desde 2012 he estado desarrollando un navegador de código abierto, siendo el único programador. En Python por sí mismo. El navegador no es lo más fácil, ahora en la parte principal del proyecto hay más de 1000 módulos y más de 120,000 líneas de código Python. En total, será una vez y media más con proyectos satelitales.

En algún momento, estaba cansado de jugar con los pisos de importación al comienzo de cada archivo, y decidí tratar este problema de una vez por todas. Así nació la biblioteca smart_imports ( github , pypi ).

La idea es bastante simple. Cualquier proyecto complejo finalmente forma su propio acuerdo para nombrar todo. Si este acuerdo se convierte en reglas más formales, cualquier entidad puede importarse automáticamente por el nombre de la variable asociada a ella.

Por ejemplo, no necesitará escribir import math para acceder a math.pi : ya podemos entender que en este caso las math son un módulo de la biblioteca estándar.

Las importaciones inteligentes admiten Python> = 3.5. La biblioteca está completamente cubierta por pruebas, cobertura> 95% . Lo he estado usando por un año.

Para más detalles, te invito a Cat.

¿Cómo funciona en general?


Entonces, el código de la imagen del encabezado funciona de la siguiente manera:

  1. Durante una llamada a smart_imports.all() biblioteca crea el AST del módulo desde el que se realiza la llamada;
  2. Encuentra variables no inicializadas;
  3. Ejecutamos el nombre de cada variable a través de una secuencia de reglas que intentan encontrar el módulo (o atributo del módulo) que se necesita para importar por nombre. Si una regla ha encontrado la entidad requerida, no se verifican las siguientes reglas.
  4. Los módulos encontrados se cargan, inicializan y colocan en el espacio de nombres global (o los atributos necesarios de estos módulos se colocan allí).

Las variables no inicializadas se buscan en todo el código, incluida la nueva sintaxis.

La importación automática está habilitada solo para aquellos componentes del proyecto que llaman explícitamente a smart_imoprts.all() . Además, el uso de importaciones inteligentes no prohíbe el uso de importaciones convencionales. Esto le permite implementar la biblioteca gradualmente, así como resolver dependencias cíclicas complejas.

Un lector meticuloso notará que el módulo AST se construye dos veces:

  • CPython lo construye por primera vez durante la importación del módulo;
  • La segunda vez que smart_imports lo construye durante una llamada a smart_imports.all() .

AST realmente se puede construir solo una vez (para esto necesita integrarse en el proceso de importación de módulos usando ganchos de importación implementados en PEP-0302 , pero esta solución ralentiza la importación.

¿Por qué piensas eso?
Comparando el rendimiento de dos implementaciones (con y sin ganchos), llegué a la conclusión de que al importar un módulo, CPython crea AST en sus estructuras de datos internas (C-shh). Convertirlos a estructuras de datos de Python es más costoso que construir un árbol desde la fuente usando el módulo ast .

Por supuesto, el AST de cada módulo se construye y analiza solo una vez por lanzamiento.

Reglas de importación predeterminadas


La biblioteca se puede usar sin configuración adicional. Por defecto, importa módulos de acuerdo con las siguientes reglas:

  1. Por coincidencia exacta del nombre, busca el módulo junto al actual (en el mismo directorio).
  2. Comprueba los módulos de la biblioteca estándar:
    • por coincidencia exacta del nombre de los paquetes de nivel superior;
    • para paquetes y módulos anidados, busca nombres compuestos, reemplazando puntos con guiones bajos. Por ejemplo, os.path se importará si la variable os_path está os_path .
  3. Por coincidencia exacta del nombre, busca paquetes de terceros instalados. Por ejemplo, las solicitudes de paquetes conocidos.

Rendimiento


Las importaciones inteligentes no afectan el rendimiento del programa, pero aumentan el tiempo que se tarda en iniciarse.

Debido a la reconstrucción del AST, el tiempo de la primera ejecución aumenta aproximadamente 1.5–2 veces. Para proyectos pequeños esto no es significativo. En proyectos grandes, el tiempo de inicio adolece de la estructura de dependencia entre los módulos en lugar del tiempo de importación de un módulo en particular.

Cuando las importaciones inteligentes se vuelven populares, reescribo el trabajo de AST a C, esto debería reducir significativamente los costos de inicio.

Para acelerar la carga, los resultados del procesamiento de los módulos AST se pueden almacenar en caché en el sistema de archivos. El almacenamiento en caché está habilitado en la configuración. Por supuesto, el caché se desactiva cuando cambia la fuente.

El tiempo de inicio se ve afectado tanto por la lista de reglas de búsqueda de módulos como por su secuencia. Dado que algunas reglas utilizan la funcionalidad estándar de Python para buscar módulos. Puede excluir estos gastos indicando explícitamente la correspondencia de nombres y módulos utilizando la regla "Nombres personalizados" (ver más abajo).

Configuracion


La configuración predeterminada se ha descrito anteriormente. Debería ser suficiente trabajar con la biblioteca estándar en pequeños proyectos.

Configuración predeterminada
 { "cache_dir": null, "rules": [{"type": "rule_local_modules"}, {"type": "rule_stdlib"}, {"type": "rule_predefined_names"}, {"type": "rule_global_modules"}] } 


Si es necesario, se puede poner una configuración más compleja en el sistema de archivos.

Un ejemplo de una configuración compleja (desde un navegador).

Durante una llamada a smart_import.all() biblioteca determina la posición del módulo de llamada en el sistema de archivos y comienza a buscar el archivo smart_imports.json en la dirección del directorio actual a la raíz. Si se encuentra dicho archivo, se considera la configuración para el módulo actual.

Puede usar varias configuraciones diferentes (colocándolas en diferentes directorios).

No hay muchas opciones de configuración ahora:

 { //     AST. //     null —   . "cache_dir": null|"string", //       . "rules": [] } 

Reglas de importación


El orden de especificar las reglas en la configuración determina el orden de su aplicación. La primera regla que funcionó detiene la búsqueda adicional de importaciones.

En los ejemplos de configuraciones, la regla rule_predefined_names a menudo aparecerá a rule_predefined_names , es necesario que las funciones integradas (por ejemplo, print ) se reconozcan correctamente.

Regla 1: Nombres predefinidos


La regla le permite ignorar nombres predefinidos como __file__ y funciones integradas como print .

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}] # } import smart_imports smart_imports.all() #        __file__ #        print(__file__) 

Regla 2: módulos locales


Comprueba si hay un módulo con el nombre especificado al lado del módulo actual (en el mismo directorio). Si lo hay, lo importa.

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules"}] # } # #    : # # my_package # |-- __init__.py # |-- a.py # |-- b.py # b.py import smart_imports smart_imports.all() #    "a.py" print(a) 

Regla 3: módulos globales


Intenta importar un módulo directamente por su nombre. Por ejemplo, el módulo de solicitudes .

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_global_modules"}] # } # #    # # pip install requests import smart_imports smart_imports.all() #    requests print(requests.get('http://example.com')) 

Regla 4: nombres personalizados


Corresponde al nombre de un módulo en particular o su atributo. El cumplimiento se indica en la configuración de la regla.

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_custom", # "variables": {"my_import_module": {"module": "os.path"}, # "my_import_attribute": {"module": "random", "attribute": "seed"}}}] # } import smart_imports smart_imports.all() #       #        print(my_import_module) print(my_import_attribute) 

Regla 5: Módulos estándar


Comprueba si el nombre es un módulo de biblioteca estándar. Por ejemplo, math u os.path que se transforma en os_path .

Funciona más rápido que la regla para importar módulos globales, ya que verifica la presencia de un módulo en una lista en caché. Las listas para cada versión de Python provienen de aquí: github.com/jackmaney/python-stdlib-list

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_stdlib"}] # } import smart_imports smart_imports.all() print(math.pi) 

Regla 6: Importar por prefijo


Importa un módulo por nombre, desde el paquete asociado con su prefijo. Es conveniente usarlo cuando tiene varios paquetes usados ​​en todo el código. Por ejemplo, se puede acceder a los módulos del paquete utils con el prefijo utils_ .

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_prefix", # "prefixes": [{"prefix": "utils_", "module": "my_package.utils"}]}] # } # #    : # # my_package # |-- __init__.py # |-- utils # |-- |-- __init__ # |-- |-- a.py # |-- |-- b.py # |-- subpackage # |-- |-- __init__ # |-- |-- c.py # c.py import smart_imports smart_imports.all() print(utils_a) print(utils_b) 

Regla 7: el módulo del paquete principal


Si tiene subpaquetes del mismo nombre en diferentes partes del proyecto (por ejemplo, tests o migrations ), puede permitirles buscar módulos para importar por nombre en los paquetes principales.

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules_from_parent", # "suffixes": [".tests"]}] # } # #    : # # my_package # |-- __init__.py # |-- a.py # |-- tests # |-- |-- __init__ # |-- |-- b.py # b.py import smart_imports smart_imports.all() print(a) 

Regla 8: Enlace a otro paquete


Para los módulos de un paquete específico, permite la búsqueda de importaciones por nombre en otros paquetes (especificados en la configuración). En mi caso, esta regla fue útil para casos en los que no quería extender el trabajo de la regla anterior (Módulo del paquete principal) a todo el proyecto.

Ejemplo
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules_from_namespace", # "map": {"my_package.subpackage_1": ["my_package.subpackage_2"]}}] # } # #    : # # my_package # |-- __init__.py # |-- subpackage_1 # |-- |-- __init__ # |-- |-- a.py # |-- subpackage_2 # |-- |-- __init__ # |-- |-- b.py # a.py import smart_imports smart_imports.all() print(b) 

Agregar sus propias reglas


Agregar tu propia regla es bastante simple:

  1. Heredamos de la clase smart_imports.rules.BaseRule .
  2. Nos damos cuenta de la lógica necesaria.
  3. Registre una regla utilizando el método smart_imports.rules.register
  4. Agregue la regla a la configuración.
  5. ???
  6. Ganancia

Se puede encontrar un ejemplo en la implementación de las reglas actuales.

Ganancia


Las listas multilíneas de importaciones al comienzo de cada fuente han desaparecido.

El número de filas ha disminuido. Antes de que el navegador cambiara a importaciones inteligentes, tenía 6688 líneas responsables de la importación. Después de la transición, quedaron 2084 (dos líneas de smart_imports por archivo + 130 importaciones, llamadas explícitamente desde funciones y lugares similares).

Una buena ventaja fue la estandarización de los nombres en el proyecto. El código se ha vuelto más fácil de leer y más fácil de escribir. No es necesario pensar en los nombres de las entidades importadas; hay algunas reglas claras que son fáciles de seguir.

Planes de desarrollo


Me gusta la idea de definir las propiedades del código por nombres de variables, por lo que intentaré desarrollarlo tanto dentro de las importaciones inteligentes como en otros proyectos.

Con respecto a las importaciones inteligentes, planeo:

  1. Agregue soporte para nuevas versiones de Python.
  2. Explore la posibilidad de confiar en las prácticas actuales de la comunidad en la anotación de tipo de código.
  3. Explore la posibilidad de realizar importaciones perezosas.
  4. Implemente utilidades para la generación automática de una configuración a partir de códigos fuente y refactorización de fuentes para usar smart_imports.
  5. Vuelva a escribir parte del código C para acelerar el trabajo con el AST.
  6. Desarrollar la integración con linters e IDEs si tienen problemas con el análisis de código sin importaciones explícitas.

Además, estoy interesado en su opinión sobre el comportamiento predeterminado de la biblioteca y las reglas de importación.

Gracias por dominar esta hoja de texto :-D

Source: https://habr.com/ru/post/459930/


All Articles