解释
从PEP 328
相对导入使用模块的 __name__ 属性来确定该模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,它被设置为'__main__')
,那么无论模块实际位于文件系统上的什么位置,相对导入都会像模块一样被解析为顶级模块。
在某些时候PEP 338与PEP 328冲突:
...相对导入依赖__name__来确定当前模块在包层次结构中的位置。在主模块中,__name__的值始终为'__main__',因此显式相对导入将始终失败(因为它们仅适用于包内的模块)
为了解决这个问题,PEP 366引入了顶级变量__package__
:
通过添加新的模块级别属性,如果使用-m
开关执行模块,此 PEP 允许相对导入自动工作。当文件按名称执行时,模块本身的少量样板将允许相对导入工作。[...]当 [属性] 存在时,相对导入将基于此属性而不是模块__name__属性。[...] 当主模块由其文件名指定时,__package__属性将设置为None。[...]当导入系统在没有设置 __package__ (或设置为 None )的模块中遇到显式相对导入时,它将计算并存储正确的值(__name__.rpartition('.')[0] 用于普通模块,__name__用于包初始化模块)
(强调我的)
如果__name__
是'__main__'
,则__name__.rpartition('.')[0]
返回空字符串。这就是错误描述中有空字符串文字的原因:
SystemError: Parent module '' not loaded, cannot perform relative import
PyImport_ImportModuleLevelObject
CPython函数的相关部分:
if (PyDict_GetItem(interp->modules, package) == NULL) {
PyErr_Format(PyExc_SystemError,
"Parent module %R not loaded, cannot perform relative "
"import", package);
goto error;
}
如果 CPython 无法在(accessible as ) 中找到package
(包的名称),则会引发此异常。由于是“将模块名称映射到已经加载的模块的字典”,现在很明显父模块必须在执行相对导入之前显式地绝对导入。interp->modules
sys.modules
sys.modules
注意: issue 18018 的补丁添加了另一个if
block,它将上面的代码之前执行:
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'>
-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将使用以下相对导入从包包中导入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
步骤是 -
用等效的绝对导入替换显式相对导入
安装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
将包安装在您的站点包目录中)(步骤 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中借用文件并更改Standalone.py:
在尝试使用绝对导入从包中导入任何内容之前,将包的父目录添加到:sys.path
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建议使用绝对导入,但指出在某些情况下显式相对导入是可以接受的:
建议使用绝对导入,因为它们通常更具可读性并且往往表现更好(或至少提供更好的错误消息)。[...] 但是,显式相对导入是绝对导入的可接受替代方案,特别是在处理复杂的包布局时,使用绝对导入会不必要地冗长。