非工作路径、非入口路径如何导入Python自定义模块

原文链接: http://www.juzicode.com/python-note-import-self-define-module/

老规矩,先抛问题:

有2个py文件,分别是在当前工作路径下的xx\modxx.py和yy\modyy.py,如果要在modxx.py中导入modyy模块,该怎么导入模块?

文件结构是下面这样子的:

import_dir\  #当前工作目录
-----\xx\
     -----modxx.py
-----\yy\
     -----modyy.py

modyy.py中的内容是这样的:

def func_add(a,b):
    return a+b
def func_multi(a,b):
    return a*b

在modxx.py中要调用modyy.py中的func_add()和func_multi(),modxx.py的主体内容是这样的:

print('juzicode.com/vx:桔子code')
x = modyy.func_add(11,55)
y = modyy.func_multi(11,55)
print(x,y)

通过《好冷的Python~ 那些同名的家伙们(Python作用域)一文我们知道Python解释器导入模块首先搜索内建模块,再搜索sys.path变量指定的模块,而sys.path初始化时首先包含的是当前被执行脚本所在的目录,后面才是Python安装目录下的库文件目录。因为modyy.py不符合前述路径的要求,所以直接用import modyy肯定是行不通的。

要解释导入模块查找路径,先了解下sys.path是什么?Python文档的解释如下:

可以看到sys.path是一个list,包含了模块导入搜索的路径,并根据环境变量PYTHONPATH完成初始化,其中sys.path[0]是要执行脚本的路径,也就是入口py文件的路径。另外程序是可以修改sys.path值的,这样就为我们修改导入路径提供了方法。

为了获取直观感受先看下sys.path是什么内容,将下面的代码保存在一个文件中,运行该py文件,:

#保存在一个文件中,再执行该文件
import sys
print(sys.path)

运行结果:

E:\juzicode\code-py\pynote\import-dir\xx>python modxx.py
['E:\juzicode\code-py\pynote\import-dir\xx', 'D:\Python\Python38\python38.zip', 'D:\Python\Python38\DLLs', 'D:\Python\Python38\lib', 'D:\Python\Python38', 'D:\Python\Python38\lib\site-packages']

sys.path的第0个元素就是该py文件的路径,后面的元素大部分都是以Python安装路径D:\\Python\\Python38\\为前缀的一系列路径。

现在知道了模块的搜索路径都保存在sys.path中,我们就可以修改sys.path的值扩展导入路径。在modxx.py中看到modyy.py的相对路径就是..\\yy\\modyy.py,所以可以这样添加路径:

sys.path.append('..\\yy\\')

修改modxx.py文件:

import sys
sys.path.append('..\\yy\\') #添加搜索路径
print('sys.path[0]',sys.path[0])
print('sys.path[-1]',sys.path[-1])
import modyy #有了前面添加路径后再import modyy模块
print('juzicode.com/vx:桔子code')
x = modyy.func_add(11,55)
y = modyy.func_multi(11,55)
print(x,y)

运行modxx.py,结果是正常的:

但是当切换工作路径到上一层路径后,出现错误了,提示找不到模块modyy:

这是因为sys.path.append()添加的是相对路径’..\\yy\\’,非常依赖当前工作路径,当切换到上一层目录后,再用’..\\yy\\’表示相对路径就发生错误了,说明该方式灵活性不高,需要加以改造。从前面2次不同工作路径运行打印出来的sys.path[0]的值来看,sys.path[0]都是确定的modxx.py的绝对路径,而从最开始讨论的文件结构已知modyy.py和modxx.py的相对路径是确定的,所以就可以通过相对路径计算出绝对路径为sys.path[0]+’\\’+’..\\yy\\’:

add_path = sys.path[0]+'\\'+'..\\yy\\' #注意sys.path[0]最后没有分隔符,所以需要添加一个\\
sys.path.append(add_path)

经改造后的modxx.py的完整代码:

import sys
add_path = sys.path[0]+'\\'+'..\\yy\\'
sys.path.append(add_path)
print('sys.path[0]',sys.path[0])
print('sys.path[-1]',sys.path[-1])
import modyy
print('juzicode.com/vx:桔子code')
x = modyy.func_add(11,55)
y = modyy.func_multi(11,55)
print(x,y)

经改造后上面这段代码在不同工作路径下运行结果都是ok的:

到这一步看起来能完美解决不同工作路径的问题,我们再稍微做些变动,如果文件结构是这样的:

import_dir\  #当前工作目录
-----main.py 
-----\xx\
     -----modxx.py
-----\yy\
     -----modyy.py

其中modyy.py文件内容不变。

modxx.py的内容改为定义了一个func_add_multi()函数,用到了modyy里的func_add()和func_multi()函数,并且被main.py调用:

import sys
add_path = sys.path[0]+'\\'+'..\\yy\\'
sys.path.append(add_path)
print('modxx,sys.path[0]',sys.path[0])
print('modxx,sys.path[-1]',sys.path[-1])
import modyy

#增加了个函数供main.py调用
def func_add_multi(a,b):
    return modyy.func_add(a,b),modyy.func_multi(a,b)
 

main.py的内容是这样的:

import sys
add_path = sys.path[0]+'\\'+'xx\\'
sys.path.append(add_path)
print('main,sys.path[0]',sys.path[0])
print('main,sys.path[-1]',sys.path[-1])
import modxx
print('modxx.func_add_multi(11,55)=',modxx.func_add_multi(11,55))

运行main.py,提示找不到modyy:

因为执行的是main.py,所以sys.path[0]的值已经变为main.py所在的路径,即使进入到modxx模块中sys.path[0]也是如此,所以再用这个路径和’..\\yy\\’组合后的路径已经不再是modyy.py的路径了。

问题就在于新增的导入路径依赖入口py文件,如果入口py文件的路径发生变化,导入路径就失效了。要解决这个问题就得想办法避免使用入口文件路径,最好是使用导入文件本身的路径。办法当然是有的,就是使用__file__变量,在《好冷的Python–if __name__==’__main__’是啥东东》中我们介绍过__file__变量,表示的就是当前文件的路径。用os.path.abspath(__file__)获取py文件本身的绝对路径,再用os.path.split(os.path.abspath(__file__))[0]分离出不包含文件名称的路径部分,所以在modxx.py中可以使用下面的方式添加路径:

import sys,os
add_path = os.path.split(os.path.abspath(__file__))[0]+'\\'+'..\\yy\\'
sys.path.append(add_path)

修改后的modxx.py就是这样的:

import sys,os
#add_path = sys.path[0]+'\\'+'..\\yy\\'
add_path = os.path.split(os.path.abspath(__file__))[0]+'\\'+'..\\yy\\'
sys.path.append(add_path)
print('modxx,sys.path[0]',sys.path[0])
print('modxx,sys.path[-1]',sys.path[-1])
import modyy
print('导入modyy ok')
#增加了个函数供main.py调用
def func_add_multi(a,b):
    return modyy.func_add(a,b),modyy.func_multi(a,b)

main.py虽然也可以用原来的方法,但是为了统一,我们也一起修改:

import sys,os
#add_path = sys.path[0]+'\\'+'xx\\'
add_path = os.path.split(os.path.abspath(__file__))[0]+'\\'+'xx\\'
sys.path.append(add_path)
print('main,sys.path[0]',sys.path[0])
print('main,sys.path[-1]',sys.path[-1])
import modxx
print('modxx.func_add_multi(11,55)=',modxx.func_add_multi(11,55))

这个时候不管是main.py还是modxx.py作为入口文件,运行都是正常的:

【tips】:

1、如果是直接启动Python解释器,看到的sys.path会稍有差异,它的下标为0的元素是一个空字符串:

>>> import sys
>>> print(sys.path)
['', 'D:\\Python\\Python38\\python38.zip', 'D:\\Python\\Python38\\DLLs', 'D:\\Python\\Python38\\lib', 'D:\\Python\\Python38', 

扩展内容:

  1.  好冷的Python~ 那些同名的家伙们(Python作用域)
  2.  

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注