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.pymypackage/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访问)中findpackagepackage的名称), 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__.pymodule.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要导入的模块的父目录(相对于脚本的目录)的数量。

从而,

  1. 将当前模块的第N个前任的父目录添加到sys.path

  2. sys.path删除当前文件的目录

  3. 使用完全限定的名称导入当前模块的父模块

  4. __package__设置为2的完全限定名称

  5. 执行相对导入

我将借用解决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

步骤是 –

  1. 用等同的绝对导入replace显式的相对导入

  2. 安装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

  1. 尝试使用绝对导入从包中导入任何内容之前 ,将的父目录添加到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 
  2. 用绝对导入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