python中import原理

2023-03-04 15:37:24 浏览数 (1)

0. 前言

在 python 中引入 Module 是再常见不过了,那么当我们 import 时它做了什么事情呢?它是如何加载 Module 使用的呢?

1. 什么是 module?

一般,Module 是一个后缀为 .py 的文件,其 module 名称一般是文件名称去除 .py,我们可以通过 __name__ 来查看 module 名称。

demo.py 是需要被引入的 module,main.py 是入口程序,它们在同一级目录。

代码语言:javascript复制
# demo.py
print(__name__)

# main.py
import demo

>>> python main.py
demo

如果 module 为入口文件,则__name__为 __main__,这也是常见 if __name__ == __main__: 的写法由来。

代码语言:javascript复制
# demo.py
print(__name__)

>>> python demo.py
__main__

2. 什么是 Package?

包含了 __init__ 文件的目录为 Package,该目录包含多个 py 文件,都属于 Module。我们在 import package 时,会初始化执行 package 的 __init__.py 文件,然后将其作为一个 Module 对象给放在当前的全局变量中。

代码语言:javascript复制
├───demo
│   │   __init__.py
|   main.py
代码语言:javascript复制
# __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'>

可以看到 package 的名称 demo 是在 globals()中的,并且其是一个 module 对象,包含了该 __init__.py 文件所在的路径。

如果想要导入 package 下的 module,可以通过 from package import module 的方式将其加载到当前的全局变量中。

代码语言:javascript复制
├───demo
│   │   __init__.py
|   |   a.py
|   main.py
代码语言:javascript复制
# __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'>

3. module 缓存

  • module 缓存初始化

在 python 程序初始化时,会将大批的内置 module 提前加载到内存中,保存在 sys.modules 中,这是一个字典,是以 module 名称或者 package 名称为 key,module 对象为 value 存储。

代码语言:javascript复制
>>> 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
  • 将 module 添加到当前去全局变量中

既然提前加载了,但是这里为什么找不到 os 呢?这是因为虽然 sys.modules 中已经存在了,但是并没有把 os 加入到当前的全局变量中。

代码语言:javascript复制
>>> globals()["os"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'os'

所以当我们通过 import os 时,它会通过模块名称在 sys.modules 找到其 module 对象,然后再将其加入到当前的全局变量中,这样就可以使用它了。

代码语言:javascript复制
>>> 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

可以看到从 sys.modules 中拿到的 os 对象的地址和当前导入的 os 的地址是一致的,无论 import 多少次相同的 module,都是从该全局 sys.modules 中获取,拿到的都是同一个对象,也是单例模式实现的一种。

  • 导入 module 中的属性

如果我只是引入 module 中的一个属性变量呢?那 sys.modules 中还是会加载该 module,将其属性变量作为全局变量引入。

代码语言:javascript复制
>>> 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'>
  • 模块不需要了,del 销毁

del 销毁的只是销毁当前全局变量中的变量,并不会影响 sys.modules 中的缓存。为什么不销毁 sys.modules 中的呢?是因为该销毁的 module 可能还会在其他的文件中引用。

代码语言:javascript复制
>>> 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'
  • module 重新加载

因为每次 import 都是从 sys.modules 的缓存中获取,那么如果 module 文件变动,则无法拿到最新的 module,这个时候需要通过手动调用 importlib.reload 来重新加载,从本地文件中重新加载 module 对象到 sys.modules 中。

在当前目录下创建 demo.py 文件,内容为空

代码语言:javascript复制
# demo.py

>>> import demo
>>> demo.Demo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'demo' has no attribute 'Demo'

这个时候在 demo.py 中添加:

代码语言:javascript复制
class Demo:
    pass

reload demo 后,可以看到加载到 Demo 了。

代码语言:javascript复制
>>> reload(demo)
<module 'demo' from '/root/work/mydemo/demo.py'>
>>> demo.Demo
<class 'demo.Demo'>

那如果 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 的加载过程:

  1. 先从 sys.modules 中查看是否有导入的模块,有,则获取该模块,并加入到当前的全局变量中。
  2. 如果 sys.modules 中没有需要导入的模块,则按照 sys.path 中的目录路径进行搜索找到对应的模块文件再加载到 module 对象中返回。

6. 加入腾讯云开发者社区

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3u3wcswfaeiok

0 人点赞