上个月第一次用Python做比较大的开发,一开始函数少还比较顺利,后期体积上来了各种包/模块调用报错,改起来真的头大,才注意到Python模块/包调用原来没有想象中的那么方便,还是有很多坑的。

于是乎,在阅读了一些文章、做了一些测试以后,有了这篇文章,梳理了Python找包的逻辑以及列举了一些容易遇到的问题,希望你看完以后再也不会被它恶心到了。

1 找包/模块原理

1.1 模块搜索路径

我们在开发项目时,在使用 pycharm 集成好的环境下,直接点击按钮运行代码,一般不会遇到自己的包找不到的错误。但是当我们在使用命令行运行 python 代码,或者在 vscode 中运行一个代码文件时(在项目的根路径下运行python xxx),可能会报错找不到某个模块。

ModuleNotFoundError: No module named 'xxxx'

首先我们要弄清楚,在 python 中使用下面的代码调用包时,python是如何找到这些包的?

import os,sys
import xxx

python是通过模块搜索找到这些包的,关键在于模块搜索的路径!

当一个名为xxx 的模块被导入的时候,解释器会在下面的路径里搜索:

  1. 内置模块

  2. 包含输入脚本的目录(或者未指定文件时的当前目录)

  3. PYTHONPATH(一个包含目录列表,它和shell变量有一样的语法)

  4. pip安装的第三方库

  5. ......

上面所有的搜索路径都会初始化在sys.path中

1.2 与自定义包找不到的关系

了解了 python 模块搜索的过程,我们再解决自己的包找不到的问题,思路就很清楚了。只要将自己写的包添加在 sys.path 中就行了。

1.3 模块和包的定义

  • 模块:python中每个.py文件称为一个模块

  • 包:每个具有__init__.py文件的文件夹被称为包

在 Python 3 中,模块(module)和包(package)之间的关系与 Python 2 基本保持一致,但也有一些重要的更新和增强:

1. 命名空间包(Namespace Packages)

  • Python 3.3 引入了命名空间包,这是对包机制的一个重要增强。在 Python 2 中,包必须包含一个 __init__.py 文件才能被识别为包。而在 Python 3.3 及之后,包可以没有 __init__.py 文件。这样的包被称为“命名空间包”。

  • 命名空间包的主要目的是允许多个目录中的模块共享相同的命名空间。例如,多个不同的目录下都可以有一个 foo 包,Python 会将它们的内容合并在一起。这在大型项目或插件系统中特别有用。

2. 包的相对导入

  • 相对导入 在 Python 3 中得到了更好的支持和推广。在 Python 3 中,相对导入可以通过使用点号来表示。例如,from . import module_a 表示从当前包导入 module_a 模块。这个很重要后面案例会说!

  • Python 3 强制要求使用明确的相对导入或绝对导入,而不再允许 Python 2 中的隐式相对导入。这使得代码更清晰、可维护。

2 常见调用案例

2.1 同级模块的导入

main.pytest.py都在blog_test目录下。在main.py中直接引用test.py后得到的输出如下:

当然一个包内的同级模块导入也一样。

2.2 同级目录下模块的导入

main.py调用pakege2目录下的pakege2_test1.py模块的pakege2_test1()函数:

注意是目录不是包

虽然在python3.3之后包可以没有__init__.py 文件,但还是有一点区别的。

下面的几个都是正确的调用:

错误的调用:

即使这个目录能被自动识别为包,但是因为没有__init__文件,系统没法知道这个包中有哪些导出的模块,所以直接.会报错。

在上面加了一行,即使没有真正使用第一行的调用,但是他告诉了系统pakage2中有这个模块,所以执行也不会报错,这印证了我们的错误解释。

2.3 同级目录下包的导入

main.py调用pakege1包的pakege1_test1.py模块的pakege1_test1()函数:

首先需要在pakage1下面创建__init__.py文件,并在里面将包中的模块导入。

这样mian函数调用的时候,就知道你这个包中的模块有哪些,下面这个方式就可以执行成功了(当然原来可以的那两个方法现在也可以):

如果__init__.py中没有写pakage1.pakage1_test1,那同样也还是不行。

这样的方式也可以一次性导入pakage1包中的所有模块(__init__中提到的):

⚠️注意__init__.pyimport的模块,在包被调用时会执行模块中的代码!

就算是from pakage1 import pakage1_test1 特意只引入test1,test2还是会执行。

总结起来就一句话,只要写进__init__中的,一切跟这个包相关的操作都会导致里面的模块全部执行。

2.4 跨级目录下包的导入

问题描述

跨级目录的导入是最麻烦的

我们在这里想在pakage2/pakage_b.py中导入pakage1/pakage_a.py,但是可以看出得到报错,找不到pakage1这个包。

在这里将sys.path打印出来,发现第一个路径是pakage2这个包的绝对路径。python在路径搜索的时候就会在pakage2下面搜索。自然是找不到pakage1这个包的。

同样的,这时如果我们在pakage_b.py中导入main.py也是报错。

注意:这个问题如果是使用pycharm中的run直接运行的,则不会出现bug,因为pychram添加了在sys.path中添加了一个路径:D:\JetBrains\PyCharm 2020.1.1\plugins\python\helpers\pycharm_matplotlib_backend。但是实际部署项目我们是通过命令行/终端,这个问题就会出现。

解决办法

我们只需要将pakage1这个包的路径添加到sys.path中就行了,其实我们也可以把项目blog_test的路径添加到sys.path中来解决这个问题,因为会递归查找包。

可见添加的一行代码是,网上一般用绝对路径,这里写成相对路径,更好些。

sys.path.append(os.path.split(sys.path[0])[0])
# 代码解释
# sys.path[0]                   : 得到C:\Users\maxu\Desktop\blog_test\pakage2
# os.path.split(sys.path[0])    : 得到['C:\Users\maxu\Desktop\blog_test',pakage2']

2.5 A调B,B再调C

对于上述报错的代码,我们修改一点,在main.py中导入pakage2/pakage_b.py这个模块,然后使用python main.py运行该模块,会发现不报错。

解释:python是将运行的这个.py文件父文件夹的路径添加到sys.path中!

注意!只会将A放进sys.path中,被调用的路径不会放入。

这会导致下面的代码报错:

如果我们test2中按照常规同级模块的调用思路这么写:

在单独执行test2文件时,没有问题,因为当前sys.pathpakage1

问题描述

但是当main调用test2,test2再调用test1时,就会报错:

我们前面提到过:

所以当前sys.path中仅有main添加的目录,在这个路径下当然没有test1了:

问题解决

这就要请出前面提到的相对导入

我们将test2中的代码修改为:

2.6 相对导入..用法

运行test1,会发生报错。这是因为..仅仅是针对包套包,里面的包调用外面的包设计的,所以遇到这种需求还是老老实实使用2.4的添加路径的方法实现吧。

相对导入中的 .. 确实有其独特的用途,当你在一个包的子包中需要访问同一父包或父包下的其他子包中的模块时,相对导入非常有用。假设项目结构如下:

my_project/
├── package_a/
│   ├── __init__.py
│   ├── module_a.py
│   └── subpackage_a1/
│       ├── __init__.py
│       └── module_a1.py
└── main.py

如果你在 module_a1.py 中需要访问 module_a.py,你可以使用以下相对导入:

from .. import module_a
文章部分参考:https://blog.csdn.net/MaXumr/article/details/109640529