说明
来自PEP 328
相对导入使用模块的 __name__ 属性来确定
模块在包层次结构中的位置。如果模块的名称
不包含任何包信息(例如,它设置为 '__main__')
然后解析相对导入,就好像模块是顶级模块一样
模块,不管模块在文件中的实际位置
系统。
在某些时候PEP 338 与PEP 328 发生冲突:
... 相对导入依赖于 __name__ 来确定当前
模块在包层次结构中的位置。在主模块中,
__name__ 的值总是 '__main__',所以显式的相对导入
总是会失败(因为它们只适用于包内的模块)
为了解决这个问题,PEP 366 引入了顶级变量__package__:
通过添加新的模块级属性,此 PEP 允许相对
如果使用 -m 执行模块,导入会自动工作
转变。模块本身中的少量样板将允许
当文件按名称执行时,相对导入起作用。 [...]当它[属性]存在时,相对导入将基于此属性
而不是模块 __name__ 属性。 [...] 当主模块由其文件名指定时,__package__ 属性将设置为 None。 [...] 当导入系统遇到一个显式的相对导入时
未设置 __package__ 的模块(或将其设置为 None),它将
计算并存储正确的值 (__name__.rpartition('.')[0]
对于普通模块和__name__对于包初始化模块)
(强调我的)
如果__name__ 是'__main__',__name__.rpartition('.')[0] 返回空字符串。这就是错误描述中有空字符串文字的原因:
SystemError: Parent module '' not loaded, cannot perform relative import
CPython的PyImport_ImportModuleLevelObject function的相关部分:
if (PyDict_GetItem(interp->modules, package) == NULL) {
PyErr_Format(PyExc_SystemError,
"Parent module %R not loaded, cannot perform relative "
"import", package);
goto error;
}
如果 CPython 在interp->modules(可作为sys.modules 访问)中找不到package(包的名称),则会引发此异常。由于sys.modules 是“将模块名称映射到已经加载的模块的字典”,现在很明显在执行相对导入之前必须明确地绝对导入父模块强>。
注意:来自issue 18018的补丁添加了another if block,它将在之前上面的代码执行: p>
if (PyUnicode_CompareWithASCIIString(package, "") == 0) {
PyErr_SetString(PyExc_ImportError,
"attempted relative import with no known parent package");
goto error;
} /* else if (PyDict_GetItem(interp->modules, package) == NULL) {
...
*/
如果package(同上)为空字符串,错误信息为
ImportError: attempted relative import with no known parent package
但是,您只会在 Python 3.6 或更高版本中看到这一点。
解决方案 #1:使用 -m 运行脚本
考虑一个目录(这是一个 Python package):
.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
package 中的所有文件都以相同的 2 行代码开头:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
我只包含这两行是为了让操作的顺序更明显。我们可以完全忽略它们,因为它们不会影响执行。
__init__.py 和 module.py 仅包含这两行(即它们实际上是空的)。
standalone.py 还尝试通过相对导入来导入 module.py:
from . import module # explicit relative import
我们很清楚/path/to/python/interpreter package/standalone.py 会失败。但是,我们可以使用-m command line option 运行模块,这将“搜索sys.path 以查找命名模块并将其内容作为__main__ 模块执行”:
vaultah@base:~$ python3 -i -m package.standalone
Importing /home/vaultah/package/__init__.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/module.py
>>> __file__
'/home/vaultah/package/standalone.py'
>>> __package__
'package'
>>> # The __package__ has been correctly set and module.py has been imported.
... # What's inside sys.modules?
... import sys
>>> sys.modules['__main__']
<module 'package.standalone' from '/home/vaultah/package/standalone.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
-m 为您完成所有导入工作并自动设置__package__,但您可以在
解决方案 #2:手动设置 __package__
请将其视为概念验证而非实际解决方案。它不适合在实际代码中使用。
PEP 366 有一个解决这个问题的方法,但是,它并不完整,因为单独设置__package__ 是不够的。您将需要在模块层次结构中至少导入 N 个前面的包,其中 N 是父目录的数量(相对于脚本的目录)将搜索正在导入的模块。
因此,
将当前模块的第N个前身的父目录添加到sys.path
从sys.path删除当前文件的目录
使用完全限定名导入当前模块的父模块
将 __package__ 设置为来自 2
的完全限定名称
执行相对导入
我将从解决方案#1中借用文件并添加更多子包:
package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
这一次 standalone.py 将使用以下相对导入从 package 包中导入 module.py
from ... import module # N = 3
我们需要在该行之前加上样板代码,以使其正常工作。
import sys
from pathlib import Path
if __name__ == '__main__' and __package__ is None:
file = Path(__file__).resolve()
parent, top = file.parent, file.parents[3]
sys.path.append(str(top))
try:
sys.path.remove(str(parent))
except ValueError: # Already removed
pass
import package.subpackage.subsubpackage
__package__ = 'package.subpackage.subsubpackage'
from ... import module # N = 3
它允许我们通过文件名执行standalone.py:
vaultah@base:~$ python3 package/subpackage/subsubpackage/standalone.py
Running /home/vaultah/package/subpackage/subsubpackage/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/subpackage/__init__.py
Importing /home/vaultah/package/subpackage/subsubpackage/__init__.py
Importing /home/vaultah/package/module.py
可以在here 中找到更通用的封装在函数中的解决方案。示例用法:
if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
步骤是-
将显式相对导入替换为等效的绝对导入
安装package 使其可导入
例如,目录结构可能如下
.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
setup.py 在哪里
from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
其余文件是从解决方案#1借来的。
安装将允许您导入包,而不管您的工作目录是什么(假设不会有命名问题)。
我们可以修改 standalone.py 来利用这个优势(步骤 1):
from package import module # absolute import
将您的工作目录更改为project 并运行/path/to/python/interpreter setup.py install --user(--user 将软件包安装到your site-packages directory)(步骤2):
vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user
让我们验证现在是否可以将 standalone.py 作为脚本运行:
vaultah@base:~/project$ python3 -i package/standalone.py
Running /home/vaultah/project/package/standalone.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
注意:如果您决定走这条路,最好使用virtual environments 单独安装软件包。
解决方案 #4:使用绝对导入和一些样板代码
坦率地说,安装不是必需的 - 您可以在脚本中添加一些样板代码以使绝对导入工作。
我要从 Solution #1 中借用文件并更改 standalone.py:
-
将 package 的父目录添加到 sys.path之前 尝试使用绝对导入从 package 导入任何内容:
import sys
from pathlib import Path # if you haven't already done so
file = Path(__file__).resolve()
parent, root = file.parent, file.parents[1]
sys.path.append(str(root))
# Additionally remove the current file's directory from sys.path
try:
sys.path.remove(str(parent))
except ValueError: # Already removed
pass
-
将相对导入替换为绝对导入:
from package import module # absolute import
standalone.py 运行没有问题:
vaultah@base:~$ python3 -i package/standalone.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
我觉得我应该警告你:尽量不要这样做,尤其是如果你的项目结构复杂。
附带说明,PEP 8 建议使用绝对导入,但指出在某些情况下可以接受显式相对导入:
建议使用绝对导入,因为它们通常更具可读性
并且往往表现得更好(或至少给出更好的错误
消息)。 [...] 但是,明确的相对导入是可以接受的
绝对导入的替代方案,尤其是在处理复杂的
不必要使用绝对导入的包布局
详细。