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
我不明白这里的逻辑,我找不到任何解释。 这看起来完全随机。
有人可以向我解释这一切背后的逻辑是什么?
不幸的是,这个模块需要放在包里面,有时候也需要作为脚本运行。 任何想法我怎么能实现呢?
有这样的布局是相当普遍的…
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
…但它有点冗长,并且不能和像#!/usr/bin/env python3
这样的shebang线混合。
假设名称mymodule
是全局唯一的,这种情况下最简单的解决scheme将是避免使用相对导入,只是使用…
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写的电子邮件中有个线索…
我对这个和其他主要机器的提议都是-1。 唯一的用例似乎是运行在模块目录中的脚本,我一直认为这是一个反模式。 为了让我改变主意,你必须说服我,它不是。
无论是在一个包中运行脚本是否是一个反模式都是主观的,但是我个人发现它在包含一些自定义的wxPython小部件的包中非常有用,所以我可以运行任何源文件的脚本来显示一个wx.Frame
仅包含该小部件的wx.Frame
用于testing目的。
说明
PEP 338有些时候与PEP 328冲突:
…相对导入依赖__name__来确定当前模块在包层次结构中的位置。 在主模块中, __name__的值总是'__main__' ,所以显式的相对导入将总是失败(因为它们只对包中的模块有效)
为了解决这个问题, PEP 366引入了顶层variables__package__
:
通过添加新的模块级属性,如果使用-m开关执行模块,则此PEP允许相对导入自动工作。 当文件按名称执行时,模块中的less量样板将允许相对导入工作。 […]当[属性]存在时,相对导入将基于此属性而不是模块__name__属性。 […]当主模块由其文件名指定时, __package__属性将被设置为None 。 […] 当导入系统在没有__package__设置(或设置为None)的模块中遇到明确的相对导入时,它将计算并存储正确的值 ( __name __。rpartition('。')[0] for正常模块和__name__包初始化模块)
(重点是我的)
如果__name__
是'__main__'
__name__.rpartition('.')[0]
'__main__'
, __name__.rpartition('.')[0]
返回空string。 这就是为什么错误描述中有空string的原因:
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->modules
(可作为sys.modules
访问)中findpackage
( package
的名称), interp->modules
引发此exception。 由于sys.modules
是“将模块名称映射到已经加载的模块的字典” ,所以现在清楚的是在执行相关导入之前,父模块必须显式地绝对导入 。
注意:来自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
(与上面相同)为空string,则会显示错误消息
ImportError: attempted relative import with no known parent package
不过,你只能在Python 3.6或更新版本中看到这个。
解决scheme#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
命令行选项运行该模块,该选项将“search指定模块的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__
,但是你可以自己做
解决scheme#2:手动设置__package__
请把它当作一个概念的certificate,而不是一个实际的解决scheme。 它不适合用于现实世界的代码。
不幸的是,仅仅设置__package__
还不够。 您将需要在模块层次结构中导入至lessN个前面的软件包,其中N是要search要导入的模块的父目录(相对于脚本的目录)的数量。
从而,
-
将当前模块的第N个前任的父目录添加到
sys.path
-
从
sys.path
删除当前文件的目录 -
使用完全限定的名称导入当前模块的父模块
-
将
__package__
设置为2的完全限定名称 -
执行相对导入
我将借用解决scheme1中的文件并添加更多的子包:
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
在这里可以find一个更通用的解决scheme。 用法示例:
if __name__ == '__main__' and __package__ is None: import_parents(level=3) # N = 3 from ... import module from ...module.submodule import thing
解决scheme#3:使用绝对导入和setuptools
步骤是 –
-
用等同的绝对导入replace显式的相对导入
-
安装
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(), )
其余的文件是从解决scheme#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
让我们validation现在可以运行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'>
注意 :如果你决定走这条路线,最好使用虚拟环境隔离安装软件包。
解决scheme4:使用绝对导入和一些样板代码
坦率地说,安装是没有必要的 – 你可以添加一些样板代码到你的脚本,使绝对导入工作。
我要从解决scheme#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
-
用绝对导入replace相对导入:
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 8build议使用绝对import量,但指出在某些情况下明确的相对import量是可以接受的:
build议使用绝对导入,因为它们通常更具可读性,往往performance更好(或者至less提供更好的错误消息)。 然而,明确的相对import是绝对import的可接受的替代scheme,特别是在处理使用绝对import的复杂包装布局不必要的冗长时。
我遇到了这个问题。 Hack解决方法是使用try / except块,如下所示:
#!/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()
如果两个软件包都在你的导入path(sys.path)中,并且你想要的模块/类是example / example.py,那么访问这个类时不需要相对导入。
from example.example import fkt