1. 模块的加载流程
python程序说到底是执行的是模块,模块要被执行都有两个步骤:
- 导入到虚拟机的内存中
- 执行其中的代码
本节主要就是讲的这个模块导入的流程
1.1. 入口模块
入口模块是一个特殊的模块.它是用户使用python xxxx
所指定的执行模块.你的程序由这个入口进入,他是一个程序第一个被导入到虚拟机中的模块.python解释器会给他的__name__
定义为__main__
,并且其他一些预设字段会设上None.
可以作为入口模块的模块有两种:
- 单独的
.py
文件形成的模块 - 文件夹形式的包中第一层的
__main__.py
文件
1.2. 模块导入
python中模块和包是一个意思.一个.py
文件就是一个模块,如果模块复杂,那么带有__init__.py
的文件夹也是一个模块.模块中可以嵌套模块以构建为一个更为复杂的模块.
python的模块引入使用的是import
语句.具体有3种形式:
import package from package import object from package import object as name
而引入机制可以分为两种:
- 相对引入
- 完全引入
1.2.1. 相对引入
相对引入中一个点号来标识引入类库的精确位置.与linux的相对路径表示相似,一个点表示当前目录,每多一个点号则代表向上一层目录.
from .string import a from ..string import a from ...string import a
相对引入使用被引入文件的__name__
属性来决定该文件在整个包结构的位置.那么如果文件的__name__
没有包含任何包的信息,例如__name__
被设置为了__main__
,则认为其为入口模块(top level script
),而不管该文件的位置,这个时候相对引入就没有引入的参考物.
1.2.2. 完全引入
完全引入,非常类似于Java的引入进制,
完全引用是Python的默认的引入机制.它的使用方法如下:
from pkg import foo from pkg.moduleA import foo
要注意的是,需要从包目录最顶层目录依次写下而不能从中间开始.
两种引用方式各有利弊.绝对引用代码更加清晰明了,可以清楚的看到引入的包名和层次,但当包名修改的时候,我们需要手动修改所有的引用代码.相对引用则比较精简,不会被包名修改所影响,但是可读性较差,不如完全引用清晰.
前文有介绍过,python的加载通常使用import
语句.import
语句实际上只是一种加载方式,还可以使用内置函数__import__(name:str, globals=None, locals=None, fromlist=(), level=0)
或者使用标准库importlib.import_module(name:str, package=None)
import importlib
module_test = importlib.import_module("module_test") # 相当于 import module_test
module_test = __import__('module_test') # 相当于 import module_test
a = __import__('module_test.a',fromlist=["b"]) # 相当于 from module_test import a,但是其下会将其子模块放入a的字段中
a.b.func()
1
1.3. 加载机制
模块的加载分为两种情况:
对未被加载过的模块进行加载
对已经加载过的模块进行加载
当Python的解释器遇到import
语句或者其他上述导入语句时,它会先去查看sys.modules
中是否已经有同名模块被导入了,如果有就直接取来用;没有就去查阅sys.path
里面所有已经储存的目录.这个列表初始化的时候,通常包含一些来自外部的库(external libraries)或者是来自操作系统的一些库,当然也会有一些类似于dist-package
的标准库在里面.这些目录通常是被按照顺序或者是直接去搜索想要的--如果说他们当中的一个包含有期望的package或者是module,这个package或者是module将会在整个过程结束的时候被直接提取出来保存在sys.modules
中(sys.modules
是一个模块名:模块对象
的字典结构).
当在这些个地址中实在是找不着时,它就会抛出一个ModuleNotFoundError
错误.
import ujson
这一机制常常被我们用来按环境加载相同功能的不同模块,以保证系统的鲁棒性
from .string import a from ..string import a from ...string import a
0
1.3.1. 导入顺序,sys.path和环境变量PYTHONPATH
我们来实际看看sys.path
的内容
from .string import a from ..string import a from ...string import a
1
from .string import a from ..string import a from ...string import a
2
from .string import a from ..string import a from ...string import a
3
可以看到第一位是个空字符串,代表的是相对路径下的当前目录.
由于在导入模块的时候,解释器会按照列表的顺序搜索,直到找到第一个模块,所以优先导入的模块为同一目录下的模块.
导入模块时搜索路径的顺序也可以改变.这里分两种情况:
通过
sys.path.append()
,sys.path.insert()
等方法来改变,这种方法当重新启动解释器的时候,原来的设置会失效.改变环境变量
PYTHONPATH
,这种设置方法随着环境变量的有效范围变化,只要启动python时先指定即可.
我们写个脚本pythonpath_test.py
测试下PYTHONPATH
的效用
from .string import a from ..string import a from ...string import a
4
from .string import a from ..string import a from ...string import a
5
from .string import a from ..string import a from ...string import a
6
from .string import a from ..string import a from ...string import a
7
from .string import a from ..string import a from ...string import a
8
可以看到,PYTHONPATH在第二位上将其值添加到了查找范围中.
1.3.2. 关于__path__的更多细节
上面提到的Python的import流在大多数情况下默认等行为就已经可以满足需求,但是事实上细节远不止这些.他省略了一些我们可以根据需要调节的地方.
首先,__path__
这个属性是我们可以在__init__.py
里面去定义的.你可以认为他像一个sys.path
的本地扩展并且只服务于我们导入的<package>
的子模块.换句话说,它包含地址时时应该寻找一个<package>
的子模块被导入.默认的情况下只有__init__.py
的目录,但是他可以扩展到包含任何其他任何的路径.
这个说的很绕,实际看段例子就可以看出来.
通常__path__
的扩展借助于标准库pkgutil
,后文还会有对其使用的介绍,我们的例子也要借助这个工具.pkgutil.extend_path(path, name)
的作用是在sys.path
范围内查找与__path__
中.
我们的测试模块叫demopkg1
其结构如下:
from .string import a from ..string import a from ...string import a
9
测试模块的扩展叫extension
,其结构如下:
from pkg import foo from pkg.moduleA import foo
0
demopkg1
的__init__.py
内容如下
from pkg import foo from pkg.moduleA import foo
1
这段代码作用是打印出模块加载后的__path__
变化情况.
我们的测试入口代码是pkgutil_extend_path.py
,其内容如下:
from pkg import foo from pkg.moduleA import foo
2
这段代码是用于检测模块下则子模块都导入了哪些的.
先来看直接执行
from pkg import foo from pkg.moduleA import foo
3
from pkg import foo from pkg.moduleA import foo
4
可以看到模块只是按默认的情况在执行,但如果先定义环境变量PYTHONPATH=extension
再执行
from pkg import foo from pkg.moduleA import foo
5
from pkg import foo from pkg.moduleA import foo
6
可以看到extension
中的not_shared
也可以被导入了.由此可见__path__
的一个很大的作用就是扩展其子模块的查找范围
1.4. *模块对象的生成
无论使用哪种导入方法,最终我们获得的都是一个模块对象,那它是怎么生成的呢?实际上这有两个步骤:
- 使用模块查找器
finder
找到模块 - 使用模块加载器
loader
将模块载入内存.
这两个的基类都可以在importlib.abc中找到
而他们之间使用模块规范对象(importlib.machinery.ModuleSpec
)来封装结果.
1.4.1. 模块查找流程
我们来完善下模块的查找流程:
当一个模块被用import
语句调用时,
from pkg import foo from pkg.moduleA import foo
7
1.4.2. finder
finder(importlib.abc.MetaPathFinder
或importlib.abc.PathEntryFinder
子类的实例)是一个模块查找器,我们总结它可以完成以下三件事中的任意一件:
- 抛出一个异常,然后完全取消所有的导入流程
- 返回一个None,意思是被导入的这个模块不能够被这个查找器所找到。但是他仍然可以被导入流的下一个阶段所找到,比如说一些自定义的查找器或者是Python的标准导入机制。
- 返回一个模块规范对象(
importlib.machinery.ModuleSpec
)用来加载实际的模块
sys.meta_path
中保存着当前可用的finder
,在加载时,虚拟机会遍历sys.meta_path
,调用其中所有元素的find_spec()
方法,以查看其中是否有一个对象是否可以找到要导入的模块.
调用find_spec()
方法时至少需要导入的模块的绝对名称.如果要导入的模块包含在包中,那么父包的__path__
属性作为第二个参数传入.
需要注意importlib.abc.MetaPathFinder
通常用于子类化sys.meta_path
中的finder类,而importlib.abc.PathEntryFinder
通常用于子类化sys.path_hooks
中工厂函数返回的finder类
我们可以看看默认的finder有哪些
from pkg import foo from pkg.moduleA import foo
8
from pkg import foo from pkg.moduleA import foo
9
1.4.3. loader
模块加载器(Loader
子类的实例)其实就是一个用来加载制定模块规范对象的对象,它会将finder中创建的模块规范对象加载到内存成为真正的模块.
loader通常在finder中使用,finder正确执行返回的模块规范对象中就包含一个字段用于存放loader,而导入流程的后半段就是执行这个模块规范对象的中loader,有两条路线:
如果loader定义了
exec_module(module)
方法:执行器就会执行loader的
exec_module(module)
方法,这个方法本身还会检测和调用create_module(spec)
.而create_module(spec)
会将spec对象转换成model对象如果loader没有定义
exec_module(module)
方法: 执行器会执行load_module(name)
.这种情况其实已经被弃用了,现在还保留只是为了向后兼容.
无论执行的哪步,最终执行的结果就是
- 为模块对象设置上了其自省字段
- 在本地模块的命名空间上设置好了要导入的名字
- 在sys.modules中注册上模块对象
1.5. *模块重加载
通常模块一旦被导入就不应该重载,但有时候确实会有这样的需求.importlib.reload(module)->new_module
提供了重新加载先前导入的模块的功能.
如果您已使用外部编辑器编辑了模块源文件,并希望在不离开Python解释器的情况下试用新版本,这将非常有用.
当执行reload
时,Python模块的代码被重新编译,模块级代码被重新执行,通过重用最初加载模块的loader
来定义一组新的对象.
1.5.1. 缺陷
- 重新导入会导致将不同的对象放置在
sys.modules
中 - 旧对象只有在引用计数下降到零后才被回收,不会因为重载而被回收
- 对旧对象的其他引用(例如模块外部的名称)不会被重新引用以引用新对象因此直接作用于使用
from ... import ...
语法导入的模块,因为难以追踪命名空间中哪些是模块中的 - 当模块被重新加载时,它的字典(包含模块的全局变量)被保留.名称的重定义将覆盖旧的定义,因此这通常不是问题.如果模块的新版本没有定义由旧版本定义的名称,则旧定义将保留.这个特性可以用于模块的优势,如果它维护一个全局表或缓存的对象可以用
try
语句,它可以测试表是否存在.
还没有评论,来说两句吧...