Relative imports in Python 3
我想从同一目录中的另一个文件导入一个函数。
有时它对我和
1 | SystemError: Parent module '' not loaded, cannot perform relative import |
有时它与
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 |
…像这样的一个
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() |
…像这样的一个
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() |
…还有一个像这样的
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() |
…当你运行
1 | from .mymodule import as_int |
你应该跑的方式是…
1 | python3 -m mypackage.myothermodule |
…但这有点冗长,不能与像
对于这种情况,最简单的解决方法是,假设名称
1 | from mymodule import as_int |
…尽管,如果它不是唯一的,或者您的包结构更复杂,您需要在
1 | from mypackage.mymodule import as_int |
…或者如果你想让它"开箱即用"的话,你可以先用这个代码从
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小部件的包中非常有用,因此我可以为任何源文件运行脚本,以显示仅包含该小部件的
解释
来自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引入了顶层变量
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.
(强调矿山)好的。
如果
1 | SystemError: Parent module '' not loaded, cannot perform relative import |
cpython的
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在
注:18018版本的补丁增加了另一个
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) { ... */ |
如果
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 |
我们很清楚,
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'> |
请把它当作概念的证明,而不是实际的解决方案。它不太适合在实际代码中使用。好的。
然而,PEP 366有一个解决这个问题的方法,它是不完整的,因为仅仅设置
因此,好的。
将当前模块的第n个前置任务的父目录添加到
从
使用当前模块的完全限定名导入父模块好的。
将
执行相对导入好的。
我将从解决方案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:使用绝对导入和设置工具
步骤如下:好的。
用等价绝对导入替换显式相对导入好的。
安装
例如,目录结构可能如下好的。
1 2 3 4 5 6 7 | . ├── project │ ├── package │ │ ├── __init__.py │ │ ├── module.py │ │ └── standalone.py │ └── 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 |
将工作目录更改为
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:好的。
在尝试使用绝对导入从包中导入任何内容之前,请将包的父目录添加到
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帖子,试图找出与上面发布的内容类似的相对导入。我按照建议做了一切,但我还是打了
因为我只是在本地开发和到处玩,所以我没有创建/运行
我意识到,当我像以前那样运行代码时,当测试与模块在同一个目录中时,我找不到模块:
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,请尝试以下任一操作:
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 |