Python导入自动化

之后
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) 
碰巧的是,自2012年以来,我一直在开发开放源代码浏览器,是唯一的程序员。 本身就是Python。 浏览器并不是最简单的事情,现在在项目的主要部分中,有1000多个模块和12万多行Python代码。 总体而言,卫星项目将增加一倍半。

在某个时候,我已经厌倦了在每个文件的开头弄乱导入层,我决定一劳永逸地解决这个问题。 因此smart_imports库诞生了( githubpypi )。

这个想法很简单。 任何复杂的项目最终都会在命名所有内容时形成自己的协议。 如果将此协议转换为更正式的规则,则可以通过与其关联的变量的名称自动导入任何实体。

例如,您无需编写import math即可访问math.pi我们math.pi了解,在这种情况下, math是标准库的模块。

智能导入支持Python> = 3.5。该库完全受测试覆盖覆盖率> 95% 。 我已经使用一年了。

有关详细信息,我邀请您加入Cat。

一般如何运作?


因此,标题图片中的代码如下所示:

  1. 在调用smart_imports.all()库将构建从其进行调用模块的AST
  2. 查找未初始化的变量;
  3. 我们通过一系列规则来运行每个变量的名称,这些规则试图找到按名称导入所需的模块(或模块属性)。 如果规则已找到所需的实体,则不会检查以下规则。
  4. 找到的模块将被加载,初始化并放置在全局名称空间中(或这些模块的必要属性放置在此处)。

未初始化的变量在整个代码中进行搜索,包括新语法。

仅对显式调用smart_imoprts.all()那些项目组件启用自动导入。 此外,智能进口的使用并不禁止使用常规进口。 这使您可以逐步实现库,并解决复杂的循环依赖性。

细心的读者会注意到AST模块被构造了两次:

  • CPython在模块导入期间首次构建它。
  • smart_imports第二次在调用smart_imports.all()期间构建它。

AST实际上只能构建一次(为此,您需要使用PEP-0302中实现的导入挂钩集成到模块的导入过程中,但是此解决方案会减慢导入速度。

你为什么这么认为?
比较两个实现(带有和不带有钩子)的性能,我得出的结论是,导入模块时,CPython在其内部(C-shh)数据结构中构建AST。 将它们转换为Python数据结构比使用ast模块从源构建树要贵。

当然,每个模块的AST每次构建和分析一次。

默认导入规则


无需额外配置即可使用该库。 默认情况下,它根据以下规则导入模块:

  1. 通过名称的完全一致,它将搜索当前模块(在同一目录中)旁边的模块。
  2. 检查标准库的模块:
    • 完全匹配顶级软件包的名称;
    • 对于嵌套的软件包和模块,请检查化合物名称,并用下划线替换点。 例如,如果存在os_path变量,则将导入os.path
  3. 通过名称的完全匹配,它将搜索已安装的第三方程序包。 例如,众所周知的包request

性能表现


智能导入不会影响程序的性能,但是会增加启动时间。

由于AST的重建,首次运行的时间增加了约1.5–2倍。 对于小型项目,这并不重要。 在大型项目中,启动时间受模块之间依赖关系的影响,而不是特定模块的导入时间。

智能导入变得流行时,我将工作从AST重写为C-这将大大降低启动成本。

为了加快加载速度,可以将AST模块的处理结果缓存在文件系统上。 在配置中启用了缓存。 当然,更改源时将禁用缓存。

启动时间受模块搜索规则列表及其顺序的影响。 由于某些规则使用标准的Python功能来搜索模块。 您可以通过使用“自定义名称”规则明确指出名称和模块的对应关系来排除这些费用(请参见下文)。

构型


默认配置已在前面进行了描述。 在小型项目中使用标准库就足够了。

默认配置
 { "cache_dir": null, "rules": [{"type": "rule_local_modules"}, {"type": "rule_stdlib"}, {"type": "rule_predefined_names"}, {"type": "rule_global_modules"}] } 


如有必要,可以在文件系统上放置更复杂的配置。

复杂配置的示例 (来自浏览器)。

在调用smart_import.all()库将确定调用模块在文件系统上的位置,并开始在从当前目录到根的方向上查找smart_imports.json文件。 如果找到了这样的文件,则将其视为当前模块的配置。

您可以使用几个不同的配置(将它们放在不同的目录中)。

现在没有很多配置选项:

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

导入规则


在配置中指定规则的顺序决定了它们的应用顺序。 第一条可行的规则停止了对进口的进一步搜索。

在配置示例中,rule_predefined_names规则通常会出现rule_predefined_names ,有必要正确识别内置函数(例如print )。

规则1:预定义名称


该规则允许您忽略诸如__file__类的预定义名称和诸如print类的内置函数。

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

规则2:本地模块


检查当前模块旁边(在同一目录中)是否存在具有指定名称的模块。 如果有,请导入。

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

规则3:全局模块


尝试直接按名称导入模块。 例如, 请求模块。

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

规则4:自定义名称


对应于特定模块的名称或其属性。 符合性在规则配置中指示。

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

规则5:标准模块


检查名称是否为标准库模块。 例如mathos.path ,它们会转换为os_path

它比导入全局模块的规则更快,因为它检查缓存列表中模块是否存在。 每个版本的Python的列表都来自这里: github.com/jackmaney/python-stdlib-list

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

规则6:按前缀导入


通过名称从与其前缀关联的包中导入模块。 当在整个代码中使用多个软件包时,使用起来很方便。 例如,可以使用utils_前缀访问utils软件包模块。

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

规则7:父包中的模块


如果在项目的不同部分中有相同名称的子包(例如, testsmigrations ),则可以允许它们搜索要在父包中按名称导入的模块。

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

规则8:绑定到另一个软件包


对于来自特定程序包的模块,它允许按名称搜索其他程序包中的导入(在配置中指定)。 就我而言,当我不想将前一个规则(父包中的模块)的工作扩展到整个项目时,该规则很有用。

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

添加自己的规则


添加自己的规则非常简单:

  1. 我们smart_imports.rules.BaseRule继承
  2. 我们意识到必要的逻辑。
  3. 使用smart_imports.rules.register方法注册规则
  4. 将规则添加到配置中。
  5. ???
  6. 获利

在当前规则实施中可以找到一个例子

获利


每个来源开头的多行导入清单已消失。

行数减少了。 在浏览器切换到智能导入之前,它有6688行负责导入。 过渡之后,剩下的2084个(每个文件两行smart_imports + 130行导入,分别从函数和类似位置显式调用)。

一个不错的好处是项目中名称的标准化。 代码变得更易于阅读和编写。 无需考虑导入实体的名称-有一些易于遵循的明确规则。

发展计划


我喜欢通过变量名定义代码属性的想法,因此我将尝试在智能导入和其他项目中进行开发。

关于智能进口,我计划:

  1. 添加对新版本Python的支持。
  2. 探索在代码类型注释上依赖当前社区实践的可能性。
  3. 探索进行懒惰进口的可能性。
  4. 实现实用程序,以从源代码自动生成配置并使用smart_imports重构源。
  5. 重写部分C代码以加快AST的工作。
  6. 如果在没有显式导入的情况下存在代码分析问题,则可以与linter和IDE进行集成。

此外,我对您对库和导入规则的默认行为感兴趣。

感谢您压制此文本表:-D

Source: https://habr.com/ru/post/zh-CN459930/


All Articles