Python 模块加载原理解析

import

在交互环境下,使用不带参数的dir()可以打印当前local命名空间的所有

 

>>> locals()
{'__builtins__': , '__name__': '__main__', '__doc__': None, '__package__': None}
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__']

通过importlocal添加sys模块

 

>>> import sys
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys']
>>> locals()
{'__builtins__': , '__name__': '__main__', 'sys': , '__doc__': None, '__package__': None}

可以看到,import机制使得加载的模块在local命名空间可见

在 Python 初始化时,就有大批 module 已经加载到内存中,但为了保持local空间的干净,并没有直接将这些 module 放入local空间,而是让用户通过import机制通知 Python:我的程序需要调用某个模块,请将它放入local命名空间。

那些被预加载到内存的模块,存放在 全局module集合sys.modules中(下面列出了部分)。此时虽然sys.modules中能看到os,但调用失败,因为还没有放入local空间

 

>>> sys.modules
{'sys': , 'os.path': , 'os': }
>>> os
Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'os' is not defined

通过import os,将模块放入local空间,用于调用

 

>>> import os
>>> os

>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'os', 'sys']
>>> locals()
{'__builtins__': , '__package__': None, 'sys': , '__name__': '__main__', 'os': , '__doc__': None}

那么,对于那些一开始就并不在sys.modules中的模块,比如用户自定义的模块,又会如何呢?

准备了一个简单的module——hello.py:

 

a = 1
b = 2

hello.py进行import操作,结果,将'hello'加载进了sys.modules,同时放入了local空间

 

>>> sys.modules['hello']
Traceback (most recent call last):
  File "", line 1, in 
KeyError: 'hello'
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'os', 'sys']
>>> import hello
>>> sys.modules['hello']

>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'hello', 'os', 'sys']

通过id模块,查看这两个对象指向了同一个内存地址空间,是同一个模块对象。

 

>>> id(hello)
4300177880
>>> id(sys.modules['hello'])
4300177880

可以看到,module对象其实就是通过一个dict维护所有的属性和值。也就是说,是一个名字空间。

 

>>> dir(hello)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'b']
>>> hello.__dict__.keys()
['a', 'b', '__builtins__', '__file__', '__package__', '__name__', '__doc__']
>>> hello.__name__
'hello'
>>> hello.__file__
'/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/hello.py'
>>> hello.__package__
>>> hello.__doc__
>>> hello.a
1
>>> hello.b
2

嵌套import

那么,对于嵌套的module呢?

hi1.py

 

import hi2

hi2.py

 

import sys

结果可以看到,在local空间中也是嵌套的

 

>>> import hi1
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'hello', 'hi1', 'os', 'sys']
>>> dir(hi1)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'hi2']
>>> dir(hi2)
Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'hi2' is not defined
>>> dir(hi1.hi2)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'sys']

这些模块也同时出现在了sys.modules中,这样,当其他地方也要调用这些模块时,可以直接返回其中的 module对象。

 

>>> sys.modules['hi1']

>>> sys.modules['hi2']

>>> sys.modules['sys']

import package

如同一些相关的 class 可以放入一个 module 中,相关的 module 也可以放入一个 package,用于管理 module。当然,多个小 package 也可以组成一个较大的 package。

 

➜  task  pwd
/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/task
➜  task  ls
tank1.py tank2.py

 

>>> import task
Traceback (most recent call last):
  File "", line 1, in 
ImportError: No module named task

package 就是一个文件夹,但并非所有文件夹都是package,只有在文件夹中有一个特殊文件__init__.py时,Python 才会认为是合法的,即使这个文件是空的。

 

➜  task  ls
__init__.py tank1.py    tank2.py

再次导入 package,自动加载执行__init__.py,生成字节码__init__.pyc

 

>>> import task

➜  task  ls
__init__.py  __init__.pyc tank1.py     tank2.py

导入 package 中的 module

 

>>> import task.tank1
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', '__warningregistry__', 'hello', 'hi1', 'os', 'sys', 'task']
>>> dir(task)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'tank1']
>>> sys.modules['task']

>>> sys.modules['tank1']
Traceback (most recent call last):
  File "", line 1, in 
KeyError: 'tank1'
>>> sys.modules['task.tank1']

可以看到,module 和 package 的导入基本一样。只不过包本身也被加载进来,而且在sys.modules中,模块名称中包含包名,这是为了区别不同包中的同名模块,以便共存。

为什么task也会被加载呢,似乎用户并不需要?这是因为对tank1的引用需要通过task.tank1实现,Python 会首先在local空间中查找task,然后在task的属性集合中查找tank1

import task.tank1时,只会导入tank1,不会同时导入tank2

而且,packagetask只会加载一次。

在导入类似A.B.C的结构时,Python 会视为树形结构,B在A下,C在B下,Python 会对这个结构进行分解,形成(A,B,C)的节点集合。从左到右依次到sys.modules中查找是否已经加载,如果是一个 package,A已经加载,但 B 和 C 还没有,就到 A 对应的 模块对象 的__path__中查找路径,并只在路径中搜索。

from import

我们可以只将 package 中的某个 module 甚至 某个 module 中的某个符号加载到内存,比如上例中,我们希望直接将tank1引入local空间,不需要引入task

 

>>> import sys
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys']
>>> from task import tank1
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys', 'tank1']
>>> sys.modules['task']

>>> sys.modules['task.tank1']

可以看到,虽然local中只有tank1,但sys.modules依然加载了tasktask.tank1,所以,import 和 from import 本质是一样的,需要将 包 和 模块 同时加载到sys.modules,区别只是加载完成后,将什么名称放入local空间。

import task.tank1中,引入了task并映射到module task,而在from task import tank1中,引入了tank1并映射到module task.tank1

可以只引入模块中的一些对象

tank1.py

 

a = 1

b = 2

 

>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys']
>>> from task.tank1 import a
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'a', 'sys']
>>> a
1
>>> sys.modules['task']

>>> sys.modules['tank1']
Traceback (most recent call last):
  File "", line 1, in 
KeyError: 'tank1'
>>> sys.modules['task.tank1']

>>> sys.modules['task.tank1.a']
Traceback (most recent call last):
  File "", line 1, in 
KeyError: 'task.tank1.a'

也可以一次性导入 module 中的所有对象

 

>>> from task.tank1 import *
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'a', 'b', 'sys']

import as & from import as

Python 还允许自己重命名被引入local的模块

 

>>> import task.tank1 as test
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys', 'test']
>>> sys.modules['test']
Traceback (most recent call last):
  File "", line 1, in 
KeyError: 'test'
>>> sys.modules['task.tank1']

这时,test映射到了module task.tank1

模块的销毁

如果一个模块不需要了,可以通过del销毁,但这样这的是销毁了这个模块对象么?

 

>>> del test
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys']
>>> sys.modules['task.tank1']

可以看出,del 只是简单地将testlocal中删除,并没有从 module集合 中销毁。但这样已经能够让 Python程序无法访问这个模块,认为test不存在了。

为什么 Python 不直接将模块从sys.modules删除?因为一个系统的多个Python文件可能都会对某个module进行 import,而 import 的作用并非一直以为的加载,而是让某个module能够被感知,即以某种符号的形式引入某个名字空间。所以,使用全局的sys.modules保存module的唯一映射,如果某个Python文件希望感知到,而sys.modules中已经加载,就将这个模块名称引入该Python文件的名称空间,然后关联到sys.modules中的该模块,如果sys.modules中不存在,才会进行加载。

模块重新载入

那么,对于sys.modules中已经加载到内存的模块,如果后来对内容进行了修改,怎么让Python知道呢?有一种重新加载的机制。

 

>>> import task.tank1 as test
>>> dir()
['__builtins__', '__doc__', '__name__', '__package__', 'sys', 'test']
>>> dir(test)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'b']
>>> test.a
1
>>> test.b
1

tank1.py 添加 整数对象 c

 

a = 1

b = 1

c = 3

重新加载,C出现了

 

>>> reload test
  File "", line 1
    reload test
              ^
SyntaxError: invalid syntax
>>> reload(test)

>>> dir(test)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'b', 'c']
>>> test.a
1
>>> test.b
1
>>> test.c
3
>>> id(test)
4300178104
>>> reload(test)

>>> id(test)
4300178104

id(test)可以看出,依然是原来的对象,Python只是在原有对象中添加了C和对应的值。

但是,删除b = 1后,还可以调用,说明Python的重新加载只是向module添加新对象,而不管是否已经删除。

可以通过用del直接删除的方式进行更新

 

>>> del test.b
>>> dir(test)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'c']
>>> reload(test)

>>> dir(test)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'c']



作者:超net
链接:https://www.jianshu.com/p/c04dc172335e
 

你可能感兴趣的:(Python,python)