Python 3中的相对导入

本文翻译自:Relative imports in Python 3

I want to import a function from another file in the same directory. 我想从同一目录中的另一个文件导入函数。

Sometimes it works for me with from .mymodule import myfunction but sometimes I get a: 有时它与from .mymodule import myfunction对我from .mymodule import myfunction但有时我得到:

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

Sometimes it works with from mymodule import myfunction , but sometimes I also get a: 有时它与from mymodule import myfunction ,但有时我也得到:

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

I don't understand the logic here, and I couldn't find any explanation. 我不了解这里的逻辑,也找不到任何解释。 This looks completely random. 这看起来完全是随机的。

Could someone explain to me what's the logic behind all this? 有人可以向我解释所有这些背后的逻辑是什么?


#1楼

参考:https://stackoom.com/question/19Flx/Python-中的相对导入


#2楼

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? 知道如何实现吗?

It's quite common to have a layout like this... 像这样的布局很普遍...

main.py
mypackage/
    __init__.py
    mymodule.py
    myothermodule.py

...with a mymodule.py like this... ...像这样的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 like this... ...像这样的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()

...and a main.py like this... ...以及像这样的main.py ...

#!/usr/bin/env python3

from mypackage.myothermodule import add

def main():
    print(add('1', '1'))

if __name__ == '__main__':
    main()

...which works fine when you run main.py or mypackage/mymodule.py , but fails with mypackage/myothermodule.py , due to the relative import... ...在您运行main.pymypackage/mymodule.py时可以正常main.py ,但由于相对导入而导致mypackage/myothermodule.py失败...

from .mymodule import as_int

The way you're supposed to run it is... 您应该运行它的方式是...

python3 -m mypackage.myothermodule

...but it's somewhat verbose, and doesn't mix well with a shebang line like #!/usr/bin/env python3 . ...但是它有点冗长,并且不能与#!/usr/bin/env python3类的shebang行混合使用。

The simplest fix for this case, assuming the name mymodule is globally unique, would be to avoid using relative imports, and just use... 假设名称mymodule在全球范围内是唯一的,这种情况下最简单的解决方法是避免使用相对导入,而只需使用...

from mymodule import as_int

...although, if it's not unique, or your package structure is more complex, you'll need to include the directory containing your package directory in PYTHONPATH , and do it like this... ...尽管它不是唯一的,或者您的程序包结构更复杂,但是您需要在PYTHONPATH包含包含您的程序包目录的目录,并按以下步骤进行操作...

from mypackage.mymodule import as_int

...or if you want it to work "out of the box", you can frob the PYTHONPATH in code first with this... ...或者如果您希望它“开箱即用”工作,则可以使用此方法首先在代码中修改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

It's kind of a pain, but there's a clue as to why in an email written by a certain Guido van Rossum... 这有点痛苦,但是有一个线索可以说明为什么某位Guido van Rossum写的电子邮件中 ...

I'm -1 on this and on any other proposed twiddlings of the __main__ machinery. 我在__main__机器上的任何其他提议上都为-1。 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. 为了让我改变主意,您必须说服我不要。

Whether running scripts inside a package is an antipattern or not is subjective, but personally I find it really useful in a package I have which contains some custom wxPython widgets, so I can run the script for any of the source files to display a wx.Frame containing only that widget for testing purposes. 在程序包中运行脚本是否是反模式是主观的,但是就我个人而言,我发现它在包含一些自定义wxPython小部件的程序包中非常有用,因此我可以为任何源文件运行脚本以显示wx.Frame仅包含该小部件以进行测试的wx.Frame


#3楼

I ran into this issue. 我遇到了这个问题。 A hack workaround is importing via an if/else block like follows: 黑客的解决方法是通过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()

#4楼

Explanation 说明

From PEP 328 PEP 328

Relative imports use a module's __name__ attribute to determine that module's position in the package hierarchy. 相对导入使用模块的__name__属性来确定该模块在包层次结构中的位置。 If the module's name does not contain any package information (eg 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. 如果模块的名称不包含任何软件包信息(例如,将其设置为'__main__'), 则相对导入的解析就好像该模块是顶级模块一样 ,无论该模块实际位于文件系统上的哪个位置。

At some point PEP 338 conflicted with PEP 328 : 在某些时候, PEP 338PEP 328冲突:

... relative imports rely on __name__ to determine the current module's position in the package hierarchy. ...相对导入依赖于__name__来确定当前模块在包层次结构中的位置。 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) 在主模块中, __name__的值始终为'__main__' ,因此显式相对导入将始终失败(因为它们仅适用于包中的模块)

and to address the issue, PEP 366 introduced the top level variable __package__ : 为了解决该问题, 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. 通过添加新的模块级别属性,如果使用-m开关执行模块,则该PEP允许相对导入自动进行。 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. [...]如果存在[属性],则相对导入将基于此属性而不是模块__name__属性。 [...] When the main module is specified by its filename, then the __package__ attribute will be set to None . [...]当通过其文件名指定主模块时, __package__属性将设置为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) [...] 当导入系统在未设置__package__的模块(或将其设置为None)的模块中遇到显式相对导入时,它将计算并存储正确的值__name __。rpartition('。')[0]普通模块__name__包初始化模块)

(emphasis mine) (强调我的)

If the __name__ is '__main__' , __name__.rpartition('.')[0] returns empty string. 如果__name__'__main__' ,则__name__.rpartition('.')[0]返回空字符串。 This is why there's empty string literal in the error description: 这就是为什么错误描述中有空字符串文字的原因:

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

The relevant part of the CPython's PyImport_ImportModuleLevelObject function : 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 raises this exception if it was unable to find package (the name of the package) in interp->modules (accessible as sys.modules ). 如果CPython无法在interp->modules (可通过sys.modules访问)中找到packagepackage的名称), interp->modules引发此异常。 Since sys.modules is "a dictionary that maps module names to modules which have already been loaded" , it's now clear that the parent module must be explicitly absolute-imported before performing relative import . 由于sys.modules“将模块名称映射到已经加载的模块的字典” ,因此现在很清楚, 必须在执行相对导入之前显式绝对导入父模块

Note: The patch from the issue 18018 has added another if block , which will be executed before the code above: 注意: 问题18018中的补丁添加了另一个if ,它将在上面的代码之前执行:

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) {
    ...
*/

If package (same as above) is empty string, the error message will be 如果package (与上面相同)为空字符串,则错误消息将为

ImportError: attempted relative import with no known parent package

However, you will only see this in Python 3.6 or newer. 但是,您只会在Python 3.6或更高版本中看到它。

Solution #1: Run your script using -m 解决方案1:使用-m运行脚本

Consider a directory (which is a Python package ): 考虑一个目录(这是一个Python ):

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

All of the files in package begin with the same 2 lines of code: 软件包中的所有文件均以相同的两行代码开头:

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

I'm including these two lines only to make the order of operations obvious. 我加入这两行只是为了使操作顺序显而易见。 We can ignore them completely, since they don't affect the execution. 我们可以完全忽略它们,因为它们不会影响执行。

__init__.py and module.py contain only those two lines (ie, they are effectively empty). __init__.pymodule.py仅包含这两行(即,它们实际上是空的)。

standalone.py additionally attempts to import module.py via relative import: standalone.py另外尝试通过相对导入来导入module.py

from . import module  # explicit relative import

We're well aware that /path/to/python/interpreter package/standalone.py will fail. 我们很清楚/path/to/python/interpreter package/standalone.py将失败。 However, we can run the module with the -m command line option that will "search sys.path for the named module and execute its contents as the __main__ module" : 但是,我们可以使用-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 does all the importing stuff for you and automatically sets __package__ , but you can do that yourself in the -m为您完成所有导入工作,并自动设置__package__ ,但是您可以在

Solution #2: Set __package__ manually 解决方案2:手动设置__package__

Please treat it as a proof of concept rather than an actual solution. 请把它当作概念证明而不是实际解决方案。 It isn't well-suited for use in real-world code. 它不适合在实际代码中使用。

PEP 366 has a workaround to this problem, however, it's incomplete, because setting __package__ alone is not enough. PEP 366可以解决此问题,但是它不完整,因为仅设置__package__是不够的。 You're going to need to import at least N preceding packages in the module hierarchy, where N is the number of parent directories (relative to the directory of the script) that will be searched for the module being imported. 您将需要至少在模块层次结构中导入N个先前的软件包,其中N是要搜索要导入的模块的父目录(相对于脚本目录)的数量。

Thus, 从而,

  1. Add the parent directory of the Nth predecessor of the current module to sys.path 将当前模块的第N个前辈的父目录添加到sys.path

  2. Remove the current file's directory from sys.path sys.path删除当前文件的目录

  3. Import the parent module of the current module using its fully-qualified name 使用标准名称导入当前模块的父模块

  4. Set __package__ to the fully-qualified name from 2 设置__package__2到完全限定名

  5. Perform the relative import 执行相对导入

I'll borrow files from the Solution #1 and add some more subpackages: 我将从解决方案1中借用文件,并添加更多子包:

package
├── __init__.py
├── module.py
└── subpackage
    ├── __init__.py
    └── subsubpackage
        ├── __init__.py
        └── standalone.py

This time standalone.py will import module.py from the package package using the following relative import 这次standalone.py将使用以下相对导入方式从软件包中导入module.py

from ... import module  # N = 3

We'll need to precede that line with the boilerplate code, to make it work. 我们需要在该行之前加上样板代码,以使其正常工作。

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

It allows us to execute standalone.py by filename: 它允许我们按文件名执行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

A more general solution wrapped in a function can be found here . 包裹在一个功能更通用的解决方案,可以发现在这里 Example usage: 用法示例:

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

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

Solution #3: Use absolute imports and setuptools 解决方案3:使用绝对导入和设置工具

The steps are - 步骤是-

  1. Replace explicit relative imports with equivalent absolute imports 将显式相对导入替换为等效的绝对导入

  2. Install package to make it importable 安装package以使其可导入

For instance, the directory structure may be as follows 例如,目录结构可以如下

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

where setup.py is setup.py在哪里

from setuptools import setup, find_packages
setup(
    name = 'your_package_name',
    packages = find_packages(),
)

The rest of the files were borrowed from the Solution #1 . 其余文件是从解决方案#1借用的。

Installation will allow you to import the package regardless of your working directory (assuming there'll be no naming issues). 安装后,无论您的工作目录如何,都可以导入软件包(假设没有命名问题)。

We can modify standalone.py to use this advantage (step 1): 我们可以修改standalone.py以利用这一优势(步骤1):

from package import module  # absolute import

Change your working directory to project and run /path/to/python/interpreter setup.py install --user ( --user installs the package in your site-packages directory ) (step 2): 将您的工作目录更改为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

Let's verify that it's now possible to run standalone.py as a script: 让我们验证一下现在可以将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'>

Note : If you decide to go down this route, you'd be better off using virtual environments to install packages in isolation. 注意 :如果您决定走这条路,最好使用虚拟环境来隔离安装软件包。

Solution #4: Use absolute imports and some boilerplate code 解决方案4:使用绝对导入和一些样板代码

Frankly, the installation is not necessary - you could add some boilerplate code to your script to make absolute imports work. 坦白地说,不需要安装-您可以在脚本中添加一些样板代码以使绝对导入工作。

I'm going to borrow files from Solution #1 and change standalone.py : 我将从解决方案1借用文件并更改standalone.py

  1. Add the parent directory of package to sys.path before attempting to import anything from package using absolute imports: 尝试使用绝对导入从软件包中导入任何内容之前 ,将软件包的父目录添加到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. Replace the relative import by the absolute import: 用绝对导入替换相对导入:

     from package import module # absolute import 

standalone.py runs without problems: 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'>

I feel that I should warn you: try not to do this, especially if your project has a complex structure. 我认为我应该警告您:请不要这样做, 尤其是在您的项目结构复杂的情况下。


As a side note, PEP 8 recommends the use of absolute imports, but states that in some scenarios explicit relative imports are acceptable: 作为附带说明, 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. [...]但是,显式相对导入是绝对导入的一种可接受的替代方法,尤其是在处理复杂的包装布局时,使用绝对导入会不必要地冗长。


#5楼

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

from example.example import fkt

#6楼

To obviate this problem, I devised a solution with the repackage package, which has worked for me for some time. 为了解决这个问题,我设计了带有重新包装软件包的解决方案,该解决方案已经为我服务了一段时间。 It adds the upper directory to the lib path: 它将上层目录添加到lib路径:

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

Repackage can make relative imports that work in a wide range of cases, using an intelligent strategy (inspecting the call stack). 重新打包可以使用智能策略(检查调用堆栈)进行相对导入,从而在各种情况下都起作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值