Automação de importação Python

ParaDepois
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) 
Aconteceu que, desde 2012, desenvolvo um navegador de código aberto, sendo o único programador. Em Python por si só. O navegador não é a coisa mais fácil, agora na parte principal do projeto existem mais de 1000 módulos e mais de 120.000 linhas de código Python. No total, será uma vez e meia mais com projetos de satélite.

Em algum momento, eu estava cansado de mexer no piso das importações no início de cada arquivo e decidi lidar com esse problema de uma vez por todas. Então a biblioteca smart_imports nasceu ( github , pypi ).

A ideia é bem simples. Qualquer projeto complexo eventualmente forma seu próprio acordo em nomear tudo. Se este contrato for transformado em regras mais formais, qualquer entidade poderá ser importada automaticamente pelo nome da variável associada a ele.

Por exemplo, você não precisará escrever import math para acessar math.pi - math.pi podemos entender que, neste caso, math é um módulo da biblioteca padrão.

As importações inteligentes suportam Python> = 3.5 A biblioteca é totalmente coberta por testes, cobertura> 95% . Eu já o uso há um ano.

Para mais detalhes, convido você para o gato.

Como isso funciona em geral?


Portanto, o código da imagem do cabeçalho funciona da seguinte maneira:

  1. Durante uma chamada para smart_imports.all() biblioteca cria o AST do módulo a partir do qual a chamada é feita;
  2. Encontre variáveis ​​não inicializadas;
  3. Executamos o nome de cada variável por meio de uma sequência de regras que tentam encontrar o módulo (ou atributo do módulo) necessário para a importação por nome. Se uma regra encontrou a entidade necessária, as seguintes regras não são verificadas.
  4. Os módulos encontrados são carregados, inicializados e colocados no espaço para nome global (ou os atributos necessários desses módulos são colocados lá).

Variáveis ​​não inicializadas são pesquisadas em todo o código, incluindo a nova sintaxe.

A importação automática é ativada apenas para os componentes do projeto que chamam explicitamente smart_imoprts.all() . Além disso, o uso de importações inteligentes não proíbe o uso de importações convencionais. Isso permite implementar a biblioteca gradualmente, assim como resolver dependências cíclicas complexas.

Um leitor meticuloso notará que o módulo AST é construído duas vezes:

  • O CPython o constrói pela primeira vez durante a importação do módulo;
  • Na segunda vez que o smart_imports o constrói durante uma chamada para smart_imports.all() .

O AST realmente pode ser construído apenas uma vez (para isso, você precisa integrar o processo de importação de módulos usando ganchos de importação implementados no PEP-0302 , mas essa solução torna a importação mais lenta.

Por que você acha isso?
Comparando o desempenho de duas implementações (com e sem ganchos), cheguei à conclusão de que, ao importar um módulo, o CPython cria o AST em suas estruturas de dados internas (C-shh). Convertê-los em estruturas de dados Python é mais caro do que construir uma árvore a partir da fonte usando o módulo ast .

Obviamente, o AST de cada módulo é construído e analisado apenas uma vez por lançamento.

Regras de importação padrão


A biblioteca pode ser usada sem configuração adicional. Por padrão, ele importa módulos de acordo com as seguintes regras:

  1. Por coincidência exata do nome, ele procura o módulo próximo ao atual (no mesmo diretório).
  2. Verifica os módulos da biblioteca padrão:
    • pela correspondência exata do nome para pacotes de nível superior;
    • para pacotes e módulos aninhados, verifica nomes de compostos, substituindo pontos por sublinhados. Por exemplo, o os.path será importado se a variável os_path estiver os_path .
  3. Por correspondência exata do nome, ele procura por pacotes de terceiros instalados. Por exemplo, o pacote conhecido solicita .

Desempenho


As importações inteligentes não afetam o desempenho do programa, mas aumentam o tempo necessário para o lançamento.

Devido à reconstrução do AST, o tempo da primeira execução aumenta cerca de 1,5 a 2 vezes. Para pequenos projetos, isso não é significativo. Em grandes projetos, o tempo de inicialização sofre com a estrutura de dependência entre os módulos, e não com o tempo de importação de um módulo específico.

Quando as importações inteligentes se tornam populares, reescrevo o trabalho do AST para o C - isso deve reduzir significativamente os custos de inicialização.

Para acelerar o carregamento, os resultados do processamento de módulos AST podem ser armazenados em cache no sistema de arquivos. O armazenamento em cache está ativado na configuração. Obviamente, o cache é desativado quando você altera a fonte.

O tempo de inicialização é afetado pela lista de regras de pesquisa do módulo e sua sequência. Como algumas regras usam a funcionalidade padrão do Python para procurar módulos. Você pode excluir essas despesas indicando explicitamente a correspondência de nomes e módulos usando a regra "Nomes personalizados" (veja abaixo).

Configuração


A configuração padrão foi descrita anteriormente. Deve ser suficiente trabalhar com a biblioteca padrão em pequenos projetos.

Configuração padrão
 { "cache_dir": null, "rules": [{"type": "rule_local_modules"}, {"type": "rule_stdlib"}, {"type": "rule_predefined_names"}, {"type": "rule_global_modules"}] } 


Se necessário, uma configuração mais complexa pode ser colocada no sistema de arquivos.

Um exemplo de uma configuração complexa (de um navegador).

Durante uma chamada para smart_import.all() biblioteca determina a posição do módulo de chamada no sistema de arquivos e começa a procurar o arquivo smart_imports.json na direção do diretório atual para a raiz. Se um arquivo desse tipo for encontrado, será considerada a configuração do módulo atual.

Você pode usar várias configurações diferentes (colocando-as em diretórios diferentes).

Não há muitas opções de configuração agora:

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

Regras de importação


A ordem de especificar as regras na configuração determina a ordem de sua aplicação. A primeira regra que funcionou interrompe a pesquisa adicional de importações.

Nos exemplos de configurações, a regra rule_predefined_names geralmente aparece rule_predefined_names , é necessário que as funções internas (por exemplo, print ) sejam reconhecidas corretamente.

Regra 1: Nomes predefinidos


A regra permite que você ignore nomes predefinidos como __file__ e funções __file__ como print .

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

Regra 2: Módulos Locais


Verifica se existe um módulo com o nome especificado ao lado do módulo atual (no mesmo diretório). Se houver, importe-o.

Exemplo
 # : # { # "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) 

Regra 3: Módulos Globais


Tenta importar um módulo diretamente pelo nome. Por exemplo, o módulo de solicitações .

Exemplo
 # : # { # "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')) 

Regra 4: Nomes personalizados


Corresponde ao nome de um módulo específico ou seu atributo. A conformidade é indicada na configuração da regra.

Exemplo
 # : # { # "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) 

Regra 5: Módulos Padrão


Verifica se o nome é um módulo de biblioteca padrão. Por exemplo math ou os.path que se transforma em os_path .

Funciona mais rápido que a regra para importar módulos globais, pois verifica a presença de um módulo em uma lista em cache. As listas para cada versão do Python vêm daqui: github.com/jackmaney/python-stdlib-list

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

Regra 6: Importar por prefixo


Importa um módulo por nome, do pacote associado ao seu prefixo. É conveniente usar quando você tem vários pacotes usados ​​em todo o código. Por exemplo, os módulos do pacote utils podem ser acessados ​​com o prefixo utils_ .

Exemplo
 # : # { # "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) 

Regra 7: O módulo do pacote pai


Se você tiver subpacotes com o mesmo nome em diferentes partes do projeto (por exemplo, tests ou migrations ), poderá permitir que eles pesquisem módulos para importar por nome nos pacotes pai.

Exemplo
 # : # { # "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) 

Regra 8: Vinculando a outro pacote


Para módulos de um pacote específico, ele permite procurar importações por nome em outros pacotes (especificados na configuração). No meu caso, essa regra foi útil para casos em que não quis estender o trabalho da regra anterior (Módulo do pacote pai) para todo o projeto.

Exemplo
 # : # { # "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) 

Adicionando suas próprias regras


Adicionar sua própria regra é bem simples:

  1. Herdamos da classe smart_imports.rules.BaseRule .
  2. Percebemos a lógica necessária.
  3. Registre uma regra usando o método smart_imports.rules.register
  4. Adicione a regra à configuração.
  5. ???
  6. Lucro

Um exemplo pode ser encontrado na implementação das regras atuais.

Lucro


As listas multilinhas de importações no início de cada fonte desapareceram.

O número de linhas diminuiu. Antes de o navegador mudar para importações inteligentes, ele possuía 6688 linhas responsáveis ​​pela importação. Após a transição, 2084 permaneceu (duas linhas de smart_imports por arquivo + 130 importações, chamadas explicitamente a partir de funções e locais semelhantes).

Um bom bônus foi a padronização de nomes no projeto. O código ficou mais fácil de ler e mais fácil de escrever. Não há necessidade de pensar nos nomes das entidades importadas - existem algumas regras claras que são fáceis de seguir.

Planos de desenvolvimento


Eu gosto da idéia de definir propriedades de código por nomes de variáveis, portanto, tentarei desenvolvê-las tanto em importações inteligentes quanto em outros projetos.

Em relação às importações inteligentes, planejo:

  1. Adicione suporte para novas versões do Python.
  2. Explore a possibilidade de confiar nas práticas atuais da comunidade na anotação de tipo de código.
  3. Explore a possibilidade de fazer importações preguiçosas.
  4. Implemente utilitários para geração automática de uma configuração a partir de códigos-fonte e refatoração de fontes para o uso de smart_imports.
  5. Reescreva parte do código C para acelerar o trabalho com o AST.
  6. Desenvolver a integração com linters e IDEs, se houver problemas com a análise de código sem importações explícitas.

Além disso, estou interessado em sua opinião sobre o comportamento padrão da biblioteca e as regras de importação.

Obrigado por dominar esta folha de texto :-D

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


All Articles