大师兄的Python源码学习笔记(三十五): 模块的动态加载机制(二)

大师兄的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,多个modulepackage最终组织成一个树形结构,从而为最初散乱的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中,packagemodule之间的区别并不是那么僵硬,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。

  • 对于fromimport的结合,还有一种更精妙的用法,可以加载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对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容