探究 python import机制、module、package与名字空间

在开始之前,先了解一个内置函数dir(),它可以帮助我们分析一些内部的东西,dir()的描述是:

dir(): 函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法__dir__(),该方法将被调用。如果参数不包含__dir__(),该方法将最大限度地收集参数信息。

简单来说,不带参数时,会返回当前名字空间的内容(通常是locals名字空间),带参数时,会返回参数的属性:


这里定义了变量a,然后查看 dir() 的内容,可以看到当前的名字空间中包含了一些系统定义的特殊方法还有我们的a。定义一个类,然后查看类的属性,可以看到虽然我们在类中什么都没有定义,但是因为继承了 object 的关系已经有一系列的魔法方法了。OK,我们马上转入到 import 机制中。


初始化的动作:

当我们在命令行敲完 python 回车之后,python已经做了很多很多的初始化工作了。我们可以想象,会有内建类型的初始化,不然我们就没办法使用这些类型来创建实例了;也会有内建函数的初始化,不然连我们的 dir() 都没法用了;还会有搜索路径的初始化,不然 python 怎么会知道你的脚本在哪里呢。总的来说,python的初始化就是设置解释器的状态信息,其中大部分信息是通过创建内置module来完成的,举个例子,我们看看刚才 dir() 里的__builtins__:

这不就是一个叫做‘builtins’的内建 module 吗,我们平常用的 import 就是用来加载模块的,只是我们不会刻意的区分内建模块和自定义模块罢了。当然了,一个模块也会有它的属性,我们 dir 看看:

哦,原来 python 把类型和内建函数初始化之后就放在这个 builtins 模块中,这里我们就可以找到我们熟悉的类型,还有各种各样的异常类型,还有内建函数等等。哎刚刚不是说还会初始化路径吗,在 locals 名字空间里也没有看到类似__path__的东西呀,原来,不是所有的初始化的动作都会暴露在名字空间中的,这样可以让名字空间更加干净,隐藏一些不需要用户知道的东西。我们来找找看,或许大家都知道了,python 的默认搜索路径可以在 sys 内建模块中找到:

这里先讲一讲为什么需要这个路径,这个路径就是我们在安装 python 的时候,需要我们去设置的环境变量。我们可以看到有site-packages文件夹的路径,这是放第三方库的地方,用 setup install 的话会把库复制到这里,比方说numpy就在里面,import numpy就是从这个路径中搜索出来的,还有dlls文件夹的路径,通常加载的pyd就在里面,还有lib等等,执行 import 语句的时候,python 会从默认路径中搜索,当然也会在脚本所在的路径搜索,如果在这些路径中都找不到要 import 的模块就会报错。0.0现在知道 import 失败为什么要去看看环境变量有没有错了吧。

*这段可以跳过。实际上搜索路径是一个限制,有时候使用相对路径 import 的时候会找不到模块,尤其是需要加载位于上层目录的模块的时候要特别注意,这是因为如果不是在 package 中,使用 import 是严格限制搜索范围的。另外有一个小技巧,python 在设置搜索路径的时候,除了设置上面的默认路径,只要在默认路径里面有 .pth 文件,会把 .pth 文件中的路径也放进去,这里写一个 .pth ,指定一个在桌面的路径,放在 site-packages 文件夹中:

我们重新启动 python 虚拟机:

这时默认搜索路径就增加了一条了,这和手动 sys.path.append() 的效果是一样的。这样做的好处是可以把工程放到另一个地方去而在哪里启动虚拟机都可以 import,缺点是增加默认搜索路径会使每次 import 要搜索的时间变长,所以还是看场景使用。


我们继续来观察一下这个 sys 模块有什么东西:

同样的定义了很多特殊方法,还有一些命令之类的东西,当然 path 也在里面,哦原来 ps1,ps2 在这里可以修改:

在 sys 中有一个属性 modules 很特别,它恰恰是放所有 modules 的地方,包括它自己,后面我们还会和它打交道:

而在众多内建 modules 中又有一个很特别,就是我们的__main__ modules,对了就是我们当前执行的脚本,其实这个脚本在python 眼中也是一个 module,但是这个module名字不叫脚本的名字而叫__main__, 所以我们经常会写的__name__ == '__main__',就是判断一个脚本是不是正在执行的脚本,而凡是通过 import 动态加载的其他脚本的名字都不会是__main__:

OK,其实 python 的初始化动作要复杂得多,这里总结一下关于 import 的动作有哪些:首先,将所有内建类型和内建函数初始化然后放到 builtins 模块中,然后创建__main__模块,sys模块等等内建模块全部放在 sys.modules 中,设置默认搜索路径放在sys.path 中,当然还会设置每一个 module 的元信息例如__name__、__doc__等等。


import 机制

有了上面的知识,要了解 import 就简单得多了。这里要挑明一个问题,import 的关键在于将要 import 的动作加载进内存,和以怎么样的方式暴露到名字空间中是两回事。我们之前看到,其实所有的内建 modules 都在 sys.modules 中,但是只有builtins 暴露在名字空间中,所以虽然 sys、os、imp等等早就在名字空间中,但我们还不能直接使用。

1、import 内建 module。内建 modules 本来就在 sys 中,所以在 python 初始化的时候已经加载进内存了,所以剩下的问题只是要把它暴露在名字空间中:


这里就很清晰了,import 一个内建 module 很轻松,只要把它放到名字空间中就可以了,import 的 os 就是 sys.modules 里的os。


2、import 自定义 module。自定义 module 不在内存中,所以没办法了只好先加载进内存,再暴露到名字空间中:



3、import as 组合。as 的唯一功能就是改变暴露到名字空间的方式,所以说加载到内存的也还是一个moudle, 只是到了名字空间就换了个名字而已,换汤不换药:



在 module 之上,python 还有一种管理名字空间的方式 package,package是一个特别的 module,在 python 眼中也还是一个module。package是以文件夹的方式实现的,一个合法的 package 必须有一个__init__.py,我们来创建一个package,里面包含了一个__init__文件和文件 b:


4、import package。package 是一个特殊的 module,所以也要加载进内存,然后暴露到名字空间中(as 也适用):


5、import package 中的 modules。定义了一个 package 之后,会为 package 设置一个专属的搜索范围,这个信息在__spec__的 submodule_search_locations中:


如果要通过 package 搜索 module 必须在这个指定的搜索路径中,否则会报错,尽管要 import 的 module 能够在默认路径中找到:

再看看内存的情况,通过 package import module 之后,除了这个 module 被加载进内存以外,连这个 package 本身也会加载进内存中(as 也适用):


注意到几个特殊的情况,首先在内存中确实有 package 这个 module,要把 package 也一起加载进内存有两个原因,一个是执行 import package.b 的时候,python 把后面的 package.b 做了递归,也就是先 import package,取出 package 指定的搜索路径,再 import b,所以 package 是先于 b 被加载的,还有一个原因是做缓存,当下一次再 import 这个 package 中的 module 的时候就不需要再次加载了。第二个情况是内存中有 package.b 但是没有 b,这其实很容易理解,如果另外一个 pacakge 中也有 b 这个 module,那 python 要怎么区分呢,所以使用通过 pacakge 的 import 的 module 必须包含 package名。第三个情况是只把 package 暴露在名字空间中,这个也很简单,因为第二个原因每次访问都只能通过 package,所以就没必要把 b, 也不能把 b 简单的暴露出来。


6、form import组合。上面说到了直接 import pacakge 里的  module 不方便,那可以通过 from 进行精准 import,

很显然,通过 from 和 as 的工作有点相似,在内存中也还是加载了 package 和 package.b,而直接将 b 暴露到名字空间中。


7、import *组合。一般来说,import 一个 module 就是把 moudle 中的属性(变量、函数、类)打包之后创建一个 module,从一个 module 中 import 一个变量相当于把这个变量暴露在名字空间中:

为了方便起见,可以使用 import * 的组合将一个 module 中所有的变量都暴露在名字空间中:

为了限制一些变量不会通过 import * 组合暴露出去,可以在 module 定义__all__属性:


8、嵌套import。嵌套 import 意思是在 import 一个 module 的时候,这个 module 也有 import 语句,这其实很常见。其实和上面的几种情况的差别不大,我们可以猜想 import 之后内存中会有两个 module,而看看这两个 module 暴露在哪个名字空间中罢了:


总结一下 import 的一些行为。重要的事情说多一遍:import 的关键在于将要 import 的动作加载进内存,和以怎么样的方式暴露到名字空间中是两回事,import 机制所有的行为都是围绕这两个问题来的,我们可以看到 sys.modules 就是所有 modules 住的地方,也是一个缓冲池,在每次 import 之前先检查在不在换冲池中,如果在就不必再次加载,这是一个很重要的点。至于要不要暴露到名字空间中,怎么暴露到名字空间中,暴露到哪个名字空间中,就是不同 import 语句要做的事情了。


*这段可以跳过。我们想一个问题,在 python 初始化内建类型和内建函数之后,一直放在 builtins 模块中,那新加载的模块怎么能够使用内建类型和内建函数呢?我们发现每次加载完模块之后,这个模块都有一个属性就叫__builtins__,我们来看看:

奇怪了,虽然确实是有一个__builtins__, 但是这是一个 dict,和那个 builtins 不是一个东西呀!别急,python 的 LEGB 规则告诉我们,除了 locals 和 globals 名字空间以外,还有一个名字空间就是 builtins 名字空间,所有的内建类型和内建函数,肯定都不会再局部和全局变量中,唯一的办法就是搜索到 builtins 中去,所以本质上 builtins 是一个名字空间,维护一个从符号到变量的 dict,也就是在 module 中的 __builtins__。而 builtins modules 是这个 dict 的包装,还维护了其他一些元信息,所以如果仔细观察,在 modules 中的__builtins__就是从 builtins modules 复制过来的。更进一步,所有的线程都是共享这个模块的。


module 的使用与名字空间

为什么要做包管理,要分开不同的 module ,还有这么复杂的规则呢?最终都是要更好地划分名字空间,使得每一个写 module 的人可以尽情使用自己名字空间中的变量,而不用担心与其他 module 冲突。python 没有外部变量这一说,只有 LEGB 规则。来看一个例子:

这个例子中,module1 加载 module2,调用其中的函数,但是在 moudle1 和 module2 中都有全局变量 value,这样一来打印的会是什么呢:

答案是10,我们用直觉想,写 module2 的人所希望的一定是打印出它定义的全局变量10,而不是先考虑有其他的 module 会有一个 value 和它冲突,因为 python 没有外部变量这一说,如果是这样,每个写模块的程序员都必须小心翼翼,这是我们不希望的,显然 python 包管理就是为了应对这种情况的,为了能够访问 module 中的变量、函数、类,都必须通过 module 作为前缀以示标识:

再换句话说,其实 module 也是一种名字空间,本质上和函数,和类没有区别,module 也有自己的属性,有自己的 locals、globals 空间,难道不觉得访问 module 的变量和访问类变量的方式很像吗?

关于名字空间的理解只能意会了,这里再解答为什么 python 能够实现输出10 而不输出 1,这是因为和类、函数相同,当需要进入一个新的名字空间的时候,会创建一个新的栈帧处理这个名字空间当中的字节码,而再这之前,locals 和 globals 名字空间会更新(也会复制 builtins 名字空间),所以 module1 调用 modules 的函数的时候,实际上 globals 名字空间中的 value 已经被更新成 10 了,所以通过LEGB规则就轻松解决不同 modules 的命名冲突了。


这篇文章到这里就算结束了,博主是根据《python源码解析》的内容整理出来的,有兴趣的小伙伴也可以阅读,对源码有兴趣的小伙帮也可以查看相关的代码。关于名字空间的例子出自书中,python2与python3差别很大,在实现 import 机制上有很大的改动,不过核心没有变化,这篇文章只是一个黑盒解析,抛砖引玉,如果有错漏的地方也请大家指出。