python 3中的相对导入

Relative imports in Python 3

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

有时它对我和from .mymodule import myfunction一起工作,但有时我得到:

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

有时它与from mymodule import myfunction一起工作,但有时我也会得到:

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

我不懂这里的逻辑,也找不到任何解释。这看起来完全是随机的。

有人能给我解释一下这一切背后的逻辑吗?


unfortunately, this module needs to be inside the package, and it also
needs to be runnable as a script, sometimes. Any idea how I could
achieve that?

像这样的布局很常见…

1
2
3
4
5
main.py
mypackage/
    __init__.py
    mymodule.py
    myothermodule.py

…像这样的一个mymodule.py

1
2
3
4
5
6
7
8
9
10
11
12
#!/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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/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

1
2
3
4
5
6
7
8
9
#!/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出现故障…

1
from .mymodule import as_int

你应该跑的方式是…

1
python3 -m mypackage.myothermodule

…但这有点冗长,不能与像#!/usr/bin/env python3这样的shebang线很好地混合。

对于这种情况,最简单的解决方法是,假设名称mymodule是全局唯一的,避免使用相对导入,只使用……

1
from mymodule import as_int

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

1
from mypackage.mymodule import as_int

…或者如果你想让它"开箱即用"的话,你可以先用这个代码从PYTHONPATH中取出…

1
2
3
4
5
6
7
8
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

这是一种痛苦,但有一个线索,为什么在一封由某个吉多·范·罗森写的电子邮件中…

I'm -1 on this and on any other proposed twiddlings of the __main__
machinery. The only use case seems to be running scripts that happen
to be living inside a module's directory, which I've always seen as an
antipattern. To make me change my mind you'd have to convince me that
it isn't.

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


解释

来自PEP 328好的。

Relative imports use a module's __name__ attribute to determine that
module's position in the package hierarchy. If the module's name does
not contain any package information (e.g. it is set to '__main__')
then relative imports are resolved as if the module were a top level
module, regardless of where the module is actually located on the file
system.

Ok.

在某种程度上,PEP 338与PEP 328发生冲突:好的。

... relative imports rely on __name__ to determine the current
module's position in the package hierarchy. In a main module, the
value of __name__ is always '__main__', so explicit relative imports
will always fail (as they only work for a module inside a package)

Ok.

为了解决这个问题,PEP 366引入了顶层变量__package__:好的。

By adding a new module level attribute, this PEP allows relative
imports to work automatically if the module is executed using the -m
switch. A small amount of boilerplate in the module itself will allow
the relative imports to work when the file is executed by name. [...] When it [the attribute] is present, relative imports will be based on this attribute
rather than the module __name__ attribute. [...] When the main module is specified by its filename, then the __package__ attribute will be set to None. [...] When the import system encounters an explicit relative import in a
module without __package__ set (or with it set to None), it will
calculate and store the correct value (__name__.rpartition('.')[0]
for normal modules and __name__ for package initialisation modules)

Ok.

(强调矿山)好的。

如果__name__'__main__'时,__name__.rpartition('.')[0]返回空字符串。这就是错误描述中存在空字符串文字的原因:好的。

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

cpython的PyImport_ImportModuleLevelObject功能的相关部分:好的。

1
2
3
4
5
6
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中找不到package(包的名称)(可访问为sys.modules中),则会引发此异常。由于sys.modules是"一个将模块名映射到已经加载的模块的字典",现在很明显,在执行相对导入之前,必须显式绝对导入父模块。好的。

注:18018版本的补丁增加了另一个if块,将在上述代码之前执行:好的。

1
2
3
4
5
6
7
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为空字符串,则错误消息为好的。

1
ImportError: attempted relative import with no known parent package

但是,您只能在Python3.6或更高版本中看到这一点。好的。解决方案1:使用-m运行脚本

考虑一个目录(它是一个python包):好的。

1
2
3
4
5
.
├── package
│   ├── __init__.py
│   ├── module.py
│   └── standalone.py

包中的所有文件都以相同的两行代码开头:好的。

1
2
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())

我将这两行包括在内,只是为了使操作顺序变得明显。我们可以完全忽略它们,因为它们不会影响执行。好的。

_ init_uuy和module.py只包含这两行(即它们实际上是空的)。好的。

standalone.py还尝试通过相对导入来导入module.py:好的。

1
from . import module  # explicit relative import

我们很清楚,/path/to/python/interpreter package/standalone.py将失败。但是,我们可以使用-m命令行选项运行该模块,该选项将"搜索sys.path"以查找指定模块并将其内容作为__main__模块执行:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:手动设置包

请把它当作概念的证明,而不是实际的解决方案。它不太适合在实际代码中使用。好的。

然而,PEP 366有一个解决这个问题的方法,它是不完整的,因为仅仅设置__package__是不够的。您需要导入模块层次结构中前面至少n个包,其中n是将要搜索的要导入模块的父目录(相对于脚本的目录)的数量。好的。

因此,好的。

  • 将当前模块的第n个前置任务的父目录添加到sys.path好的。

  • sys.path中删除当前文件的目录好的。

  • 使用当前模块的完全限定名导入父模块好的。

  • __package__设置为从2开始的完全限定名好的。

  • 执行相对导入好的。

  • 我将从解决方案1中借用文件并添加更多子包:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    package
    ├── __init__.py
    ├── module.py
    └── subpackage
        ├── __init__.py
        └── subsubpackage
            ├── __init__.py
            └── standalone.py

    这次,standalone.py将使用以下相对导入从包包包中导入module.py好的。

    1
    from ... import module  # N = 3

    我们需要在这一行前面加上样板代码,才能使其正常工作。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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:好的。

    1
    2
    3
    4
    5
    6
    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

    在这里可以找到一个更通用的用函数包装的解决方案。示例用法:好的。

    1
    2
    3
    4
    5
    if __name__ == '__main__' and __package__ is None:
        import_parents(level=3) # N = 3

    from ... import module
    from ...module.submodule import thing

    解决方案3:使用绝对导入和设置工具

    步骤如下:好的。

  • 用等价绝对导入替换显式相对导入好的。

  • 安装package使其可导入好的。

  • 例如,目录结构可能如下好的。

    1
    2
    3
    4
    5
    6
    7
    .
    ├── project
    │&nbsp;&nbsp; ├── package
    │&nbsp;&nbsp; │&nbsp;&nbsp; ├── __init__.py
    │&nbsp;&nbsp; │&nbsp;&nbsp; ├── module.py
    │&nbsp;&nbsp; │&nbsp;&nbsp; └── standalone.py
    │&nbsp;&nbsp; └── setup.py

    setup.py的位置好的。

    1
    2
    3
    4
    5
    from setuptools import setup, find_packages
    setup(
        name = 'your_package_name',
        packages = find_packages(),
    )

    其余的文件是从解决方案1中借用的。好的。

    安装将允许您导入包,而不管您的工作目录是什么(假设不会出现命名问题)。好的。

    我们可以修改standalone.py以使用此优势(步骤1):好的。

    1
    from package import module  # absolute import

    将工作目录更改为project并运行/path/to/python/interpreter setup.py install --user(--user将包安装到站点包目录中)(步骤2):好的。

    1
    2
    vaultah@base:~$ cd project
    vaultah@base:~/project$ python3 setup.py install --user

    让我们验证一下,现在可以将standalone.py作为脚本运行:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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运行无问题:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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'>

    我觉得我应该警告你:不要这样做,特别是如果你的项目有一个复杂的结构。好的。

    作为补充说明,PEP8建议使用绝对进口,但指出在某些情况下,明确的相对进口是可以接受的:好的。

    Absolute imports are recommended, as they are usually more readable
    and tend to be better behaved (or at least give better error
    messages). [...] However, explicit relative imports are an acceptable
    alternative to absolute imports, especially when dealing with complex
    package layouts where using absolute imports would be unnecessarily
    verbose.

    Ok.

    好啊。


    把这个放在你的包的文件里:

    1
    2
    # For relative imports to work in Python 3.6
    import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))

    假设你的包裹是这样的:

    1
    2
    3
    4
    5
    6
    ├── project
    │   ├── package
    │   │   ├── __init__.py
    │   │   ├── module1.py
    │   │   └── module2.py
    │   └── setup.py

    现在在包中使用常规导入,例如:

    1
    2
    # in module2.py
    from module1 import class1

    这在Python2和3中都有效。


    我遇到了这个问题。黑客解决方案通过如下if/else块导入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #!/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()


    希望这对其他人有价值——我浏览了半打stackoverflow帖子,试图找出与上面发布的内容类似的相对导入。我按照建议做了一切,但我还是打了ModuleNotFoundError: No module named 'my_module_name'

    因为我只是在本地开发和到处玩,所以我没有创建/运行setup.py文件。我也没有明显地设置我的PYTHONPATH

    我意识到,当我像以前那样运行代码时,当测试与模块在同一个目录中时,我找不到模块:

    1
    2
    3
    4
    5
    $ 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'

    但是,当我明确指定路径时,事情开始工作:

    1
    2
    3
    4
    5
    6
    $ 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将包添加到路径中:
  • 1
    2
    3
    4
    5
    6
    7
    # setup.py
    from setuptools import setup, find_packages

    setup(
        name='sample',
        packages=find_packages()
    )

    我需要从主项目目录运行python3才能使其正常工作。

    例如,如果项目具有以下结构:

    1
    2
    3
    4
    5
    6
    7
    project_demo/
    ├── main.py
    ├── some_package/
    │   ├── __init__.py
    │   └── project_configs.py
    └── test/
        └── test_project_configs.py

    解决方案

    我会在文件夹项目中运行python3,然后执行

    1
    from some_package import project_configs


    为了避免这个问题,我设计了一个重新打包的解决方案,这个方案已经为我工作了一段时间。它将上层目录添加到lib路径:

    1
    2
    3
    import repackage
    repackage.up()
    from mypackage.mymodule import myfunction

    使用智能策略(检查调用堆栈),重新打包可以使相对导入在广泛的情况下工作。


    如果两个包都在导入路径(sys.path)中,而您想要的模块/类在example/example.py中,那么要在不进行相对导入的情况下访问该类,请尝试:

    1
    from example.example import fkt