Python 3 中的相对导入

新手上路,请多包涵

我想从同一目录中的另一个文件导入一个函数。

通常,以下工作之一:

 from .mymodule import myfunction

 from mymodule import myfunction

…但另一个给了我以下错误之一:

 ImportError: attempted relative import with no known parent package

 ModuleNotFoundError: No module named 'mymodule'

 SystemError: Parent module '' not loaded, cannot perform relative import

为什么是这样?

原文由 John Smith Optional 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 832
2 个回答

不幸的是,这个模块需要在包中,有时它还需要作为脚本运行。知道我怎么能做到这一点吗?

有这样的布局很常见……

 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()

…a 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 是全局唯一的,将避免使用相对导入,而只需使用…

 from mymodule import as_int

…虽然,如果它不是唯一的,或者您的包结构更复杂,您需要在 PYTHONPATH 中包含包含您的包目录的目录,然后这样做……

 from mypackage.mymodule import as_int

…或者,如果您希望它“开箱即用”,您可以先使用此代码在代码中使用 PYTHONPATH

 import sys
import os

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.dirname(SCRIPT_DIR))

from mypackage.mymodule import as_int

这有点痛苦,但在某位 Guido van Rossum 写 的电子邮件 中,有一个线索可以解释为什么……

我在这个和任何其他提议的 __main__ 机器上的玩弄都是-1。唯一的用例似乎是运行恰好位于模块目录中的脚本,我一直将其视为反模式。要让我改变主意,你必须说服我它不是。

在包中运行脚本是否是反模式是主观的,但我个人发现它在我拥有的包含一些自定义 wxPython 小部件的包中非常有用,因此我可以为任何源文件运行脚本以显示 wx.Frame 仅包含用于测试目的的小部件。

原文由 Aya 发布,翻译遵循 CC BY-SA 4.0 许可协议

解释

来自 PEP 328

相对导入使用模块的 name 属性来确定该模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,它被设置为’main‘) ,那么相对导入将被解析为就好像该模块是顶级模块一样,而不管该模块实际位于文件系统中的何处。

在某些时候 PEP 338PEP 328 冲突:

…相对导入依赖 name 来确定当前模块在包层次结构中的位置。在主模块中, name 的值始终为 _’main’_ ,因此显式相对导入将始终失败(因为它们仅适用于包内的模块)

为了解决这个问题, PEP 366 引入了顶级变量 __package__

通过添加一个新的模块级属性,如果使用 -m 开关执行模块,此 PEP 允许相对导入自动工作。当文件按名称执行时,模块本身中的少量样板将允许相对导入工作。 […] 当它 [属性] 存在时,相对导入将基于此属性而不是模块 name 属性。 […] 当主模块由其文件名指定时, package 属性将设置为 None 。 […] 当导入系统在没有设置 package (或设置为无)的模块中遇到显式相对导入时,它将计算并存储正确的值name.rpartition(‘.’)[0] for普通模块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 (包的名称)(可通过 sys.modules 访问), interp->modules 引发此异常。由于 sys.modules“将模块名称映射到已经加载的模块的字典” ,现在很明显, 在执行相对导入之前,必须显式绝对导入父模块

注意: issue 18018 的patch增加了 另一个 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 是将搜索要导入的模块的父目录(相对于脚本目录)的数量。

因此,

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

  2. sys.path

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

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

  5. 执行相对导入

我将从 解决方案 #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:使用绝对导入和 设置工具

步骤是 -

  1. 用等效的绝对导入替换显式相对导入

  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(),
)

其余文件是从 解决方案 #1 借来的。

安装将允许您导入包,而不管您的工作目录如何(假设没有命名问题)。

我们可以修改 standalone.py 来利用这个优势(第 1 步):

 from package import module  # absolute import

将您的工作目录更改为 project 并运行 /path/to/python/interpreter setup.py install --user ( --user您的站点安装包)-steps2 目录

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

  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

  1. 用绝对导入替换相对导入:
    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 建议使用绝对导入,但指出在某些情况下显式相对导入是可以接受的:

建议使用绝对导入,因为它们通常更具可读性并且往往表现得更好(或者至少给出更好的错误消息)。 […] 但是,显式相对导入是绝对导入的可接受替代方案,特别是在处理复杂的包布局时,使用绝对导入会不必要地冗长。

原文由 vaultah 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题