Python 3中的相对导入


711

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

有时它对我有用,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

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

有人可以向我解释所有这些背后的逻辑是什么?


76
这意味着您正在以脚本的形式运行包中的模块。仅从程序包外部运行脚本。
马丁·彼得

3
也许您应该定义提到的“有时”情况。我了解您并不意味着您有随机错误。
joaquin

15
@MartijnPieters:不幸的是,这个模块需要放在包中,有时还需要作为脚本运行。知道如何实现吗?
约翰·史密斯

22
@JohnSmithOptional:在程序包中混合脚本很棘手,应尽可能避免。使用包装脚本导入包装,然后运行您的“脚本”功能。
马丁·彼得斯

3
似乎很不幸。我用可以解析/分析某种文件的类/方法制作了一个核心模块,并且(主要是为我自己)我也有单独的辅助模块和导入该文件的脚本-这些可以对这些文件进行按摩/转换。但是我还希望能够将单个核心文件(而不是整个复杂的软件包)交给最终用户,这样他们可以轻松地将其放置在文件旁边并运行它。在该“脚本模式”下,它将分析和分析文件和编码,汇总各种字段/值/特殊字符,并给出报告。但是它实际上并没有修改文件。反模式?
乔恩·库姆斯

Answers:


526

不幸的是,该模块需要位于程序包内部,有时还需要作为脚本运行。知道如何实现吗?

像这样的布局很普遍...

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

...但是有些冗长,并且与像这样的shebang行不能很好地融合在一起#!/usr/bin/env python3

假设名称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

这有点痛苦,但是有一个线索可以说明为什么某位Guido van Rossum写的电子邮件中 ...

我对此表示怀疑,也对任何其他提议的__main__ 机械装置都为-1 。唯一的用例似乎是正在运行的脚本,它们恰好位于模块目录中,我一直将其视为反模式。为了让我改变主意,您必须说服我不要。

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


7
一个更好的办法让SCRIPTDIR在给定的导入模块从一个相对路径的注释os.path.realpath(os.path.dirname(inspect.getfile(inspect.currentframe()))),如果你确信你的模块具有总是正确的file,你也可以使用os.path.realpath(os.path.dirname(__file__))
marcz 2014年

2
您可以通过应用更短且可读的代码段来扩展PYTHONPATH: sys.path.append( os.path.join( os.path.dirname(__file__), os.path.pardir ) )
Alex-Bogdanov

12
...which I've always seen as an antipattern.我不知道这是一种反模式...似乎简单地使相对导入直观地工作将非常方便。我只希望能够导入我知道在同一目录中的内容。我想知道他的理由是什么
-YungGun

8
Guido再次罢工:将本来有用的东西混在一起。好吧,那将不再发生。
javadba

3
这是我所见过的关于Python的最可悲的事情。
AtilioA

262

说明

PEP 328

相对导入使用模块的__name__属性来确定该模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,将其设置为'__main__'), 则相对导入的解析就好像该模块是顶级模块一样,无论该模块实际位于文件系统上的哪个位置。

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

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

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

通过添加新的模块级别属性,如果使用-m 开关执行模块,则该PEP允许相对导入自动进行。当按名称执行文件时,模块本身中的少量样板文件将允许相对导入工作。[...]如果存在[属性],则相对导入将基于此属性而不是模块__name__属性。[...]当通过其文件名指定主模块时,__package__属性将设置为None。[...] 当导入系统在未设置__package__的模块(或将其设置为None)的模块中遇到显式相对导入时,它将计算并存储正确的值__name __。rpartition('。')[0]用于常规模块__ 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 packageinterp->modules(可通过访问)中找不到(包的名称),则会引发此异常sys.modules。由于sys.modules“将模块名称映射到已经加载的模块的字典”,因此现在很清楚,必须在执行相对导入之前显式绝对导入父模块

注意:问题18018中 的补丁添加了另一个ifblock,它将在以上代码之前执行:

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__.pymodule.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将软件包安装在site-packages目录中)(步骤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'>

注意:如果您决定走这条路,最好使用虚拟环境来隔离安装软件包。

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

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


3
__package__如果__main__要解决问题,可以手动设置名称吗?
Paulo Scardine,2015年

谢谢,很好的答案!我能够使用该imp模块加载模块并进行相应设置__package__,但结果显然是反模式。
Paulo Scardine,2015年

我得到了错误AttributeError: 'PosixPath' object has no attribute 'path'
用户

感谢您的快速回复。我正在使用nltk软件包,但出现错误:`nltk.decorators的<module>中的文件“ /usr/local/lib/python3.5/dist-packages/nltk/__init__.py”,第115行导入装饰器,在<module> sys.path = [在p。sys.path = [p表示sys.path中的p表示p / usr / local / lib / python3.5 / dist-packages / nltk / decorators.py“ “不在p中]在<listcomp> sys.path = 23中的文件“ /usr/local/lib/python3.5/dist-packages/nltk/decorators.py”中,第23行= [如果“ nltk“不在p中] TypeError:类型'PosixPath'的参数不可迭代`
用户,

1
:您还可以通过导入文件路径的文件(相对太)docs.python.org/3/library/...
按Ctrl-C

84

将其放入包的__init__.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。


如果我们将其包装成卷,会完成这项工作
Alex Punnen

1
我也认为这值得更多的选票。将其放入每一个__init__.py将基本上解决所有相对的导入错误。
frankliuao

3
我不能代表别人,但是我倾向于避免进行修改,sys.path因为我担心它可能会影响其他代码。(部分原因是因为我不知道它如何工作的复杂性。)
pianoJames

@pianoJames我知道您的意思,这种魔术修复(看起来,经过很多反复)似乎有点太简单了。但这有效。有兴趣的人不知道这些是否有负面影响。
乔恩

我现在正在使用它:到目前为止,一切都很好。
javadba

37

我遇到了这个问题。黑客的解决方法是通过if / else块导入,如下所示:

#!/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()

29
那不是一个很好的解决方案。另外,光是except:坏的。用except ImportError:代替!
ThiefMaster 2015年

6
这是SystemError在这里。(Py 3.4)
Avi

8
这不是一个糟糕的主意,但最好是检测要使用的导入,而不是try / except。有点像if __name__ == '__main__': from mymod import as_int; else: from .mymod import as_int
珀金斯

@Perkins好吧...在大多数情况下不会。我认为相对进口也许是个例外。
wizzwizz4

8

希望这对那里的某人有价值-我浏览了六堆stackoverflow帖子,试图找出与上面上面发布的内容类似的相对进口量。我按照建议设置了所有内容,但仍在执行ModuleNotFoundError: No module named 'my_module_name'

由于我只是在本地开发和玩耍,所以我没有创建/运行setup.py文件。我也没有明显地设置了我PYTHONPATH

我意识到,当我像在模块位于同一目录中那样运行代码时,找不到模块:

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

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

$ PYTHONPATH=. python3 test/my_module/module_test.py                                                                                                  2.4.0
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

因此,如果有人尝试了一些建议,则认为他们的代码结构正确,并且如果您不将当前目录导出到PYTHONPATH,则仍然遇到与我类似的情况:

  1. 运行您的代码,并明确包含如下路径: $ PYTHONPATH=. python3 test/my_module/module_test.py
  2. 为避免调用PYTHONPATH=.,请创建一个setup.py内容如下的文件,然后运行python setup.py development以将软件包添加到路径中:
# setup.py
from setuptools import setup, find_packages

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

6

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

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

project_demo/
├── main.py
├── some_package/
   ├── __init__.py
   └── project_configs.py
└── test/
    └── test_project_configs.py

我将在文件夹project_demo /中运行python3 ,然后执行

from some_package import project_configs

4

为了解决这个问题,我设计了带有重新包装软件包的解决方案,该解决方案已经为我服务了一段时间。它将上层目录添加到lib路径:

import repackage
repackage.up()
from mypackage.mymodule import myfunction

重新打包可以使用智能策略(检查调用堆栈)进行相对导入,从而在各种情况下都起作用。


到目前为止,最简单的解决方案!谢谢!
CodingInCircles

1
谢谢!我没有尝试给出最佳答案,而是尝试给出了可用的答案:-)
fralau

3

如果两个软件包都在您的导入路径(sys.path)中,并且您想要的模块/类在example / example.py中,则在没有相对导入的情况下访问该类,请尝试:

from example.example import fkt


0

我有一个类似的问题:我需要一个Linux服务和cgi插件,它们使用公共常量进行协作。做到这一点的“自然”方法是将它们放在程序包的init .py中,但是我无法使用-m参数启动cgi插件。

我的最终解决方案与上述解决方案2类似:

import sys
import pathlib as p
import importlib

pp = p.Path(sys.argv[0])
pack = pp.resolve().parent

pkg = importlib.import_module('__init__', package=str(pack))

缺点是必须在常量(或通用函数)前加上pkg:

print(pkg.Glob)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.