Python 3中的相对导入

2024-04-26 21:38:52 发布

您现在位置:Python中文网/ 问答频道 /正文

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

有时它对我有用,但有时我会得到:

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

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

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

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

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


Tags: 文件函数fromimport目录not逻辑perform
3条回答

解释

来自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.

在某个点上PEP 338PEP 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)

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

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)

(强调我的)

如果__name__'__main__',则__name__.rpartition('.')[0]返回空字符串。这就是为什么错误描述中有空字符串文字:

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

CPython的^{} function的相关部分:

if (PyDict_GetItem(interp->modules, package) == NULL) {
    PyErr_Format(PyExc_SystemError,
            "Parent module %R not loaded, cannot perform relative "
            "import", package);
    goto error;
}

如果在interp->modules(可作为^{}访问)中找不到package(包的名称),CPython将引发此异常。由于sys.modules是一个将模块名映射到已加载“的模块的字典,现在很明显,在执行相对导入之前,必须显式绝对导入父模块。

注意:来自issue 18018的修补程序添加了another ^{} 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

但是,您只能在Python3.6或更新版本中看到这一点。

解决方案1:使用-m运行脚本

考虑一个目录(它是一个Pythonpackage):

.
├── package
│   ├── __init__.py
│   ├── module.py
│   └── standalone.py

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

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

我把这两条线包括进去,只是为了让手术的顺序更清楚。我们可以完全忽略它们,因为它们不会影响执行。

初始化py和模块py只包含这两行(即,它们实际上是空的)。

standalone.py还尝试通过相对导入导入module.py

from . import module  # explicit relative import

我们很清楚/path/to/python/interpreter package/standalone.py将失败。但是,我们可以使用^{} command line option运行该模块,该模块将“搜索^{}指定模块,并将其内容作为__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:手动设置软件包

请将其视为概念的证明,而不是实际的解决方案。它不太适合在实际代码中使用。

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

因此

  1. 将当前模块的前一个Nth的父目录添加到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将使用以下相对导入从包导入模块

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

可以找到包装在函数中的更一般的解决方案here。示例用法:

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

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

解决方案3:使用绝对导入和setuptools

步骤是-

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

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

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

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

from package import module  # absolute import

将工作目录更改为project,然后运行/path/to/python/interpreter setup.py install --user--useryour site-packages directory中安装包)(步骤2):

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'>

注意:如果您决定走这条路,最好使用virtual environments单独安装软件包。

解决方案4:使用绝对导入和一些样板代码

坦率地说,安装是不必要的-您可以添加一些样板代码到您的脚本,使绝对导入工作。

我将从解决方案1中借用文件,并更改standalone.py

  1. 在尝试使用绝对导入从package导入任何内容之前,将package的父目录添加到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. 用绝对导入替换相对导入:

    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建议使用绝对导入,但声明在某些情况下可以接受显式相对导入:

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.

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?

像这样的布局很常见。。。

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是全局唯一的,最简单的解决方法是避免使用相对导入,而只使用。。。

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

这是一种痛苦,但有一个线索可以解释为什么在某个吉多·范·罗森写的an email中。。。

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,用于测试目的。

将此文件放入包的初始py文件中:

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

假设你的包裹是这样的:

├── project
│   ├── package
│   │   ├── __init__.py
│   │   ├── module1.py
│   │   └── module2.py
│   └── setup.py

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

# in module2.py
from module1 import class1

这在python 2和3中都有效。

相关问题 更多 >