一、Module and Package?
在Python中,Module和Package十分相似,但又有所不同。
1. 相同点
import os # module
import concurrent # package
无论是module还是package, 直接使用import语句导入它们都是没有问题的。
此外,Python解释器导入module或package的第一步, 都是先查找缓存。如果缓存里没有这个变量,它才会真正去做"load"这个动作。
并且,当我们打印:
print(type(os)) # <class 'module'>
print(type(concurrent)) # <class 'module'>
我们会发现,module和package实际上都是module类。实际上,package就是特殊的module。
2. 不同点
Module通常指一个.py文件,而Package则是一个文件夹。
-
当Python解释器load一个Module时,它首先会在一个单独的命名空间中执行一遍这个module对应的.py文件,然后将运行时得到的Python object作为属性赋值给module object。module此时就是一个普普通通的对象,你可以用module.XX来访问这个对象中的属性。
-
当Python解释器load一个package时,它首先会在package对应的文件夹中寻找是否存在__init__.py这个文件。如果找不到这个文件,那么什么文件都不会运行。我们除了得到一个全局变量package module对象外,不会得到任何东西。反之,如果有__init__.py文件,python解释器会主动运行它,并将运行时得到的Python object作为属性赋值给package, 就像上面的module一样。
二、Python解释器运行时如何查找Package和module所在位置?
1. 区分内置模块、标准库模块和 第三方模块
作为过来人,我们很容易误以为Python的内置模块和标准库模块是同一个东西,因为它们在诸多方面有种完全相同的特征。但事实上,它们是不同的。
-
Built-in Module 内置模块是 Python解释器直接提供的模块,它们是编译到 Python 解释器中的,不需要从文件系统加载。千万务必注意最后这句话:“不需要从文件系统加载”。 (问题1)
-
Standard Library Modules标准库模块是在Python安装时就直接存在于安装目录的Lib目录下的模块。这也就意味着,它们的导入依赖文件系统,你真把这个文件删了,Python也就没法导入这个标准库了。
-
Third-party Modules,通常是pip安装的模块,也有可能是你自己在当前目录写的package或module。总之,既不是内置模块,也不是标准库模块的,通通称之为第三方模块。
2. 最高优先级的Built-in模块
正是由于Built-in模块已经深深嵌入Python解释器当中,当我们使用语句,如:
import test
Python解释器的第一步,就是“自查”,看看test是不是嵌入模块。像os就是一个嵌入式模块。
此时,哪怕你是否在当前目录下存在一个os.py文件,Python解释器都不会错误的把当前目录的os.py作为module导入。
3. 同等地位的标准库和第三方模块
比Built-in模块查找地位略低的,是标准库模块和第三方模块。
还是上面的例子,当我们import test时,当Python解释器确认这不是一个Built-in模块后,它就会跑到sys.path中进行寻找。
import sys
print(sys.path)
# ['D:\\Learning\\Python.learning\\XXX', 'D:\\python3.11\\python311.zip', 'D:\\python3.11\\DLLs', 'D:\\python3.11\\Lib', 'D:\\python3.11', 'C:\\Users\\pbl\\AppData\\Roaming\\Python\\Python311\\site-packages', 'D:\\python3.11\\Lib\\site-packages', 'D:\\python3.11\\Lib\\site-packages\\win32', 'D:\\python3.11\\Lib\\site-packages\\win32\\lib', 'D:\\python3.11\\Lib\\site-packages\\Pythonwin']
需要注意的是,Python解释器在查找模块时,是严格按照sys.path中的顺序进行的。而sys.path中的第一个路径就是你当前的工作目录,优先级最高。
这也就意味着: 如果你在当前目录下创建了一个与标准模块或第三方同名的.py文件或者文件夹,那么就会产生糟糕的“覆盖”现象!

运行结果:
ModuleNotFoundError: No module named 'concurrent.futures'; 'concurrent' is not a package。
三、绝对导入与相对导入
1. 直观的写法区别
from package import moduleA # 绝对导入
from . import moduleB # 相对导入
从写法上直观的看,相对导入的模块以"."开头,而绝对导入则直接以模块名开头。
2. 绝对导入的不同形式
绝对导入有多种不同的形式:
import packageA # ①
import packageA.moduleA # ②
from packageA.moduleA import A # ③
尽管形式多样,但它们的第一步都是先按照(二)中的顺序进行模块查找;
例如导入语句:
import packageA.moduleA
Python解释器首先会看package是不是嵌入式模块,如果不是,就按照sys.path里的顺序查找package;
找到package后,就在package里找有没有moduleA。如果没有,就退出去,继续在sys.path里看还有没有其他package,直到最后找到package, 并且在package中找到moduleA;
回顾前面提到的知识点,由于packageA是一个包而非模块,Python解释器会看packageA中是否有init.py文件,并尝试运行。如果没有,由于导入语句最终指向模块的是moduleA,Python解释器会将moduleA作为属性添加到packageA中。
特别需要注意,哪怕packageA中本来有moduleA.py这个文件,但如果你只是使用导入语句:
import packageA
packageA这个不会有moduleA这个属性。这一点你可以通过代码print(dir(packageA))进行查看。这便是导入语句①②的区别。
而对于导入语句③,它们和①②唯一的区别就是: 在Python解释器通过moduleA或packageA找到对应模块,将模块对应的__init__.py脚本运行一遍(如果存在)。在把模块中对应的A变量赋值给当前运行脚本的A变量后就完成了任务。packageA.moduleA这个变量也是无法访问的。
3. 让人疑惑的相对导入
如果你尝试过直接运行过使用相对导入的.py文件,例如:
from .moduleA import A
你会遭遇错误:ImportError: attempted relative import with no known parent package。 我们首先得明白,一切相对导入,都只是形式上的。真正在Python运行时,相对导入都会变成绝对导入。 那么Python是如果把相对导入变成绝对导入的呢?事实上,它是依靠__package__这个变量。
当我们在一个.py中打印__package__时:
print(__package__) # None
我们会得到None。这意思是,Python不认为当前文件所在目录是模块,而是一个脚本文件。 但是,如果我们导入一个package, 如 from package import moduleA, 然后打印moduleA.__package__时,我们就会得到moduleA当前所在package文件夹的真实名字: package。
假设在moduleA中存在导入语句:
from .utils import Utils
那么在Python运行时,它就会变成:
from package.utils import Utils;
所以,本质上,你无法运行一个使用相对导入的文件,是因为它无法根据__package__(=None)把"相对导入"变成"绝对导入"。
但如果你是从别处导入这个文件,那么这个使用相对导入文件的package变量就是正确的。
那么有没有办法直接以模块的形式运行使用相对导入的模块呢?很简单,你可以使用:
python -m moduleA.py。
其中, “-m”参数就是以模块形式运行".py"文件的意思。 通过这种方式, 这个相对导入文件中的package就是当前文件夹的名字,而非None了。
4. 相对导入有何用?
如果你尝试过自己写一个package, 并且package中有多个python文件,文件与文件之间存在互相导入的关系。那么,你就会发现,如果此时文件与文件之间的导入全部是绝对导入,当你在直接运行这些module时,不会出现任何问题。
那么什么时候会出现问题呢?答案是当一个外部文件导入这个package的时候!
为了更好地模拟,我们假设存在这么一个文件目录结构:
Root/
│
├── mypackage/
│ │
│ ├── packageA/
│ │ └── moduleA.py
│ │
│ ├── packageB/
│ │ └── moduleB.py
│ │
│ └── __init__.py
│
└── main.py
假设我在mypackage包的__init__.py使用导入语句:
from packageA import moduleA
紧接着,在main.py中使用导入语句:
import mypackage
那么将会报错:ModuleNotFoundError: No module named 'packageA'
那么,为什么会Python解释器会找不到packageA呢?理由很简单,因为使用了绝对导入,Python解释器会按照(二)中提到的顺序:内置模块->sys.path查找这个模块的位置,然而这些路径中根本找不到packageA这个文件夹啊!
如果你一定要在__init__.py使用绝对导入, 应该写成:
from mypackage.packageA import moduleA
但是,这种写法意味着,你要保证mypackage这个包的名字不会发生变化,这并不优雅!
相反,使用相对导入:
from .packageA import moduleA
在Python运行时,自然可以根据__package__这个变量将这个语句变成上述的绝对导入语句。而且这么写更加简洁明了,何乐而不为呢?
四、其他问题解答
问题一:
内置模块是 Python 解释器直接提供的模块,它们是编译到 Python 解释器中的,不需要从文件系统加载。可是我在Python安装目录中看到了os.py文件。如果说os模块已经嵌入到了Python解释器中,是不是意味着哪怕我删掉这个os.py文件,Python解释器依然可以正常导入os模块呢?
解答:
- 内置模块的特性 内置模块(如 os、sys、math 等)是直接编译到 Python 解释器中的。这意味着它们的实现是用 C 语言(或其他底层语言)编写的,并且在 Python 解释器启动时就已经加载到内存中。因此,这些模块的加载不依赖于文件系统。
- 为什么 Python 安装目录中有 os.py 文件? 尽管 os 是一个内置模块,但 Python 安装目录中确实存在一个 os.py 文件。这是因为: os.py 是一个包装器:os 模块的核心功能是内置的,但 os.py 文件是一个 Python 编写的包装器,它提供了额外的功能和接口,使得 os 模块更加易于使用。 跨平台兼容性:os 模块的行为在不同操作系统(如 Windows、Linux 和 macOS)上可能有所不同。os.py 文件中包含了针对不同操作系统的特定实现和接口,使得 os 模块在不同平台上都能正常工作。
- 即使删除 os.py 文件,os 模块是否仍然可用? 答案是:是的,os 模块仍然可以正常导入和使用,但可能会丢失一些高级功能。 核心功能:os 模块的核心功能是内置的,即使删除了 os.py 文件,这些核心功能仍然可用。 高级功能:os.py 文件中包含了一些额外的实现和接口,这些功能可能会丢失。例如,某些跨平台的特性或高级功能可能会失效。
问题二:
是不是所有相对导入语句都可以写成绝对导入语句?
解答:
- 只要你的脚本文件是正常运行到package之外的,哪怕你把package中所有module的相对导入语句写死成绝对导入语句,也是不会报错的。因此,只要你正确的运行脚本,所有相对导入语句都可以写成绝对导入语句而不影响实际运行。
浙公网安备 33010602011771号