Python相对路径导入模块时需要注意的问题

在最近的工程中,把一些公用的代码集中到一些独立文件夹中,这样其他工程可以在不同的路径下,通过相对路径导入这些模块。

之所以用相对路径,是因为不想使用 setup.py 等方式将这些某块注册到空间以免污染命名空间,同时也不具备可迁移性。

由于整个项目中的文件夹相对路径是比较固定的,所以无论整体拷贝到任何地方(甚至不同系统),都能正常工作。

但是,python中用相对路径导入模块,还是存在一些需要注意的问题。

先简单介绍下相对路径导入需要了解的基础知识:

当前项目的搜索路径,都包含在 sys.path 当中,这是一个列表,并且可以通过 sys.path.append 的方式向内添加搜索空间。

当代码中使用 import / from ... import 等方式导入模块的时候,python 就按照列表中的顺序寻找名称匹配的模块。

先看一个简单例子:

目录结构:

|

-- test/a.py

|

-- test2/b.py

|

a.py 代码如下:

import sys
sys.path.append('../test2')
import b

if __name__ == '__main__':
    print sys.path
    print dir()

b.py 代码如下:

import sys
import os
print 'from b: %s' % (os.path.realpath(__file__))

其中 a.py 为程序入口,并且以 a.py 所在的目录 test 作为程序运行目录, 运行可以看到:

from b: c:\Users\zhoudongbin\Downloads\test2\b.py

['c:\Users\zhoudongbin\Downloads\test', 'C:\Anaconda2\python27.zip', 'C:\Anaconda2\DLLs', 'C:\Anaconda2\lib', 'C:\Anaconda2\lib\plat-win', 'C:\Anaconda2\lib\lib-tk', 'C:\Anaconda2', 'C:\Anaconda2\lib\site-packages', 'C:\Anaconda2\lib\site-packages\Sphinx-1.4.6-py2.7.egg', 'C:\Anaconda2\lib\site-packages\win32', 'C:\Anaconda2\lib\site-packages\win32\lib', 'C:\Anaconda2\lib\site-packages\Pythonwin', 'C:\Anaconda2\lib\site-packages\setuptools-27.2.0-py2.7.egg', '../test2']

['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'b', 'sys']

可见,a.py 为了导入不在同一目录下的 b 模块,使用 sys.path.append 添加了相对路径'../test2',这一结果能在 sys.path 列表的最后一项看到。正因为有了这一项,所以 import b 这一句才能够成功,因而打印出了 b.py 所在的文件路径。并且能够通过打印 dir() 的结果,也能看到 b 模块被成功导入了。

现在,让问题稍微复杂一点,再增加一个目录:

|

-- test/a.py

|

-- test2/b.py

|

-- test3/c.py

|

修改代码:

a.py 代码如下:

import sys
sys.path.append('../test2')
import b
import c

if __name__ == '__main__':
    print sys.path
    print dir()

b.py 代码如下:

import sys
import os

sys.path.append('../test3')
import c

print 'from b: %s' % (os.path.realpath(__file__))

c.py 代码如下:

import sys
import os
print 'from c: %s' % (os.path.realpath(__file__)) 

运行结果如下:

from c: c:\Users\zhoudongbin\Downloads\test3\c.pyc

from b: c:\Users\zhoudongbin\Downloads\test2\b.py

['c:\Users\zhoudongbin\Downloads\test', 'C:\Anaconda2\python27.zip', 'C:\Anaconda2\DLLs', 'C:\Anaconda2\lib', 'C:\Anaconda2\lib\plat-win', 'C:\Anaconda2\lib\lib-tk', 'C:\Anaconda2', 'C:\Anaconda2\lib\site-packages', 'C:\Anaconda2\lib\site-packages\Sphinx-1.4.6-py2.7.egg', 'C:\Anaconda2\lib\site-packages\win32', 'C:\Anaconda2\lib\site-packages\win32\lib', 'C:\Anaconda2\lib\site-packages\Pythonwin', 'C:\Anaconda2\lib\site-packages\setuptools-27.2.0-py2.7.egg', '../test2', '../test3']

['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'b', 'c', 'sys']

这里面要注意一个有意思的事情,b 模块中,通过相路径引入了模块 c,同时也将 test3 添加到了当前项目的搜索空间,这样一来,在 a.py 当中,尽管没有添加 test3路径,也一样能够成功导入模块 c。也就是说,同一个项目中的不同模块调用 sys.path.append 所添加的路径,是可以全局共享的。

另外,即使 b.py a.py 都导入了 c 模块,但是 c 的路径只被打印了一次,这说明同一个模块在同一个项目空间中只会被包含一次。

好,有了这些认识,现在把问题变得更复杂一些:

|

-- test/a.py

|

-- test2/b.py

-- test2/test4/d.py

|

-- test3/c.py

|

我们在 test2 目录下,再新建一个 test4目录,同时在里面建立 d 模块,同时修改代码:

b.py 代码如下:

import sys
import os

sys.path.append('../test3')
import c

sys.path.append('./test4')
import d

print 'from b: %s' % (os.path.realpath(__file__))

d.py 代码如下:

import sys
import os
print 'from d: %s' % (os.path.realpath(__file__)) 

b.py 通过相对路径,引入了模块 d ,这看起来没什么问题,先做一点小验证,切换程序目录到 test2, 从 test2 中运行 b.py 可以看到:

from c: c:\Users\zhoudongbin\Downloads\test3\c.pyc

from d: c:\Users\zhoudongbin\Downloads\test2\test4\d.py

from b: c:\Users\zhoudongbin\Downloads\test2\b.py

看起来还不错,b 分别通过相对路径引入了上级目录和下级目录中的模块,似乎没什么问题。但是,如果我们再回到 test 目录运行 a.py 呢?

from c: c:\Users\zhoudongbin\Downloads\test3\c.pyc

Traceback (most recent call last):

File "c:\Users\zhoudongbin\Downloads\test\test.py", line 5, in <module>

import b

File "../test2\b.py", line 10, in <module>

import d

ImportError: No module named d

提示找不到模块 d 。这是因为,模块 d 所在路径相对于 a.py 所在的 test 和 b.py 所在的 test2 来说是不同的, 而 sys.path 中的路径,是相对当前项目工作路径来说的 所以分别从两个不同目录运行程序,所获得的结果却不相同。

那有什么办法能解决这个问题呢?

其实可以借助 __file__ 在当前项目中,用绝对路径代替相对路径来导入模块,这样的好处是,路径是绝对的,从任意路径中导入都不会有问题,同时该绝对路径只针对当前项目运行时有效,不会污染全局命名空间。

具体修改如下:

b.py 代码如下:

import sys
import os

sys.path.append('../test3')
import c

# convert relative path to absolute path!
path_test4 = os.path.join(os.path.dirname(os.path.realpath(__file__)), './test4')
sys.path.append(path_test4)
import d

print 'from b: %s' % (os.path.realpath(__file__))

使用 os.path.join 拼接文件本身所在的绝对路径和相对路径,就能获得目标模块所在的绝对路径,如此修改之后,无论从 test2 运行 b.py, 还是从 test 运行 a.py 都能够得到想要的结果。

a.py 代码如下:

import sys
sys.path.append('../test2')
import b
import c
import d

if __name__ == '__main__':
    print sys.path
    print dir()

从 test 文件夹运行 a.py 的结果:

from c: c:\Users\zhoudongbin\Downloads\test3\c.pyc

from d: c:\Users\zhoudongbin\Downloads\test2\test4\d.pyc

from b: c:\Users\zhoudongbin\Downloads\test2\b.pyc

['c:\Users\zhoudongbin\Downloads\test', 'C:\Anaconda2\python27.zip', 'C:\Anaconda2\DLLs', 'C:\Anaconda2\lib', 'C:\Anaconda2\lib\plat-win', 'C:\Anaconda2\lib\lib-tk', 'C:\Anaconda2', 'C:\Anaconda2\lib\site-packages', 'C:\Anaconda2\lib\site-packages\Sphinx-1.4.6-py2.7.egg', 'C:\Anaconda2\lib\site-packages\win32', 'C:\Anaconda2\lib\site-packages\win32\lib', 'C:\Anaconda2\lib\site-packages\Pythonwin', 'C:\Anaconda2\lib\site-packages\setuptools-27.2.0-py2.7.egg', '../test2', '../test3', 'c:\Users\zhoudongbin\Downloads\test2\./test4']

['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'b', 'c', 'd', 'sys']

可以看到,现在 a.py 也能成功加载 d 模块了。

总结:在项目中,虽然也能通过 sys.path.append 的方式通过添加相对路径来导入其他路径(尤其是上级路径)中的模块,但由于相对路径只对当前程序运行目录有效,所以不具可迁移性。可以借助 os.path.realpath(__file__)能够获取当前文件的绝对路径这个特性,将相对路径转化为绝对路径,能够获得更好的通用性。