【Python】对绝对导入与相对导入的理解的补充

2021-10-28 14:58:41 浏览数 (1)

先把代码放出来,test.py:

代码语言:javascript复制
import sys
from test_lib.lib1 import func1

print(f'n__name__: {__name__} in ./test.py')
print(f'__package__: {__package__} in ./test.py')
print(sys.path)

test_lib/lib1.py:

代码语言:javascript复制
import sys
from .lib2 import func2

def func1():
    print('func1')

print(f'n__name__: {__name__} in ./test_lib/lib1.py')
print(f'__package__: {__package__} in ./test_lib/lib1.py')
print(sys.path)

test_lib/lib2.py:

代码语言:javascript复制
import sys
print(f'n__name__: {__name__} in ./test_lib/lib2.py')
print(f'__package__: {__package__} in ./test_lib/lib2.py')
print(sys.path)

def func2():
    print('func2')

另外,还有两个__init__.py文件(空文件)就省略了。

1. 相对导入


在前一篇文章对相对导入的“相对”其实已经讲得比较清楚了,关键的一点是,“相对”是相对package的意思。相对导入的常用语法:

代码语言:javascript复制
from .lib2 import func2
from . import lib2

另外还有直接导入上级的包或者模块,但是这很容易出错,建议不要使用。上面的两种语法,建议也只使用第一种。

当然,前面一篇文章已经说过了,使用相对导入是有缺陷的,没法直接运行该文件进行测试。

2. 绝对导入


前一篇已经提到,理解绝对导入,最重要的就是理解sys.path这个环境变量,绝对导入的时候,会按顺序在这个path指定的路径中查找。如果找到了,则加载进来,而如果对所有路径都查找完了还是没有找到,则会报错。

前面的代码已经写好,我们直接运行:python test.py,会得到输出:

代码语言:javascript复制
__name__: test_lib.lib2 in ./test_lib/lib2.py
__package__: test_lib in ./test_lib/lib2.py
['/home/deeao/test', 
 '/home/alex/.local/lib/python3.8/site-packages', 
 '/usr/local/lib/python3.8/dist-packages', 
 '/usr/lib/python3/dist-packages'
]

__name__: test_lib.lib1 in ./test_lib/lib1.py
__package__: test_lib in ./test_lib/lib1.py
['/home/deeao/test', 
 '/home/alex/.local/lib/python3.8/site-packages', 
 '/usr/local/lib/python3.8/dist-packages', 
 '/usr/lib/python3/dist-packages'
]

__name__: __main__ in ./test.py
__package__: None in ./test.py
['/home/deeao/test', 
 '/home/alex/.local/lib/python3.8/site-packages', 
 '/usr/local/lib/python3.8/dist-packages', 
 '/usr/lib/python3/dist-packages'
]

可以看到sys.path在三个脚本中的输出结果都是一样的,默认加入到path路径的只有直接被运行的文件所在的目录(如果在其他目录下,运行test.py文件结果也是一样的,例如在上级目录运行:python test/test.py),“/home/deeao/test”这个是test.py所在的目录。所以:

  1. 在test.py文件中调用lib1.py可以使用绝对引用:from test_lib.lib1 import func1;
  2. 而在lib1中引用lib2的时候,就要使用相对引用了:from .lib2 import func2,如果把lib2前面的点号去掉,则会报错,因为在sys.path的路径中找不到lib2这个模块。

如果在lib1中不想使用相对导入怎么办?

一种解决方法是改成:“from test_list.lib2 import func2”,这样在路径/home/deeao/test中就能找到对应的模块了。但是这样会有一个很大的弊端:这时如果直接运行lib1.py进行测试的时候,则会报错,因为在系统路径中没法找到“test_list.lib2”这个模块。但是直接运行文件测试又是非常常用的。

另一种解决方案是,在import之前将当前路径加入到sys.path中:

代码语言:javascript复制
import sys
from os.path import abspath, dirname
# 把当前文件所在的目录加入到sys.path中
# 相当于把 /home/deeao/test/test_lib 加入了路径中
sys.path.insert(0, abspath(dirname(__file__)))

from lib2 import func2

__file__这个内置变量是当前python文件的完整的路径。

这样可以解决外部调用的问题,也可以解决直接运行该文件的问题。但是这并不完美,因为:

  1. 当我们的项目比较大的时候,子模块就会比较多,这时就会有好多的路径加入到了系统路径中,搜索效率还是小问题,最大的问题是不同目录下的文件名是可能有冲突的,这时可能就会加载到错误的模块了;
  2. 如果每个模块都加上这么一个代码也很不优雅(DRY),维护也不方便。

3. 建议的选择


前面已经看到了,无论是相对导入,还是绝对导入,都是有缺陷的,那我们已经怎么选择呢?

我的建议:优先使用相对导入

不要使用直接运行python文件的方式来测试,而是使用单元测试,例如对于lib1.py的测试应该是单独建立一个单元测试文件:lib1_test.py,由这个文件来进行(单元)测试。

这是我能想到的最优雅的方式了。

4. __all__变量与__init__.py文件


关于包和模块还有两点是值得说道说道的:

4.1 __all__变量

直接看代码,lib2.py:

代码语言:javascript复制
__all__ = ['func2']

def func2():
    print('func2')

def func3():
    print('func3')

lib1.py:

代码语言:javascript复制
from .lib2 import *

func2()
func3()

这时运行python lib1.py,会报错:

代码语言:javascript复制
NameError: name 'func3' is not defined

而如果把import语句改成“from .lib2 import func2, func3”,就能正常运行。

也就是说,通过星号导入的只能是__all__变量定义的对象。

不过建议在导入的时候,不要使用星号。

4.2 __init__.py文件

__init__.py也是个神奇的文件,很多比较初级的工程师可能都比较疑惑,这个文件有什么用。前一篇文章已经说过,这个文件是用来定义一个package的,有这个文件,就表示当前目录是一个package了。

还是先看代码,test_lib/__init__.py:

代码语言:javascript复制
from .lib1 import func1
from .lib2 import func2

./test.py:

代码语言:javascript复制
from test_lib import func1, func2

这样是可以正常运行的,也就是说,在__init__.py定义的变量函数什么的,或者引用其他模块的,在其他package可以通过包名直接导入。

不过,如果没有特别的,通常保持一个空文件__init__.py就好了,没必要操这个心。

5. 小结

我的建议:

  1. 同一个package的,优先使用相对导入;
  2. 需要对模块文件测试时,除非是单一的文件,否则不建议使用"if __name__ == '__main__'",而是直接使用一个对应的单元测试文件来测试,这最优雅;
  3. __all__这个变量作用不大,通常不要使用,import的时候不要使用星号;
  4. __init__.py定义的是一个package,通常保持空文件就好了。

20210724

0 人点赞