先把代码放出来,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所在的目录。所以:
- 在test.py文件中调用lib1.py可以使用绝对引用:from test_lib.lib1 import func1;
- 而在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文件的完整的路径。
这样可以解决外部调用的问题,也可以解决直接运行该文件的问题。但是这并不完美,因为:
- 当我们的项目比较大的时候,子模块就会比较多,这时就会有好多的路径加入到了系统路径中,搜索效率还是小问题,最大的问题是不同目录下的文件名是可能有冲突的,这时可能就会加载到错误的模块了;
- 如果每个模块都加上这么一个代码也很不优雅(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. 小结
我的建议:
- 同一个package的,优先使用相对导入;
- 需要对模块文件测试时,除非是单一的文件,否则不建议使用"if __name__ == '__main__'",而是直接使用一个对应的单元测试文件来测试,这最优雅;
- __all__这个变量作用不大,通常不要使用,import的时候不要使用星号;
- __init__.py定义的是一个package,通常保持空文件就好了。
20210724