python import失败_Python import 报错问题

本文详细分析了Python import自定义模块时遇到的各种报错情况,包括顶级目录、次级目录导入,以及模块间相互导入的问题。通过案例介绍了Python 3.3后的隐式命名空间包,并解释了sys.path在导入过程中的作用。同时提醒开发者不要完全依赖pylint的检查,因为import错误可能与运行时的sys.path有关。最后提出了使用虚拟环境和.pth文件的解决方案,以提高项目的可移植性和避免导入问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近遇到 import python 自定义模块出现报错的问题,捣鼓了很久,终于算是比较搞清楚了,为了描述清楚,有测试项目目录如下:

a315e79282ce

项目结构

环境描述:

自定义模块 pkg_a,包含两个文件 file_a_01.py,file_a_02.py(以下测试模块的测试文件都分别有两个包含模块标识的文件,不一一赘述了)

自定义模块 pkg_c,属于 pkg_a 模块的子模块

自定义模块 pkg_b

入口文件目录 src,模拟常规的运行脚本 runner.py 以及一个仅包含一些字符串的配置文件 conf.py

顶级目录入口文件 test.py

以上每个文件运行都会打印出自己的文件名,方便根据输出判断导入成功与否

本项目使用了 python 3.7 的虚拟环境

经常出现的使用场景:

场景一:顶级目录入口文件导入

在 test.py 文件测试了以下代码:

# 导入配置文件 src/conf.py

import src.conf

# 导入包 pkg_a

import pkg_a.file_a_02

输出:

I'm conf.py

I'm pkg_a_02

分析:

src 里没有包含 init.py 为什么还可以导入成功?

原因是 python3.3 之后支持了隐式命名空间包,可以参考https://www.python.org/dev/peps/pep-0420/#specification

从顶级目录可以导入包,这里为什么不需要写 sys.path.append(${项目目录})?

test.py 处于项目的根目录,在运行此入口文件的时候,会自动把入口文件所在的文件夹目录添加到 sys.path 里面,有兴趣的同学可以在运行入口文件的时候输出 sys.path 看一下

场景二:次级目录入口文件导入

在 runner.py 文件测试了以下代码:

# 导入配置文件 conf.py

import conf

# 导入包 pkg_a

import pkg_a.file_a_02

输出:

I'm conf.py

Traceback (most recent call last):

File "src/runner.py", line 9, in

import pkg_a.file_a_02

ModuleNotFoundError: No module named 'pkg_a'

分析:

导入 pkg_a 为什么会报错?

import sys

for p in sys.path:

print(p)

"""

输出:

test-python-import/src

...

"""

通过 python xxx.py 运行的时候,会把 xxx.py 所在的目录添加到 sys.path 的 0 位,可以通过输出 sys.path 观察到

从上面的输出可以看出,运行 runner.py 的时候,仅把 runner.py 对应的目录添加到了 sys.path 里面,所以 python 去找 package 的时候出现了找不到的问题。这个时候可以通过 sys.path.append('${project_pth}/test-python-import/src') 来临时解决问题,但是这里又引入了另外一些很繁琐的问题,如果有 runner_01.py,runner_02.py,runner_0n.py ... 那么每次又要再写一遍

pylint 出现的错误提示产生的误导

如果这个目录是个命名空间目录,而不是包目录,那么 pylint 会出现如下图的报错,这个看起来也算正常,毕竟此时 sys.path 的确没有 pkg_a 的信息

a315e79282ce

pylint 报错提示

那么,这个时候模拟另外一种情况,往 src 里添加一个 init.py 文件,让这个目录变成包,如下图所示:

a315e79282ce

新的目录结构

诡异的事情出现了,现在的 pylint 提示变成了找不到 conf,但是却找得到 pkg_a

a315e79282ce

新的报错提示

再次运行一下 runner.py ,此时输出,结果还是不变的找不到 pkg_a:

I'm conf.py

Traceback (most recent call last):

File "src/runner.py", line 2, in

import pkg_a.file_a_02

ModuleNotFoundError: No module named 'pkg_a'

然后出现了一个会诡异的情况,我换了一个行,不报错了...我也是很醉

a315e79282ce

诡异换行之后正常

有兴趣的同学可以继续研究一下这个报错是啥问题,有一些方向:

"python.linting.pylintArgs": [

"--init-hook",

"import sys; sys.path.insert(0, './')"

]

总之,不要被 pylint 误导了,上面有情况出现 pylint 没有对 pkg_a 检查出导入报错,但是还是运行的时候找不到包,所以最好还是通过运行时判断,不要相信 pylint。后续谈到的 pth 解决方案也会解决这个 pylint 的问题(但梅开二度说一次还是请不要相信 pylint,时刻记得 import 只和 python 的搜索路径有关,手动狗头)

场景三:模块导入另外一个模块

在 pkg_a/file_a_01.py 测试了以下代码

from pkg_b import file_b_01

import file_a_02

print('I am pkg_a_01')

输出:

I'm pkg_a_02

Traceback (most recent call last):

File "pkg_a/file_a_01.py", line 2, in

from pkg_b import file_b_01

ModuleNotFoundError: No module named 'pkg_b'

分析:

导入报错

同样的问题,此时 python 在路径里找不到 pkg_b

相对导入问题

修改一下 pkg_a/file_a_01.py 的代码如下:

from . import file_a_02

from pkg_b import file_b_01

print('I am pkg_a_01')

输出:

Traceback (most recent call last):

File "pkg_a/file_a_01.py", line 1, in

from . import file_a_02

ImportError: cannot import name 'file_a_02' from '__main__' (pkg_a/file_a_0

1.py)

这个问题出现的 __main__ 是不是很眼熟,就是我们经常会写一个代码

if __name__ == '__main__':

pass

这个 __name__ 其实就是命名空间,当 file_a_01.py 自运行的时候,__name__ 会被设置为 __main__,所以这个相对导入会出现错误。但是如果这样写,被别的文件引用的时候就不会报错(前提是能找到 pkg_a),试试在顶级目录入口文件 test.py 导入 pkg_a/file_a_01.py

import src.conf

import pkg_a.file_a_01

输出

I'm conf.py

I'm pkg_a_02

I am pkg_b_01

I am pkg_a_01

这里也牵涉了一个平常不太注意的问题,当我们写一个包的时候,应该怎么导入该包内其他的文件呢?有兴趣的同学可以参考一下著名的包 requests 的导入写法,里面用的全部都是相对导入。当然也有另外一种写法,就是 import pkg_name.xxx,类比到上面的例子就是:

# 相对导入

from . import file_a_02

# 带包名的写法

from pkg_a import file_a_02

通过这些例子可以加强对导入的理解,本质上还是理解路径搜索

场景四:次级模块导入另外一个模块

在 pkg_c/file_c_01.py 测试了以下代码

import pkg_b

print('I am pkg_c_01')

输出

Traceback (most recent call last):

File "pkg_a/pkg_c/file_c_01.py", line 1, in

import pkg_b

ModuleNotFoundError: No module named 'pkg_b'

分析

导入报错,都读到这的同学,估计都已经非常清楚导入报错问题了,子模块也是同理没什么特殊的地方

这里讨论一个常见的使用场景,次级模块导入上级模块的文件,把 pkg_c/file_c_01.py 代码修改为:

from .. import file_a_02

print('I am pkg_c_01')

输出:

Traceback (most recent call last):

File "pkg_a/pkg_c/file_c_01.py", line 1, in

from .. import file_a_02

ValueError: attempted relative import beyond top-level package

再结合两个例子一起说明问题

在 test.py 进行调用

import src.conf

import pkg_a.pkg_c.file_c_01

输出

I'm conf.py

I'm pkg_a_02

I'm pkg_c_01

修改一下文件内容

pkg_a/pkg_c/file_c_01.py

print(__name__)

from ...pkg_b import file_b_01

print("I'm pkg_c_01")

test.py 保持例子 1 不变,仍然导入 file_c_01.py

import src.conf

import pkg_a.pkg_c.file_c_01

输出:

I'm conf.py

pkg_a.pkg_c.file_c_01

Traceback (most recent call last):

File "test.py", line 7, in

import pkg_a.pkg_c.file_c_01

File "/xxx/xxx/test-python-import/pkg_a/pkg_c/file

_c_01.py", line 2, in

from ...pkg_b import file_b_01

ValueError: attempted relative import beyond top-level package

分析:

为什么会出现 attempted relative import beyond top-level package 这个报错?

通过 .. 或者 ... 这种相对查找父级目录的导入,是相对该文件的目录(命名空间)去找的

参考例子 2 的情况:

file_c_01 的 __name__ 输出是 pkg_a.pkg_c.file_c_01

此时如果通过 ... 这个去找的时候会超出 pkg_a 的范围,因为 .. 就已经是顶级包名 pkg_a 了,所以会报错

参考一开始那个自运行的例子:

__name__ 是 __main__,所以再通过 .. 去找父目录的时候,就会报错

解决方案

上述通过几个实际的例子说明了一些常见的报错原因,下面来讨论一下解决办法。

先来看一下目前现网给出的比较多的解决办法,具体怎么做就不展开了,一搜一大堆:

最原始的,环境变量添加项目根目录进去

这种方法比方法 2 好的就是不用写那么多冗余代码,但是可移植性太差了,不推荐

每次使用之前需要添加包的绝对路径进去 sys.path.append('${absolute_path}')

这种方法应该是比较多人使用的了,但是每次都要写这堆代码到文件头上,太冗余了

很容易被 IDE 格式化,比较不友好

会欺骗 pylint 的检查,梅开三度再吐槽一下 pylint

本文推荐的解决方案是虚拟环境 + pth 文件解决导入问题

首先来说一下使用虚拟环境的好处(可能还有些同学没接触,就啰嗦一下)

将项目环境与系统环境隔离,这样安装包的时候不会与系统环境的包发生冲突。尤其是在公用的机器上,系统环境的冲突往往引发一堆没必要的问题

python cli ,如果系统存在 python 和 python3 的时候,每次运行脚本都要 python3 xxx.py,很不利于拼写的补全(因为每次都是先出来 python 要自己补充一个 3)。如果更改 python 指向了 python3,又可能引发很多系统的问题,因为系统很多地方还是用的 python2。

以 python 3.7 为例子,创建好虚拟环境之后,在 venv/lib/python3.7/site-packages 下添加一个文件 myproject.pth (名字随便都可以),然后添加项目的绝对路径:

${绝对路径}/test-python-import

添加完之后,无论在项目的哪个目录运行脚本,python 都能搜到我们的根目录,这样我们导入包的时候,就可以直接通过包名导入了。例如,在 src/runner.py 这种二级目录不做额外处理是找不到根目录下 pkg_a 这种包名,在添加完路径之后,就可以搜得到。

所以理解好 import 本质还是要理解好 python 的路径搜索问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值