python中import原理
# 0. 前言
在 python 中引入 Module 是再常见不过了,那么当我们 import 时它做了什么事情呢?它是如何加载 Module 使用的呢?
# 1. 什么是 module?
一般,Module 是一个后缀为 .py
的文件,其 module 名称一般是文件名称去除 .py
,我们可以通过 __name__
来查看 module 名称。
demo.py 是需要被引入的 module,main.py 是入口程序,它们在同一级目录。
# demo.py
print(__name__)
# main.py
import demo
>>> python main.py
demo
2
3
4
5
6
7
8
如果 module 为入口文件,则__name__为 __main__
,这也是常见 if __name__ == __main__:
的写法由来。
# demo.py
print(__name__)
>>> python demo.py
__main__
2
3
4
5
# 2. 什么是 Package?
包含了 __init__
文件的目录为 Package,该目录包含多个 py 文件,都属于 Module。我们在 import package 时,会初始化执行 package 的 __init__.py
文件,然后将其作为一个 Module 对象给放在当前的全局变量中。
├───demo
│ │ __init__.py
| main.py
2
3
# __init__.py
print("demo __init__")
# main.py
import demo
print(demo)
print(globals()["demo"])
>>> python main.py
output:
demo __init__.py
<module 'demo' from 'D:\\code\\my_demo\\demo\\__init__.py'>
<module 'demo' from 'D:\\code\\my_demo\\demo\\__init__.py'>
2
3
4
5
6
7
8
9
10
11
12
13
可以看到 package 的名称 demo 是在 globals()中的,并且其是一个 module 对象,包含了该 __init__.py
文件所在的路径。
如果想要导入 package 下的 module,可以通过 from package import module
的方式将其加载到当前的全局变量中。
├───demo
│ │ __init__.py
| | a.py
| main.py
2
3
4
# __init__.py
# a.py
class Demo:
pass
# main.py
from demo import a
print(a)
print(a.Demo)
print(globals()["a"])
>>> python main.py
<module 'demo.a' from 'D:\\code\\my_demo\\demo\\a.py'>
<class 'demo.a.Demo'>
<module 'demo.a' from 'D:\\code\\my_demo\\demo\\a.py'>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3. module 缓存
- module 缓存初始化
在 python 程序初始化时,会将大批的内置 module 提前加载到内存中,保存在 sys.modules
中,这是一个字典,是以 module 名称或者 package 名称为 key,module 对象为 value 存储。
>>> import sys
>>> sys.modules
{... 'os': <module 'os' from '/usr/lib64/python3.6/os.py'> ...}
>>> sys.modules["os"].cpu_count()
8
>>> os.cpu_count()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'os' is not defined
2
3
4
5
6
7
8
9
- 将 module 添加到当前去全局变量中
既然提前加载了,但是这里为什么找不到 os 呢?这是因为虽然 sys.modules
中已经存在了,但是并没有把 os 加入到当前的全局变量中。
>>> globals()["os"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'os'
2
3
4
所以当我们通过 import os 时,它会通过模块名称在 sys.modules 找到其 module 对象,然后再将其加入到当前的全局变量中,这样就可以使用它了。
>>> import os
>>> globals()["os"]
<module 'os' from '/usr/lib64/python3.6/os.py'>
>>> os.cpu_count()
8
>>> id(sys.modules["os"])
140260375998856
>>> id(os)
140260375998856
2
3
4
5
6
7
8
9
可以看到从 sys.modules 中拿到的 os 对象的地址和当前导入的 os 的地址是一致的,无论 import 多少次相同的 module,都是从该全局 sys.modules 中获取,拿到的都是同一个对象,也是单例模式实现的一种。
- 导入 module 中的属性
如果我只是引入 module 中的一个属性变量呢?那 sys.modules
中还是会加载该 module,将其属性变量作为全局变量引入。
>>> import sys
>>> sys.modules["json"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'json'
>>> from json import load
>>> sys.modules["json"]
<module 'json' from '/usr/lib64/python3.6/json/__init__.py'>
2
3
4
5
6
7
8
- 模块不需要了,del 销毁
del 销毁的只是销毁当前全局变量中的变量,并不会影响 sys.modules 中的缓存。为什么不销毁 sys.modules 中的呢?是因为该销毁的 module 可能还会在其他的文件中引用。
>>> import json
>>> import sys
>>> sys.modules["json"]
<module 'json' from '/usr/lib64/python3.6/json/__init__.py'>
>>> globals()["json"]
<module 'json' from '/usr/lib64/python3.6/json/__init__.py'>
>>> del json
>>> sys.modules["json"]
<module 'json' from '/usr/lib64/python3.6/json/__init__.py'>
>>> globals()["json"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'json'
2
3
4
5
6
7
8
9
10
11
12
13
- module 重新加载
因为每次 import 都是从 sys.modules 的缓存中获取,那么如果 module 文件变动,则无法拿到最新的 module,这个时候需要通过手动调用 importlib.reload 来重新加载,从本地文件中重新加载 module 对象到 sys.modules 中。
在当前目录下创建 demo.py 文件,内容为空
# demo.py
>>> import demo
>>> demo.Demo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'demo' has no attribute 'Demo'
2
3
4
5
6
7
这个时候在 demo.py 中添加:
class Demo:
pass
2
reload demo 后,可以看到加载到 Demo 了。
>>> reload(demo)
<module 'demo' from '/root/work/mydemo/demo.py'>
>>> demo.Demo
<class 'demo.Demo'>
2
3
4
那如果 import 的 module 或者 package 没有在 sys.modules
中呢,这个时候就要去 sys.path
中去本地搜索了。
# 4. 搜索路径
sys.path
是一个列表,其中包含了要去搜索 module 的本地路径。当 sys.modules 中查找不到 module 时,将会从该路径中搜索到 module 文件并将其加载到 sys.modules 中来。
sys.path 的路径的来源有:
- 运行脚本所在的目录
PYTHONPATH
环境变量- python 安装时的默认设置
当在搜索路径找到该 module 的本地路径后,会将其加载到 sys.modules 中,然后再将其添加到当前的全局变量中。
# 5. 总结
import 的加载过程:
- 先从 sys.modules 中查看是否有导入的模块,有,则获取该模块,并加入到当前的全局变量中。
- 如果 sys.modules 中没有需要导入的模块,则按照 sys.path 中的目录路径进行搜索找到对应的模块文件再加载到 module 对象中返回。
# 6. 加入腾讯云开发者社区
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3u3wcswfaeiok