问题:Python 3中的相对导入
我想从同一目录中的另一个文件导入函数。
有时它对我有用,from .mymodule import myfunction
但有时我得到:
SystemError: Parent module '' not loaded, cannot perform relative import
有时它可与一起使用from mymodule import myfunction
,但有时我也会得到:
SystemError: Parent module '' not loaded, cannot perform relative import
我不了解这里的逻辑,也找不到任何解释。这看起来完全是随机的。
有人可以向我解释所有这些背后的逻辑是什么?
回答 0
不幸的是,该模块需要位于程序包内部,有时还需要作为脚本运行。知道如何实现吗?
像这样的布局很普遍…
main.py
mypackage/
__init__.py
mymodule.py
myothermodule.py
… mymodule.py
像这样…
#!/usr/bin/env python3
# Exported function
def as_int(a):
return int(a)
# Test function for module
def _test():
assert as_int('1') == 1
if __name__ == '__main__':
_test()
……一个myothermodule.py
像这样…
#!/usr/bin/env python3
from .mymodule import as_int
# Exported function
def add(a, b):
return as_int(a) + as_int(b)
# Test function for module
def _test():
assert add('1', '1') == 2
if __name__ == '__main__':
_test()
… main.py
这样的…
#!/usr/bin/env python3
from mypackage.myothermodule import add
def main():
print(add('1', '1'))
if __name__ == '__main__':
main()
…在您运行main.py
或时工作正常mypackage/mymodule.py
,但mypackage/myothermodule.py
由于相对导入而失败,…
from .mymodule import as_int
您应该运行它的方式是…
python3 -m mypackage.myothermodule
…但是有些冗长,并且与像这样的shebang行不能很好地融合在一起#!/usr/bin/env python3
。
假设名称mymodule
在全球范围内是唯一的,这种情况下最简单的解决方法是避免使用相对导入,而只需使用…
from mymodule import as_int
…尽管它不是唯一的,或者您的包结构更复杂,您仍需要在中包含包含包目录的目录PYTHONPATH
,并按以下步骤进行操作…
from mypackage.mymodule import as_int
…或者如果您希望它“开箱即用”运行,则可以PYTHONPATH
使用此方法首先获取输入代码…
import sys
import os
PACKAGE_PARENT = '..'
SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
from mypackage.mymodule import as_int
这有点痛苦,但是有一个线索可以说明为什么某位Guido van Rossum写的电子邮件中 …
我对此表示怀疑,也对任何其他提议的
__main__
机械装置都为-1 。唯一的用例似乎是正在运行的脚本,它们恰好位于模块目录中,我一直将其视为反模式。为了让我改变主意,您必须说服我不要。
在程序包中运行脚本是否是反模式是主观的,但是就我个人而言,我发现它在包含一些自定义wxPython小部件的程序包中非常有用,因此我可以为任何源文件运行脚本以wx.Frame
仅显示包含该小部件用于测试目的。
回答 1
说明
相对导入使用模块的__name__属性来确定该模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,将其设置为’__main__’), 则相对导入的解析就好像该模块是顶级模块一样,无论该模块实际位于文件系统上的哪个位置。
…相对导入依赖于__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
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 package
在interp->modules
(可通过访问)中找不到(包的名称),则会引发此异常sys.modules
。由于sys.modules
是“将模块名称映射到已经加载的模块的字典”,因此现在很清楚,必须在执行相对导入之前显式绝对导入父模块。
注意:问题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
软件包中的所有文件均以相同的两行代码开头:
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
解决方案3:使用绝对导入和设置工具
步骤是-
将显式相对导入替换为等效的绝对导入
安装
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
将软件包安装在site-packages目录中)(步骤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建议使用绝对导入,但指出在某些情况下,显式相对导入是可以接受的:
建议使用绝对导入,因为它们通常更具可读性,并且往往表现得更好(或至少会提供更好的错误消息)。[…]但是,显式相对导入是绝对导入的一种可接受的替代方法,尤其是在处理复杂的包装布局时,使用绝对导入会不必要地冗长。
回答 2
将其放入包的__init__.py文件中:
# For relative imports to work in Python 3.6
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
假设您的包裹是这样的:
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module1.py
│ │ └── module2.py
│ └── setup.py
现在在包中使用常规导入,例如:
# in module2.py
from module1 import class1
这适用于python 2和3。
回答 3
我遇到了这个问题。黑客的解决方法是通过if / else块导入,如下所示:
#!/usr/bin/env python3
#myothermodule
if __name__ == '__main__':
from mymodule import as_int
else:
from .mymodule import as_int
# Exported function
def add(a, b):
return as_int(a) + as_int(b)
# Test function for module
def _test():
assert add('1', '1') == 2
if __name__ == '__main__':
_test()
回答 4
希望这对那里的某人有价值-我浏览了六堆stackoverflow帖子,试图找出与上面上面发布的内容类似的相对进口量。我按照建议设置了所有内容,但仍在执行ModuleNotFoundError: No module named 'my_module_name'
由于我只是在本地开发和玩耍,所以我没有创建/运行setup.py
文件。我也没有明显地设置了我PYTHONPATH
。
我意识到,当我像在模块位于同一目录中那样运行代码时,找不到模块:
$ python3 test/my_module/module_test.py 2.4.0
Traceback (most recent call last):
File "test/my_module/module_test.py", line 6, in <module>
from my_module.module import *
ModuleNotFoundError: No module named 'my_module'
但是,当我明确指定路径时,事情开始起作用:
$ PYTHONPATH=. python3 test/my_module/module_test.py 2.4.0
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s
OK
因此,如果有人尝试了一些建议,则认为他们的代码结构正确,并且如果您不将当前目录导出到PYTHONPATH,则仍然遇到与我类似的情况:
- 运行您的代码,并明确包含如下路径:
$ PYTHONPATH=. python3 test/my_module/module_test.py
- 为避免调用
PYTHONPATH=.
,请创建一个setup.py
内容如下的文件,然后运行python setup.py development
以将软件包添加到路径中:
# setup.py from setuptools import setup, find_packages setup( name='sample', packages=find_packages() )
回答 5
我需要从主项目目录运行python3才能使其工作。
例如,如果项目具有以下结构:
project_demo/
├── main.py
├── some_package/
│ ├── __init__.py
│ └── project_configs.py
└── test/
└── test_project_configs.py
解
我将在文件夹project_demo /中运行python3 ,然后执行
from some_package import project_configs
回答 6
为了解决这个问题,我设计了带有重新包装软件包的解决方案,该解决方案已经为我服务了一段时间。它将上层目录添加到lib路径:
import repackage
repackage.up()
from mypackage.mymodule import myfunction
重新打包可以使用智能策略(检查调用堆栈)进行相对导入,从而在各种情况下都起作用。
回答 7
如果两个软件包都在您的导入路径(sys.path)中,并且您想要的模块/类在example / example.py中,则在没有相对导入的情况下访问该类,请尝试:
from example.example import fkt
回答 8
回答 9
我有一个类似的问题:我需要一个Linux服务和cgi插件,它们使用公共常量进行协作。做到这一点的“自然”方法是将它们放在程序包的init .py中,但是我无法使用-m参数启动cgi插件。
我的最终解决方案与上述解决方案2类似:
import sys
import pathlib as p
import importlib
pp = p.Path(sys.argv[0])
pack = pp.resolve().parent
pkg = importlib.import_module('__init__', package=str(pack))
缺点是必须在常量(或通用函数)前加上pkg:
print(pkg.Glob)