大师兄的Python源码学习笔记(三十五): 模块的动态加载机制
2021-09-30 本文已影响0人
superkmi
大师兄的Python源码学习笔记(三十四): 模块的动态加载机制(一)
大师兄的Python源码学习笔记(三十六): 模块的动态加载机制(三)
二、import机制的黑盒探测
- 从Python语法角度来说,import有多种写法:
import pandas
import pandas.arrays
from pandas import Dataframe
from pandas import Dataframe as df
from pandas import *
- 从导入的目标来说,可以分为系统的标准模块和用户自己写的模块。
- 而用户写的模块,可以又分为python原生实现的模块和C语言实现并以dll或者so形式存在的模块。
1. 标准import
1.1 Python内建Module
- 以sys模块为例,查看import对当前名字空间的影响:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>> import sys
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'sys']
>>> type(sys)
<class 'module'>
- 可以发现,在
import sys
后,名字空间中增加了sys,而sys对应的是一个module对象,即源码中的PyModuleObject对象。 - 根据前面的章节,我们知道Python在初始化的过程中,会将一大批module加载到内存中,其中也包括sys module。
- 但为了使local名字空间能够达到最干净的效果,Python并没有将这些符号暴露在当前的local名字空间中,而是需要用户通过import机制通知Python实现这一点。
- 这些预先被加载进内存的module存放在sys.module中:
demo.py
>>>import sys
>>>def show_modules():
>>> for item in sys.modules.items():
>>> print(item)
>>>show_modules()
('sys', <module 'sys' (built-in)>)
('builtins', <module 'builtins' (built-in)>)
('_frozen_importlib', <module 'importlib._bootstrap' (frozen)>)
('_imp', <module '_imp' (built-in)>)
('_warnings', <module '_warnings' (built-in)>)
('_frozen_importlib_external', <module 'importlib._bootstrap_external' (frozen)>)
('_io', <module 'io' (built-in)>)
... ...
- 如果模拟os模块import到local名字空间的过程:
demo.py
>>> import sys
>>> id(sys.modules['os'])
1755873529264
>>> import os
>>> id(os)
1755873529264
- 可以看出手动导入和import的id是一样的,综上所述,可以证明类似sys module这样的内建module是从sys.modules中导入的。
1.2 用户自定义Module
- 在Python中,用户可以通过.py文件创建自己的module,也可以通过C语言创建.dll或.so生成扩展module,这些都不是Python的内建module。
- 建立一个简单的案例:
test.py
>>>a=1
>>>b=2
demo.py
>>>import sys
>>>def test_in_modules():
>>> return print("test" in sys.modules.keys())
>>>test_in_modules()
False
>>>import test
>>>test_in_modules()
True
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys', 'test', 'test_in_modules']
>>>print(id(test))
2018381998256
>>>print(id(sys.modules['test']))
2018381998256
>>>print(type(test))
<class 'module'>
- 根据代码结果,Python通过import机制创建了一个新的module,将其引入到local名字空间中,并且还将其加载到sys.module中。
- 由于id相同,表示其在local名字空间和sys.module中背后对应的是同一个PyModuleObject对象。
- 进一步探索test内部:
>>>import test
>>>print(dir(test))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
>>>print(dir(test.__dict__.keys()))
['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'isdisjoint']
>>>print(test.__name__)
test
>>>print(test.__file__)
.\test.py
- 可以看出module对象内部实际上是通过一个dict维护所有的属性和属性值。
- 所以同class一样,module是一个名字空间。
- 如果这时查看目录,可以发现在import过程中,Python在__pycache__文件夹下生成了用于储存编译结果的test.pyc文件。
- 观察__builtins__符号:
demo.py
>>>import test
>>>print(type(__builtins__))
<class 'module'>
>>>print(id(__builtins__))
1761693991696
>>>print(type(test.__builtins__))
<class 'dict'>
>>>print(id(test.__builtins__))
1761693989184
- 可以看出当前local名字空间中的__builtins__和test中的__builtins__虽然名字一样,但一个是module对象,一个是dict,且id不同,所以并不是同一个东西。
- 再深挖__builtins__:
demo.py
import test
>>>print(id(test.__builtins__))
2325670918464
>>>print(id(__builtins__.__dict__))
2325670918464
>>>print(id(sys.modules['builtins'].__dict__))
2325670918464
- 可以发现test.__builtins__对应的dict正是当前local名字空间中的__builtins__对应的module对象所维护的那个dict对象。
- 而其实它们两个都只是表象,它们背后的真身实际上就是我们在对Python运行环境初始化分析中看到的那个__builtin__ module以及它所维护的dict。
- 这个__builtin__ module和其它被Python预先加载到内存的module一样,维护在sys.modules中。
2. 嵌套import
- 首先建立一个嵌套import案例:
test1.py
import sys
test2.py
import test1
demo.py
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys']
>>>import test2
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys', 'test2']
>>>print(dir(test2))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'test1']
>>>print(dir(test2.test1))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys']
- 可以发现,test1和test2中进行的import动作并没有影响到上一层名字空间,而只影响了各个module自身的名字空间,也就是module自身维护的dict对象。
- 但确实会影响到全局module集合:
demo.py
>>>import test2
>>>print(sys.modules['test2'])
<module 'test2' from 'D:\\pythonProject\\parser_learn\\test2.py'>
- 所有的import动作,不论发生在任何时间和位置,都会影响到全局module集合。
- 这样的好处是当程序重复import模块时,Python虚拟机只需要返回全局module集合中缓存的那个module对象即可:
demo.py
>>>import test2,test1
>>>print(id(test1))
2555034952864
>>>print(id(test2.test1))
2555034952864
3. import package
- 在Python中,package(包)用于管理多个module(模块),一个package通常就是一个目录。
- 多个小package也可以聚合成一个较大的package,多个module、package最终组织成一个树形结构,从而为最初散乱的class建立起一种方便管理、维护和用户试用的结构,以xml package为例:
mypackage.test.py
>>>a=1
>>>b=2
- 在Python2中,如果要成为一个package,则在目录下必须有一个文件__init__.py,在Python3中则没有这么严格,但是如果想调用package中的模块,还是需要先在__init__.py中定义。
demo.py
import mypackage
>>>print(mypackage)
<module 'mypackage' (namespace)>
>>>print(mypackage.test)
Traceback (most recent call last):
File "D:/demo.py", line 4, in <module>
print(mypackage.test)
AttributeError: module 'mypackage' has no attribute 'test'
- 增加__init__.py后:
mypackage.__init__.py
from . import test
demo.py
import mypackage
>>>print(mypackage)
<module 'mypackage' from 'D:\\mypackage\\__init__.py'>
>>>print(mypackage.test)
<module 'mypackage.test' from 'D:\\mypackage\\test.py'>
- 可以看出python导入一个包,会先执行这个包的__init__文件。
- 再深入分析导入的结果:
demo.py
>>>import sys
>>>import mypackage.test
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'mypackage', 'sys']
>>>import mypackage
>>>print(dir(mypackage))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'test']
>>>print(id(mypackage.test))
2378910617488
>>>print(id(sys.modules['mypackage.test']))
2378910617488
- 可以看出在导入mypackage.test时,实际连mypackage一起加载到名字空间了,这说明在Python中,package和module之间的区别并不是那么僵硬,package也可以像module一样呗加载,行为和module也是一样的。
- 对于test的访问必须通过mypackage.test来实现的好处,是避免在不同名字空间中产生名字冲突,这和C++中的namespace和Java中的package机制是一样的。
- 至于为什么会在加载mypackage.test时同时也加载mypackage,是因为对于test module的引用只能通过mypackage.test来实现,Python会首先在当前的local名字空间中查找mypackage对应的对象,然后再在该对象的属性集合(名字空间)中查找test。
- 但如果在同一个package中有多个module时,加载其中一个module并不会加载其它module:
mypackage.test1
>>>a=1
>>>b=2
mypackage.test2
>>>c=3
>>>d=4
demo.py
>>>import mypackage.test1
>>>print(dir(mypackage))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'test1']
>>>import mypackage.test2
>>>print(dir(mypackage))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'test1', 'test2']
4. from 与 import
- 通过from关键字与import结合,可以实现精准控制加载对象,只将我们期望的module,甚至是module中的某个符号动态加载到内存中,避免名字空间遭到污染。
demo.py
>>>import sys
>>>from mypackage import test1
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys', 'test1']
>>>print(sys.modules['mypackage.test1'])
<module 'mypackage.test1' from 'D:\\mypackage\\test1.py'>
- 这种方式本质上与
import mypackage.test1
是一样的,都是将package mypackage和module mypackage.test1动态加载到了sys.modules集合中。 - 不同之处在于当import动作要结束时,Python会在当前的local名字空间中引入什么符号:
在
import mypackage.test1
中,Python虚拟机引入了符号mypackage,并将其映射到module mypackage。
在from mypackage import test1
中,Python虚拟机引入了符号test,并将其映射到module mypackage.test。
- 对于from与import的结合,还有一种更精妙的用法,可以加载module的某个部分:
demo.py
>>>import sys
>>>from mypackage.test1 import a
>>>print(a)
1
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'sys']
>>>print(sys.modules['mypackage.test1'])
<module 'mypackage.test1' from 'D:\\mypackage\\test1.py'>
- 除此之外,Python还提供了一种机制,允许将一个module中的所有对象一次性地引入到当前名字空间中:
demo.py
>>>from mypackage.test1 import *
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
5. 符号重命名
- Python通过关键字as提供了一种符号重命名机制,为动态加载机制提供了更大的灵活性。
- 通过as可以控制module以什么名字被引入到当前的local名字空间中:
demo.py
>>>import sys
>>>import mypackage.test1 as test
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys', 'test']
>>>print(sys.modules['mypackage.test1'])
<module 'mypackage.test1' from 'D:\\mypackage\\test1.py'>
- 可以看出,在上面代码中,test实际是被映射到module mypackage.test1。
6. 符号的销毁与重载
- 模块使用之后也可能会删除,原因可能是释放内存或给名字空间瘦身等。
- 在python中,通常删除一个对象可以使用del关键字:
demo.py
>>>import sys
>>>import mypackage.test1 as test
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys', 'test']
>>>del test
>>>print(dir())
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys']
>>>print(sys.modules['mypackage.test1'])
<module 'mypackage.test1' from 'D:\\pythonProject\\parser_learn\\mypackage\\test1.py'>
- 可以看出,del过后,test确实被从名字空间中删除了,但是module mypackage.test1依然在系统中,他没有被删除,只是被隐藏起来了。
- 之所以要采取这种类似module pool的缓存机制,是因为组成一个完整系统的多个py文件可能都会对某个module进行import动作,希望使用这个module所提供的的功能。
- 从Python的角度看,import其实并不完全等同于我们锁熟知的动态加载概念,它的真实含义是希望某个module能够被感知,就是将module以某个符号的形式引入到某个名字空间中。
- 所以Python引入了全局module集合sys.modules,这个集合为modules pool,保存了module的唯一映像,当某个py文件通过import声明希望感知某个module时,如果已经在pool中,则引用一个符号到该py文件的名字空间中,并关联到该module;如果pool中不存在该module才会执行动态加载动作。
- 假如在加载了module后,module本身被更新,则需要使用builtin module中的reload操纵实现:
demo.py
>>> import importlib,sys
>>> import mypackage.test1 as test
>>> id(test)
2543593826720
>>> sys.modules['mypackage.test1']
<module 'mypackage.test1' from 'D:\\mypackage\\test1.py'>
>>> dir(sys.modules['mypackage.test1'])
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
>>> importlib.reload(test) # 这里module test发生了变化
<module 'mypackage.test1' from 'D:\\mypackage\\test1.py'>
>>> dir(sys.modules['mypackage.test1'])
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'c']
>>> id(test)
2543593826720
- 可以看出,经过reload后,module确实更新了,但是id并没有变化,所以Python并没有创建新的module对象。