目录
前言
上一次系统性学习python还是2017年,当时主要是觉得应该学点什么,但是没有很明确的目标性,跟着廖大神的博客边学边练,一直到看到手写web服务那块,因为没有实际的项目练手,当时学了一段时间,就扔掉了。最近半年因为一些其他原因,又重新将python拾了起来,有具体的业务问题需要解决,开发中遇到的问题就比较多。 今天主要记录下关于import模块或者包的时候遇到的问题,通过对比java中的import来记录下我自己的一些个人理解。
需求及问题
Python版本:3.7
开发工具:Pycharm
核心诉求:
python和java有些类似,但是比java灵活的多,很多时候可能想做单元测试,就直接在某个model中写个main方法直接去运行然后验证,但整个工程的实际入口可能是最外层的某个model。主入口不同,model的默认目录地址不同,在子目录进行import的时候及单元测试的时候,总是会遇到各种modelError的问题,关于import使用的过程中,最常见遇到的报错信息如下三种:
(1)找不到模块
ModuleNotFoundError: No module named 'xxx'
遇到这种情况一般有两种情况,要么是写代码的时候手误,写错了名字。要么就是sys.path中没有该model对应的路径。第一种简单,修改名字即可。如果是第二种情况,那为什么会找不到model,为什么model对应的路径没有在os.path呢?造成这个问题的原因主要还在于入口model所在的路径。main方法对应的model的路径就是默认路径,如果涉及子目录,子包,在子包中又有import,这种情况很容出现,尤其在pycharm中代码不会报错,但是因为启动的main方法对应model所在目录不同,级联import的时候很容易出现该问题。主要原因还是因为model的路径没有再sys.path中。解决方法我们后面再摆。
(2)尝试在没有已知父包的情况下进行相对导入
ImportError: attempted relative import with no known parent package
根据这个错误描述其实不难发现,出现这个问题,肯定是使用了相对路径,比如 .xx或者..xx的方式去引入包了,但是最上层或者某一层并不是python的package包,所以会hi有这个问题。
(3)试图在顶级包之外进行相对导入
ValueError: attempted relative import beyond top-level package
出现这个问题,肯定也是使用了.或者..进行相对路径import了,但是因为main方法执行的model所在路径不同,会出现这样的问题。
本篇我会记录和描述以下几种情况的引用:
(1)同级package内的model进行import和调用,并支持直接main方法执行
(2)import同级package下的model,该model又import了和它自己同级的其他model
(3)main方法在第一层的model,import的model有多层级,深层import场景
我们先来看一个工程的包目录结构,如下图:
这是一个很简单的demo工程,str1、st2、st3、stduy四个package下对应的model都非常简单,func都是简单的print用于验证调用关系。
需求描述
(1)main.py中调用f1.py,f1.py会需要调用f2.py、py2.py、py3.py
(2)不管在哪一级,都需要支持随时随地的main方法的测试和执行
(3)在pycharm中不需要修改代码的情况下,任何层级作为main入口都可以正常运行
解决方案
想要从根本上解决各种import的问题,重点还是需要搞清楚问题的主要原因。 不管是上面提到的那种问题,最直接的原因主要还是model对应的目录没有再sys.path中,python找不到对应的model。因为python执行的时候,默认的model对应的目录除了系统变量层面的,工程层面的目录就是入口的model对应的目录,而子目录是不会被加载进去的。 所以子目录中如果没有使用相对路径引入,就会找不到对应的model。但是如果使用相对路径引入,就没办法在开发的过程中直接运行main方法在各个子model进行单元测试,因为import的相对路径不对,大概率会出现上面提到后面两种问题。
关于这些问题,有两种解决思路,如下,
(1)通过sys.path.append和os.path在对应子model中将其对应的路径添加到sys.path中,在import的时候,可以直接import,而不用使用相对路径。(注意:这种方法如果在pycharm中开发,可能会遇到编译器报错,但是运行没错的现象,主要是编译器会对工程设置默认的根目录,它预校验的时候是按照设定的根目录去校验的,所以可能会报找不到model,但是运行又没事)。
比如在main.py中调用f1,f1中调用py2,py2调用py3,代码如下:
#main.py
import study.f1 as f1
if __name__ == '__main__':
f1.showtest()
#f1.py
from study import f2
from st2 import py2
def showtest():
f2.sec()
py2.test2()
#py2.py
from st3 import py3
def test2():
print('in st2.py2')
py3.test3()
#py3.py
def test3():
print('in st2.str3.py3')
按照如上方式写,在pycharm中预校验不报错,但是运行的时候,会报第一个错误,如下:
主要原因是因为项目目录默认到pyStudy目录,所以study和st2目录下的model都没问题,但是st2/st3的目录不存在,所以报py3对应的st3这个module不存在。解决方法是尝试将st3目录加到sys.path里,修改py2.py的代码,如下:
#py2.py
import os
import sys
print(os.getcwd())
sys.path.append(os.path.join(os.getcwd(), 'st2'))
from st3 import py3
def test2():
print('in st2.py2')
py3.test3()
再重新到main方法中执行,可以了。
(2)上面的方案其实就是将对应model添加到对应的sys.path中解决的。但是如果按照如上代码,在f1.py中直接运行的话,还是会发现有问题,因为默认的model路径变成了pyStudy/study了,st2/st3的目录没有,并且通过os.getcwd()获取的路径还是到pyStudy/study,所以想通过join方式拼接也比较麻烦。 这也不符合在任意py文件中运行进行单元测试的预期。
在java中import包路径的时候时候是import的全路径,例如:
java.打头的是jdk相关的,类似系统变量的,对应python中的类似 os/sys这样的模块。com.打头的是当前工程的包目录,可以看到java中import类的时候时候走的全路径。 基于这个思路,python中是否也可以呢?
我们先查看下当前工程的根目录是什么,File--->Settings--->Project:pyStudy--->Project Structure:
可以看到根目录是pyStudy,那就意味str1/str2/study这三个package下的model是可以被直接引用到的,但是str2/str3下的不行,我们尝试修改py2.py代码如下:
from st2.st3 import py3
def test2():
print('in st2.py2')
py3.test3()
直接运行main.py和f1.py都没问题。
个人理解
两种方案,第二种肯定是最优的解决方案,也就是说,在import的时候,直接从根目录下的package包名开始写,这样不需要考虑相对引用,也不需要考虑听过sys.path维护解决问题。比较彻底的解决了诉求。