python中的Import总结

动机

最近的一些基于python的框架型的工作,有很多涉及到动态用户代码加载的功能。因此,对python的导入(import)做一个梳理总结。

基础概念

  • 模块(module):一般是指python源代码文件,即.py文件。还有可能是:.pyo.pyc.pyd.so文件。
  • 包(package):包是含有module的文件夹,为了避免模块名冲突而引入的。当一个文件夹下有__init__.py时,代表这个文件夹是一个package。package也可以看做是特殊的module,它的替身就是对应的__init.py__文件。
  • 命名空间(namespace)
    • local namespace:每个函数function特有,用于保存函数的变量。可用内置函数locals()查看,该函数不可写。
    • enclosing function namespace:闭包命名空间:闭包函数 的名称空间(Python 3 引入)。
    • global namespace:每个模块module特有,用于保存模块的变量。可用内置函数globals()查看,该函数可写。
    • builtin namespace:内建命名空间:Python 解释器启动时自动载入__built__模块后所形成的名称空间;如 str/list/dict等内置对象的名称就出于这里。

相关的内置属性:

  • __all__:一般在__init__.py中用于指定此package在被外部代码import时,哪些module会被引进当前作用域中。
  • __name__:直接运行本module,值为__main__import module,值为module名字。
  • __path__:保存当前package的路径。修改__path__可以改变package内的搜索路径。任何具有__path__属性的模块都会被当作是包。
  • __file__:如果是package,则保存__init__.py的绝对路径;如果是module,返回module本身的绝对路径。
  • __package__:保存module对应的package名称。

import

import语句时发起导入机制的最常用方式,会调用内置的__import__()。包括以下两个操作:

    1. 搜索指定名称的模块。
    1. 将搜索结果绑定到当前作用域中。

搜索

    1. 先检查sys.modules中是否有模块缓存;
    1. 如果没有缓存,会接着搜索sys.meta_path路径下的查找器再进行查找。
    1. 查找器会使用sys.path来搜索模块。sys.path会将环境变量PYTHONPATH指定的路径添加进去,并且path[0]会给脚本存放的路径保留。

加载

    1. 如果之前没有缓存,会将模块加入到sys.modules中。
    1. 注册到当前module的全局命名空间中。

importlib

1
importlib.import_module(name, package=None)

在实践中,我会经常用importlib去加载用户指定的代码,比如user_project/module.py,可以用importlib.import_module("user_project.module")来导入。另外需要确保user_project是包含在sys.path的。

exec

execeval都可以用来动态地执行python代码,区别是前者可以是大段的代码;后者是一个python表达式。

因此,用户的代码片段,使用exec来动态加载也是一个不错的选项。

需要注意的是,exec除了接收代码对象外,还接受执行的上下文:exec(object[, globals[, locals]]),也就是两个重要的参数:globalslocals

  • globals用来指定代码执行时可以使用的全局变量以及收集代码执行后的全局变量。而且默认会加上__builtins__
  • locals用来指定代码执行时的局部变量以及收集代码执行后的局部变量

上面的描述不能完全表达这两个参数的逻辑,注意理解下面代码的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
In [1]: code = """
...: def func():
...: pass
...: a = 1"""

In [2]: g={"__builtins__":{}}

In [3]: l = {}

In [4]: exec(code, g, l)

In [5]: g
Out[5]: {'__builtins__': {}}

In [6]: l
Out[6]: {'func': <function func()>, 'a': 1}

In [7]: exec(code, g)

In [8]: g
Out[8]: {'__builtins__': {}, 'func': <function func()>, 'a': 1}

总结:代码片段中新增加的变量,都保存在locals中;若只传一个globals参数,则locals也会使用globals传入的参数,所以这时globals中会有代码片段执行后新增加的变量。至于为什么新增的变量会只在locals中,可以这么理解:exec中执行的代码是在函数exec_xxx()中执行,而不是直接在当前上下文执行;如果需要达到当前上下文执行的效果,就把globals()传给locals参数即可。

参考资料