大師兄的Python源碼學習筆記(三十四): 模塊的動態(tài)加載機制(一)
大師兄的Python源碼學習筆記(三十六): 模塊的動態(tài)加載機制(三)
二邦邦、import機制的黑盒探測
- 從Python語法角度來說,import有多種寫法:
import pandas
import pandas.arrays
from pandas import Dataframe
from pandas import Dataframe as df
from pandas import *
- 從導入的目標來說逼龟,可以分為系統(tǒng)的標準模塊和用戶自己寫的模塊恬汁。
- 而用戶寫的模塊奢方,可以又分為python原生實現(xiàn)的模塊和C語言實現(xiàn)并以dll或者so形式存在的模塊。
1. 標準import
1.1 Python內(nèi)建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'>
- 可以發(fā)現(xiàn)歪沃,在
import sys
后,名字空間中增加了sys嫌松,而sys對應的是一個module對象沪曙,即源碼中的PyModuleObject對象。 - 根據(jù)前面的章節(jié)萎羔,我們知道Python在初始化的過程中液走,會將一大批module加載到內(nèi)存中,其中也包括sys module。
- 但為了使local名字空間能夠達到最干凈的效果缘眶,Python并沒有將這些符號暴露在當前的local名字空間中腻窒,而是需要用戶通過import機制通知Python實現(xiàn)這一點。
- 這些預先被加載進內(nèi)存的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這樣的內(nèi)建module是從sys.modules中導入的。
1.2 用戶自定義Module
- 在Python中砸喻,用戶可以通過.py文件創(chuàng)建自己的module柔逼,也可以通過C語言創(chuàng)建.dll或.so生成擴展module,這些都不是Python的內(nèi)建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'>
- 根據(jù)代碼結果愉适,Python通過import機制創(chuàng)建了一個新的module,將其引入到local名字空間中癣漆,并且還將其加載到sys.module中维咸。
- 由于id相同,表示其在local名字空間和sys.module中背后對應的是同一個PyModuleObject對象惠爽。
- 進一步探索test內(nèi)部:
>>>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對象內(nèi)部實際上是通過一個dict維護所有的屬性和屬性值癌蓖。
- 所以同class一樣,module是一個名字空間婚肆。
- 如果這時查看目錄租副,可以發(fā)現(xiàn)在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
- 可以看出當前l(fā)ocal名字空間中的__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
- 可以發(fā)現(xiàn)test.__builtins__對應的dict正是當前l(fā)ocal名字空間中的__builtins__對應的module對象所維護的那個dict對象攀操。
- 而其實它們兩個都只是表象院仿,它們背后的真身實際上就是我們在對Python運行環(huán)境初始化分析中看到的那個__builtin__ module以及它所維護的dict。
- 這個__builtin__ module和其它被Python預先加載到內(nèi)存的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']
- 可以發(fā)現(xiàn),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動作慈迈,不論發(fā)生在任何時間和位置若贮,都會影響到全局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中則沒有這么嚴格匈子,但是如果想調(diào)用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導入一個包虎敦,會先執(zhí)行這個包的__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之間的區(qū)別并不是那么僵硬堕仔,package也可以像module一樣唄加載擂橘,行為和module也是一樣的。
- 對于test的訪問必須通過mypackage.test來實現(xiàn)的好處摩骨,是避免在不同名字空間中產(chǎn)生名字沖突,這和C++中的namespace和Java中的package機制是一樣的朗若。
- 至于為什么會在加載mypackage.test時同時也加載mypackage恼五,是因為對于test module的引用只能通過mypackage.test來實現(xiàn),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結合遣总,可以實現(xiàn)精準控制加載對象睬罗,只將我們期望的module,甚至是module中的某個符號動態(tài)加載到內(nèi)存中旭斥,避免名字空間遭到污染容达。
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動態(tài)加載到了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提供了一種符號重命名機制,為動態(tài)加載機制提供了更大的靈活性奖亚。
- 通過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. 符號的銷毀與重載
- 模塊使用之后也可能會刪除爆袍,原因可能是釋放內(nèi)存或給名字空間瘦身等。
- 在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依然在系統(tǒng)中蜘醋,他沒有被刪除,只是被隱藏起來了咏尝。
- 之所以要采取這種類似module pool的緩存機制压语,是因為組成一個完整系統(tǒng)的多個py文件可能都會對某個module進行import動作,希望使用這個module所提供的的功能编检。
- 從Python的角度看胎食,import其實并不完全等同于我們鎖熟知的動態(tài)加載概念,它的真實含義是希望某個module能夠被感知允懂,就是將module以某個符號的形式引入到某個名字空間中厕怜。
- 所以Python引入了全局module集合sys.modules,這個集合為modules pool,保存了module的唯一映像蕾总,當某個py文件通過import聲明希望感知某個module時粥航,如果已經(jīng)在pool中,則引用一個符號到該py文件的名字空間中生百,并關聯(lián)到該module递雀;如果pool中不存在該module才會執(zhí)行動態(tài)加載動作。
- 假如在加載了module后置侍,module本身被更新映之,則需要使用builtin module中的reload操縱實現(xiàn):
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發(fā)生了變化
<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
- 可以看出拦焚,經(jīng)過reload后,module確實更新了杠输,但是id并沒有變化赎败,所以Python并沒有創(chuàng)建新的module對象。