这篇博客主要解决python项目中,各个模块相互import导致的各种奇怪问题,主要参考 stackoverflow
常见错误:
ValueError: attempted relative import beyond top-level package
SystemError: Parent module '' not loaded, cannot perform relative import
ImportError: No module named 'xxxx'
1. python什么情况下会把文件夹认为是package呢?
答: 只要在文件夹下建一个 __init__.py 即可
2. python的script(.py)什么情况下被当成top-level script,什么时候module?
答:当使用python xx.py时,xx.py被认为top-level script,当使用import xx或者python -m xx,此时xx.py被认为module
3. python和python -m的区别
答: 看下以下目录结构
program
|-- test
|-- main.py
|-- __init__.py # 必须加,否则test就不是package
其中,main.py中的代码如下
import sys
print(sys.path)
print(__name__)
print(__package__)
我们在program目录下执行:
# python test/main.py
['E:program\\test', ...]
__main__
None
# python -m test.main
['', ...]
__main__
test
可以看到两种方式__name__都设置为了__main__, 但是__package__是有区别的,不加-m并不会认为main.py在任何package里,并且sys.path加入了main.py所在的目录,也就是解释器通过扫描sys.path,在目录E:program\\test下找到的main.py,所以叫top-level script,而加-m后,main.py被认为在package中,并且sys.path加入了当前目录(执行python -m的目录),也就是解释器通过扫描sys.path在当前目录E:program下找到test (package) 下的 main.py,所以叫module
4. 相对导入有以下形式
import . from xx # 从当前package导入xx
import .. from xx # 从父package导入xx
import ... from xx # 从祖父package导入xx
# 多少个点就是向上多少层package
我们在test目录下新建hello.py,输入print('hello'), 然后将main.py改成from . import hello, 此时用上述两种方式运行:
# python test/main.py
SystemError: Parent module '' not loaded, cannot perform relative import
# python -m test.main
hello
不加-m报错,原因很简单,因为上面这种方式__package__为None, main.py根本没有在任何package下,而加-m就成功运行了,此时.指的就是test (package),而这个package下刚好有hello.py,因此成功导入了
如果我们把目录切到test下再运行python -m main, 显然也报错啊,因为此时__package__为'',sys.path被加入了E:program\\test
所以我们可以看到相对导入显然和在什么路径下执行python命令有很大关系,本质上是__package__在发生变化
来一个复杂的项目结构:
program
|-- test1
|-- test1.py
|-- __init__.py
|-- test2
|-- test2.py
|-- __init__.py
|-- test3
|-- test3.py
|-- __init__.py
|-- test
|-- main.py
|-- __init__.py
各文件内容如下:
# main.py
print('p: main')
from .test2 import test2
print('p: main')
# test1.py
print('p: test1')
print(__name__)
print(__package__)
print('p: test1')
# test2.py
print('p: test2')
print(__name__)
print(__package__)
from .test3 import test3
print('p: test2')
# test3.py
print('p: test3')
print(__name__)
print(__package__)
from ...test1 import test1
print('p: test3')
进入program目录,执行python -m test.main得到如下运行结果
p: main
p: test2
test.test2.test2
test.test2
p: test3
test.test2.test3.test3
test.test2.test3
p: test1
test.test1.test1
test.test1
p: test1
p: test3
p: test2
p: main
这个结果很符合预期,main.py在test(package)下,所以main.py中使用from .进入test,而test2.py在test.test2(package),也使用from .进入test2, 而test3.py在test.test2.test3(package), 使用from ...向上三级package到test下,然后到test1下进入test1.py
对于上面的例子,当在test目录下执行命令python或者python -m显然报错,原因就是上面所提到的main.py的不在任何package里,不能使用相对导入。如果把main.py改成from test2 import test2,在test2下执行python -m main呢?此时仍然会报错,但是错在test3.py,因为此时sys.path里包含E:program\\test,test并不算package了,所以test3.py在test2.test3(package)里,所以此时...到超出了顶层package,那用..呢?也不行,因为..表示移到父package test2,而test2下并没有package test1,所以这种情况完全没法实现相对导入的,那怎么办?直接from test1 import test1,因为E:program\\test下直接就有test1 package
总结,原来执行python命令的位置一改变,代码需要发生巨变啊,相对导入这种方式好扯淡啊。那为什么很多优秀的源码还使用了呢? 原因很简单,因为作者认为你不会进入项目目录去执行命令,正常用户都是pip install package, 然后自动安装到site-package目录下,而sys.path默认就加了site-package目录,所以当你import的时候就从site-package下的package引入,好比在我的例子里,你只会从program目录去执行,真是妙哉啊!