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