解释
来自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函数的相关部分:
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->模块(可作为sys.modules访问)中找到包(包的名称),则会引发此异常。由于sys.module是“将模块名称映射到已加载的模块的字典”,因此现在很明显,在执行相对导入之前,必须显式绝对导入父模块。
注意:第18018期的补丁添加了另一个if块,将在上面的代码之前执行:
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
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
包中的所有文件都以相同的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命令行选项运行该模块,该选项将“在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'>
-我为您完成所有导入工作并自动设置__package__,但您可以在
解决方案#2:手动设置__package__
请将其视为概念的证明,而不是实际的解决方案。它不太适合在实际代码中使用。
PEP366有一个解决这个问题的方法,但是,它是不完整的,因为仅设置__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将使用以下相对导入从包包中导入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
在这里可以找到封装在函数中的更一般的解决方案。示例用法:
if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
解决方案3:使用绝对导入和设置工具
步骤如下-
用等价的绝对进口替换显式相对进口安装程序包以使其可导入
例如,目录结构可能如下
.
├── 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在站点包目录中安装包)(步骤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'>
注意:如果您决定走这条路,最好使用虚拟环境来单独安装软件包。
解决方案4:使用绝对导入和一些样板代码
坦率地说,安装是不必要的——您可以在脚本中添加一些样板代码,以使绝对导入有效。
我将从解决方案#1借用文件并更改标准。py:
在尝试使用绝对导入从包中导入任何内容之前,请将包的父目录添加到sys.path:导入系统从pathlib导入路径#(如果尚未这样做)file=路径(__file__).resolve()parent,root=file.parent,file.parents[1]sys.path.append(str(root))#另外,从sys.path中删除当前文件的目录尝试:sys.path.remove(str(父级))除了ValueError:#已删除通过用绝对导入替换相对导入:来自包导入模块#绝对导入
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建议使用绝对进口,但指出在某些情况下,明确的相对进口是可以接受的:
建议使用绝对导入,因为它们通常更可读并且倾向于表现得更好(或至少给出更好的错误消息)。[…]然而,明确的相对进口是可以接受的替代绝对进口,特别是在处理复杂不需要使用绝对导入的包布局冗长的