python的相对导入和绝对导入-小白教程(ModuleNotFoundError & attempted relative import beyond top-level package)

我的个人网站:https://www.gentlecp.com

前言

在运行python项目的时候,你是否遇到过这样的问题(ModuleNotFoundError):
在这里插入图片描述
又或者是这样的问题(attempted relative import beyond top-level package):
在这里插入图片描述
其实这些问题都涉及到了python的导入机制,关于第一类问题,可以阅读我的这篇博客-python的import机制当然我建议你看完import机制之后再回过头将本篇文章完整看完,相信我,你会有不一样的收获。

实际案例

为了能更直观地理解,我们创建一个Import的python项目,并在其中创建目录结构如下:

Import
	|--A
		|--D
			|--d.py
		|--E
			|--e.py
		|--a.py
	|--B
		|--b.py
	|--C
		|--c.py
	|--main.py  # 项目入口

其中A,B,C,D均为目录,a.py, b.py, c.py, d.py 都是一个简单python文件,每个文件中有一个对应的hello函数,用于打印对应的hello信息,例如a.py 中:
在这里插入图片描述
其他的就是将a换成对应字母。
我们从实际项目开发出发,一般项目开发中有多级目录,我们已经实现了,在项目本身根目录下会有一个入口文件(main.py),他用于调用其他模块的文件达到执行整个项目的目的。

项目根目录下运行

我们在main.py中写入如下代码,并运行查看结果(为了体现ide的特殊之处,后面的运行结果如果不一样,我会展示ide和终端运行两种结果,原因你在看过python的import机制就会明白):

"""
项目的入口,用于调用其他模块,运行整个项目
"""
from A import a
from B import b
from C import c

if __name__ == '__main__':
    a.hello_a()
    b.hello_b()
    c.hello_c()
  • pycharm运行
    在这里插入图片描述

可以发现这样导入运行都是没问题的。

但是这是最简单的情况,实际开发中各个模块之间会相互调用,并不仅仅只是main去调用其他人,因此我们在b.py中修改代码如下:

from A import a

def hello_b():
    print("hello, I'm b!")
    a.hello_a()

在d.py中修改代码如下:

from C import c

def hello_d():
    print("hello, I'm d!")
    c.hello_c()

修改main.py如下:

from A import a
from B import b
from C import c
from A.D import d

if __name__ == '__main__':
    a.hello_a()
    b.hello_b()
    c.hello_c()
    d.hello_d()

再运行main.py查看结果:

  • pycharm运行
    在这里插入图片描述

可以发现还是没有问题的。

下面我们将绝对导入改成相对导入的形式看看结果(为了简便,这里仅对d.py进行修改):

from ..E import e

def hello_d():
    print("hello, I'm d!")
    e.hello_e()
  • pycham运行

截至目前,我们可以得出一个结论,只要从main.py入口运行,其他模块的内部导入符合python导入规则,无论相对导入还是绝对导入都是不会发生ModuleNotFoundError & attempted relative import beyond top-level package这两种问题的

那么我们的问题又是怎么产生的呢?别急,之前我是在d.py中修改相对导入,下面我在b.py中做相同的操作,如下:
在这里插入图片描述
注意我这里用图片而不是代码是想告诉你,pycharm对这行代码没有显示红色波浪线错误,证明语法上是没问题的。

  • pycharm运行
    在这里插入图片描述

可以看到同样的操作在b上就出了问题, 提示attempted relative import beyond top-level package。

这里直接给出解释:python默认将运行文件作为顶级,其子目录中的文件仅能导入比执行文件低级或同级的目录及文件。例如以main.py运行,那么Import就是顶级包,A,B,C均为其同级包,..A首先想要访问Import,再从Import中导入A,这是不被允许的。所以就报了这个错误。而对d.py而言,..E首先想要访问A,再从A中导入E,这是可以的,因为A是main.py的同级目录,允许被访问。所以b.py想要导入A,必须直接用from A import ...的形式。

子目录下运行

前面一直采用main.py导入其他模块的包调用运行的形式,实际开发过程也是这样,那么我们如果遇到想要单独运行某个单一文件,或者仅执行某个子目录下内容进行部分测试怎么办呢?

一个简单的解决方法是在main.py的同级目录新建一个test.py用相同的方式导入运行对应模块。但有的人就是喜欢直接执行对应文件来查看输出呢?显然这种方式也更佳直白,简单。

还是以d.py为例,修改d.py内容让其进行单独运行测试:
在这里插入图片描述

  • pycharm运行
    在这里插入图片描述

咦?原来好好的,单独运行d怎么报错了?(注意pycharm仍然没有红波浪线提示)还记得之前说的python会把当前运行文件的目录作为顶端包吗?此时用d.py运行,那么顶端包就是D,..E试图访问A自然超出了范围。

那怎么办,我在d中该如何访问到E?自然是借助A,用相对访问的方式访问不到A,我们改成绝对导入

from A.E import e

def hello_d():
    print("hello, I'm d!")
    e.hello_e()


if __name__ == '__main__':
    hello_d()

在这里插入图片描述
成功了?先别急着高兴,这里还有一个隐患(这个隐患跟python的import机制一文内容有关,你能猜到吗?),我们先扣着在后面继续说。

经过上面的测试我们总结出了以下两点:

    1. 用项目根目录入口文件执行,无论是相对导入,还是绝对导入都没有ModuleNotFoundError & attempted relative import beyond top-level package两种问题
    1. 相对导入会将执行文件对应目录设置为top-level package,所以在单独运行使用了相对导入的python文件时,会导致attempted relative import beyond top-level package问题。

那么是不是就用绝对导入就比相对导入要好呢?不一定,这个问题与前面说的隐患有联系。我们在终端直接运行d.py查看结果:
在这里插入图片描述
你会发现程序找不到A了,这个原因在python的import机制里解释过。pycharm会将项目根目录自动添加到sys.path中,所以在pycharm中运行的时候,Import是被加到sys.path中的,自然可以通过Import找到A,但是在终端运行的时候就不会添加,导致A无法被找到。

所以你发现了吗?如果我们只采用绝对导入的方式,一旦有人将我们的项目作为子项目添加到他的项目中,就会发现模块找不到的问题,因为pycharm自动添加的是他的项目的目录,而不是我们项目的目录,原本直接A.E可以访问的,需要X.Import.A.E才能访问(X是他的项目目录名)。

总结

OK,说了那么多,你肯定会问我,那我到底什么时候用相对导入,什么时候用绝对导入的方式才好啊?我罗列了几种情况如下,你可以对照参考:

仅供参考,实际开发请依据需求设计

  • 1. 该项目是一个独立项目,不会合并到其他项目内容当中 ,且该项目体量较小
    我说的体量,指的是项目的分级结构深度。例如Import->A->D仅有3级结构,属于体谅较小。采用绝对导入的方式,即导入模块的时候从根目录算起,导入都是Demo.A.B 的形式,这种方式的好处是简洁明了,随便拿出单独一个文件你都知道它从哪里导入了什么,并且这样允许你直接对单个文件或子模块进行运行测试。不会出现attempted relative import beyond top-level package。

  • 2. 该项目是一个独立项目,不会合并到其他项目内容中,但是该项目体量较大
    体量较大的项目如果你仍全部用绝对导入的方式,也不是不行,但容易出现下面的情况,你要导入一个包from Demo.A.B.C.D.E import method,这种方式显得十分冗余且不美观。所以这时候我们采用绝对导入和相对导入结合的形式,如果要导入的包通过2层就能找到(Demo.A),那可以用绝对导入的形式,不然采用相对导入,例如Demo下的一个小demo目录下的我建议都可以采用相对导入的形式,就像例子中的d.py导入Efrom ..E import e。你可以通过在小demo目录下设置一个test.py对该demo下的内容进行测试,由于运行test.py会将该小demo的目录添加到sys.path中,所以test.py中可以采用绝对导入的形式导入要测试的包。

  • 3.该项目是一个子项目 ,未来要添加到其他项目中
    这种就没得说了,采用相对导入的形式,但是为了保证你自己单独测试项目的时候依然可以运行,你可以给你的项目多嵌套一层目录,假装它已经被包含到其他项目中了。

如果你还有任何疑惑,欢迎评论留言给我,我会及时给予反馈。

发布了46 篇原创文章 · 获赞 99 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/GentleCP/article/details/99624495