相对进口量达十亿次
- 2024-11-15 08:36:00
- admin 原创
- 16
问题描述:
我来过这里:
PEP 328 – 导入:多行和绝对/相对
模块、包
Python 包:相对导入
Python 相对导入示例代码不起作用
Python 2.5 中的相对导入
Python 中的相对导入
Python:禁用相对导入
还有许多我没有复制的 URL,一些在 SO 上,一些在其他网站上,当时我以为我很快就能找到解决方案。
永远重复出现的问题是:我该如何解决这个“尝试在非包中进行相对导入”消息?
ImportError:尝试相对导入,但没有已知父包
我在 pep-0328 上构建了该包的精确副本:
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py
导入是从控制台完成的。
我确实在相应的模块中创建了名为 spam 和 eggs 的函数。当然,它没有起作用。答案显然在我列出的第 4 个 URL 中,但对我来说都是校友。我访问的其中一个 URL 上有这样的响应:
相对导入使用模块的名称属性来确定模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,它被设置为“main”),则相对导入将被解析为模块是顶级模块,而不管模块在文件系统上的实际位置。
上面的回复看起来很有希望,但对我来说都是象形文字。如何让 Python 不返回“尝试在非包中进行相对导入”?-m
据说它有一个涉及的答案。
Python 为何会给出该错误消息?“非软件包”是什么意思?为什么以及如何定义“软件包”?
解决方案 1:
脚本与模块
以下是解释。简而言之,直接运行 Python 文件和从其他地方导入该文件之间存在很大差异。 仅仅知道文件位于哪个目录并不能确定 Python 认为它在哪个包中。 这还取决于您如何将文件加载到 Python 中(通过运行或导入)。
有两种方法可以加载 Python 文件:作为顶级脚本或作为模块。如果您直接执行文件(例如通过python myfile.py
在命令行中键入),则文件将作为顶级脚本加载。当import
在其他文件中遇到语句时,它将作为模块加载。一次只能有一个顶级脚本;顶级脚本是您运行以启动程序的 Python 文件。
命名
当文件被加载时,会为其指定一个名称(存储在其__name__
属性中)。如果它是作为顶级脚本加载的,则其名称为__main__
。如果它是作为模块加载的,则其名称为文件名,前面是它所属的任何包/子包的名称,以点分隔。
例如在您的例子中:
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleA.py
如果你导入了moduleX
(注意:导入了,而不是直接执行),它的名字将是package.subpackage1.moduleX
。如果你导入了moduleA
,它的名字将是package.moduleA
。但是,如果你直接 moduleX
从命令行运行,它的名字将是__main__
,如果你直接moduleA
从命令行运行,它的名字将是__main__
。当模块作为顶级脚本运行时,它会失去其正常名称,而其名称将是__main__
。
不通过包含模块的包来访问模块
还有一个问题:模块的名称取决于它是从所在目录“直接”导入的还是通过包导入的。只有当您在目录中运行 Python,并尝试导入同一目录(或其子目录)中的文件时,这才会有所不同。例如,如果您在目录中启动 Python 解释器package/subpackage1
,然后执行import moduleX
, 的名称moduleX
将只是moduleX
,而不是package.subpackage1.moduleX
。这是因为当以交互方式进入解释器时,Python 会将当前目录添加到其搜索路径中;如果它在当前目录中找到要导入的模块,它将不知道该目录是包的一部分,并且包信息不会成为模块名称的一部分。
一种特殊情况是,如果您以交互方式运行解释器(例如,只需键入python
并开始即时输入 Python 代码)。在这种情况下,该交互式会话的名称是__main__
。
现在,这是错误消息的关键:如果模块的名称没有点,则不将其视为包的一部分。文件在磁盘上的实际位置并不重要。重要的是它的名称是什么,而它的名称取决于您如何加载它。
现在看一下你在问题中引用的话:
相对导入使用模块的名称属性来确定该模块在软件包层次结构中的位置。如果模块的名称不包含任何软件包信息(例如,它被设置为“main”),则相对导入将被解析为该模块是顶级模块,而不管该模块在文件系统上的实际位置。
相对进口...
相对导入使用模块的名称来确定它在包中的位置。当您使用相对导入(如)时from .. import foo
,点表示要在包层次结构中提升一定级别的级别。例如,如果您当前的模块名称是package.subpackage1.moduleX
,则..moduleA
意味着package.moduleA
。要使from .. import
正常工作,模块名称必须至少包含与语句中一样多的点import
。
... 仅在包中是相对的
但是,如果您的模块名称为__main__
,则它不被视为在包中。其名称中没有点,因此您无法from .. import
在其中使用语句。如果您尝试这样做,您将收到“非包中的相对导入”错误。
脚本无法导入相对
您可能尝试从命令行运行或类似操作moduleX
。执行此操作时,其名称设置为__main__
,这意味着其中的相对导入将失败,因为其名称未表明它位于包中。请注意,如果您从模块所在的同一目录运行 Python,然后尝试导入该模块,也会发生这种情况,因为如上所述,Python 会“过早”在当前目录中找到该模块,而没有意识到它是包的一部分。
还要记住,当您运行交互式解释器时,该交互式会话的“名称”始终是__main__
。因此,您不能直接从交互式会话进行相对导入。相对导入仅供在模块文件内使用。
两种解决方案:
如果你确实想
moduleX
直接运行,但仍希望将其视为包的一部分,则可以执行python -m package.subpackage1.moduleX
。-m
告诉 Python 将其作为模块加载,而不是作为顶级脚本加载。或者也许您实际上并不想运行
moduleX
,您只想运行其他脚本,例如myfile.py
,该脚本使用中的函数moduleX
。如果是这种情况,请将其放在myfile.py
其他地方(不是目录内package
)并运行它。如果myfile.py
您在里面执行类似 的操作from package.moduleA import spam
,它将正常工作。
笔记
对于这两种解决方案,包目录(
package
在您的示例中)必须可以从 Python 模块搜索路径(sys.path
)访问。如果不能,您将根本无法可靠地使用包中的任何内容。自 Python 2.6 起,用于包解析的模块“名称”不仅由其
__name__
属性决定,还由__package__
属性决定。这就是为什么我避免使用显式符号__name__
来引用模块的“名称”。自 Python 2.6 起,模块的“名称”实际上是__package__ + '.' + __name__
,或者只是__name__
如果__package__
是None
。)
解决方案 2:
这确实是 Python 内部的一个问题。造成混淆的原因是人们错误地将相对导入视为路径相对,但事实并非如此。
例如当你在faa.py中写入:
from .. import foo
仅当faa.py在执行期间被python识别并加载为包的一部分时,这才有意义。在这种情况下, faa.py
的模块名称将是例如some_packagename.faa。如果文件只是因为位于当前目录中而被加载,则在运行 python 时,其名称不会引用任何包,最终相对导入将失败。
引用当前目录中的模块的一个简单解决方案是使用以下命令:
if __package__ is None or __package__ == '':
# uses current directory visibility
import foo
else:
# uses current package visibility
from . import foo
解决方案 3:
外语的长答案太多了。所以我会尽量写得简短些。
如果您写的from . import module
与您想的相反,module
则不会从当前目录导入,而是从包的顶层导入!如果您将.py文件作为脚本运行,它根本不知道顶层在哪里,因此拒绝工作。
如果你像这样py -m package.module
从上面的目录启动它package
,那么 Python 就知道顶层在哪里。这与 Java 非常相似:java -cp bin_directory package.class
解决方案 4:
因此,在与其他人一起抱怨了这个问题之后,我偶然发现了Dorian B在这篇文章中发布的一条注释,它解决了我遇到的具体问题,即我将开发用于 Web 服务的模块和类,但我还希望能够在编码时使用 PyCharm 中的调试器功能对它们进行测试。要在自包含类中运行测试,我将在类文件的末尾包含以下内容:
if __name__ == '__main__':
# run test code here...
但是如果我想导入同一文件夹中的其他类或模块,那么我必须将所有导入语句从相对表示法更改为本地引用(即删除点 (.))但在阅读 Dorian 的建议后,我尝试了他的“一行代码”,它成功了!现在我可以在 PyCharm 中进行测试,当我在另一个被测类中使用该类时,或者当我在我的 Web 服务中使用该类时,我可以保留我的测试代码!
# import any site-lib modules first, then...
import sys
parent_module = sys.modules['.'.join(__name__.split('.')[:-1]) or '__main__']
if __name__ == '__main__' or parent_module.__name__ == '__main__':
from codex import Codex # these are in same folder as module under test!
from dblogger import DbLogger
else:
from .codex import Codex
from .dblogger import DbLogger
if 语句检查我们是否将此模块作为main运行,或者它是否在另一个作为main测试的模块中使用。也许这是显而易见的,但我在这里提供这条说明,以防其他因上述相对导入问题而感到沮丧的人可以利用它。
解决方案 5:
这是一个我不推荐的解决方案,但在某些模块根本无法生成的情况下可能会有用:
import os
import sys
parent_dir_name = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
sys.path.append(parent_dir_name + "/your_dir")
import your_script
your_script.a_function()
解决方案 6:
这是一个通用的配方,经过修改以适合作为示例,我现在用它来处理以包形式编写的 Python 库,其中包含相互依赖的文件,我希望能够逐个测试它们的各个部分。我们调用它lib.foo
并假设它需要访问lib.fileA
函数f1
和f2
,以及lib.fileB
类Class3
。
我添加了几个print
调用来帮助说明其工作原理。实际上,您可能希望删除它们(可能还有该from __future__ import print_function
行)。
这个特定示例过于简单,无法展示我们何时真正需要将条目插入sys.path
。(请参阅Lars 的回答,了解我们确实需要它的情况,即当我们拥有两级或更多级的包目录时,然后我们使用os.path.dirname(os.path.dirname(__file__))
— 但在这里它实际上也没有什么坏处。)无需测试即可安全地执行此操作if _i in sys.path
。但是,如果每个导入的文件都插入相同的路径(例如,如果fileA
和fileB
都想从包中导入实用程序),这会因sys.path
多次使用相同的路径而变得混乱,因此最好if _i not in sys.path
在样板中使用 。
from __future__ import print_function # only when showing how this works
if __package__:
print('Package named {!r}; __name__ is {!r}'.format(__package__, __name__))
from .fileA import f1, f2
from .fileB import Class3
else:
print('Not a package; __name__ is {!r}'.format(__name__))
# these next steps should be used only with care and if needed
# (remove the sys.path manipulation for simple cases!)
import os, sys
_i = os.path.dirname(os.path.abspath(__file__))
if _i not in sys.path:
print('inserting {!r} into sys.path'.format(_i))
sys.path.insert(0, _i)
else:
print('{!r} is already in sys.path'.format(_i))
del _i # clean up global name space
from fileA import f1, f2
from fileB import Class3
... all the code as usual ...
if __name__ == '__main__':
import doctest, sys
ret = doctest.testmod()
sys.exit(0 if ret.failed == 0 else 1)
这里的想法是这样的(请注意,这些在 Python 2.7 和 Python 3.x 中的功能都相同):
如果作为
import lib
或from lib import foo
作为常规包从普通代码导入运行,__package
则为lib
且__name__
为lib.foo
。我们采用第一个代码路径,从 导入.fileA
,等等。如果以 方式运行
python lib/foo.py
,__package__
则将为 None ,并且__name__
将为__main__
。
我们采用第二个代码路径。lib
目录已经存在,sys.path
因此无需添加。我们从 导入fileA
,等等。
如果在
lib
目录中运行python foo.py
,则行为与情况 2 相同。如果在
lib
目录中以运行python -m foo
,则行为类似于情况 2 和 3。但是,lib
目录的路径不在 中sys.path
,因此我们在导入之前添加它。如果我们运行 Python 然后 ,情况也是如此import foo
。
(由于.
在中sys.path
,我们实际上不需要在这里添加路径的绝对版本。这是我们想要做的更深的包嵌套结构from ..otherlib.fileC import ...
产生影响的地方。如果你不这样做,你可以sys.path
完全省略所有的操作。)
笔记
还有一个怪癖。如果你从外部运行整个过程:
python2 lib.foo
或者:
python3 lib.foo
行为取决于 的内容lib/__init__.py
。如果 存在且为空,则一切正常:
Package named 'lib'; __name__ is '__main__'
但是如果lib/__init__.py
它本身导入routine
以便可以routine.name
直接导出为lib.name
,则会得到:
python2 lib.foo
输出:
Package named 'lib'; __name__ is 'lib.foo'
Package named 'lib'; __name__ is '__main__'
也就是说,模块被导入两次,一次通过包,然后再次通过 as 导入__main__
,以便运行您的main
代码。Python 3.6 及更高版本对此发出警告:
python3 lib.routine
输出:
Package named 'lib'; __name__ is 'lib.foo'
[...]/runpy.py:125: RuntimeWarning: 'lib.foo' found in sys.modules
after import of package 'lib', but prior to execution of 'lib.foo';
this may result in unpredictable behaviour
warn(RuntimeWarning(msg))
Package named 'lib'; __name__ is '__main__'
警告是新的,但警告行为却不是。这是一些人所说的双重导入陷阱的一部分。(有关更多详细信息,请参阅问题 27487。)Nick Coghlan 说:
下一个陷阱存在于所有当前的 Python 版本中,包括 3.3,并且可以总结为以下一般准则:“永远不要将包目录或包内的任何目录直接添加到 Python 路径中”。
请注意,虽然我们在这里违反了该规则,但我们仅在加载的文件未作为包的一部分加载时才这样做,并且我们的修改专门用于允许我们访问该包中的其他文件。 (并且,正如我所指出的,对于单级包,我们可能根本不应该这样做。)如果我们想要更加干净,我们可以将其重写为,例如:
import os, sys
_i = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _i not in sys.path:
sys.path.insert(0, _i)
else:
_i = None
from sub.fileA import f1, f2
from sub.fileB import Class3
if _i:
sys.path.remove(_i)
del _i
也就是说,我们进行sys.path
足够长的时间修改以实现我们的导入,然后将其恢复到原来的状态(_i
当且仅当我们添加了一份副本时才删除一份副本_i
)。
解决方案 7:
@BrenBarn 的回答说明了一切,但如果你和我一样,可能需要一段时间才能理解。以下是我的案例以及 @BrenBarn 的回答如何适用于它,也许它会对你有所帮助。
案件
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleA.py
使用我们熟悉的示例,并向其中添加 moduleX.py 具有相对于 ..moduleA 的相对导入。鉴于我尝试在导入 moduleX 的 subpackage1 目录中编写测试脚本,但随后出现了 OP 描述的可怕错误。
解决方案
将测试脚本移至与包相同的级别并导入 package.subpackage1.moduleX
解释
如上所述,相对导入是相对于当前名称进行的。当我的测试脚本从同一目录导入 moduleX 时,moduleX 内的模块名称为 moduleX。当遇到相对导入时,解释器无法备份包层次结构,因为它已经位于顶部
当我从上面导入 moduleX 时,moduleX 内部的名称是 package.subpackage1.moduleX,并且可以找到相对导入
解决方案 8:
我遇到过类似的问题,我不想更改 Python 模块搜索路径,而需要从脚本中相对地加载模块(尽管“脚本不能全部导入相对模块”,正如BrenBarn 很好地解释的那样)。
所以我使用了以下 hack。不幸的是,它依赖于imp
自版本 3.4 以来被弃用的模块,该模块被删除以支持importlib
。(这也适用于importlib
吗?我不知道。)不过,该 hack 目前有效。
从文件夹中的脚本moduleX
访问成员的示例:subpackage1
`subpackage2`
#!/usr/bin/env python3
import inspect
import imp
import os
def get_script_dir(follow_symlinks=True):
"""
Return directory of code defining this very function.
Should work from a module as well as from a script.
"""
script_path = inspect.getabsfile(get_script_dir)
if follow_symlinks:
script_path = os.path.realpath(script_path)
return os.path.dirname(script_path)
# loading the module (hack, relying on deprecated imp-module)
PARENT_PATH = os.path.dirname(get_script_dir())
(x_file, x_path, x_desc) = imp.find_module('moduleX', [PARENT_PATH+'/'+'subpackage1'])
module_x = imp.load_module('subpackage1.moduleX', x_file, x_path, x_desc)
# importing a function and a value
function = module_x.my_function
VALUE = module_x.MY_CONST
一种更简洁的方法似乎是修改Federico 提到的用于加载模块的 sys.path 。
#!/usr/bin/env python3
if __name__ == '__main__' and __package__ is None:
from os import sys, path
# __file__ should be defined in this case
PARENT_DIR = path.dirname(path.dirname(path.abspath(__file__)))
sys.path.append(PARENT_DIR)
from subpackage1.moduleX import *
解决方案 9:
如果您不想执行任何以下操作,这里有一个非常简单的解决方案:
添加
__init__.py
文件运行
python -m mymodule
编辑
__package__
添加
if
签到__main__
sys.path
手工编辑编辑
PYTHONPATH
重组项目
pip 安装 importmonkey
这是 sys.path hack 的强大包装器,可以让一切保持简单和整洁。
[ github ] [ pip ] [ docs ]
├─ src
│ └─ project
│ └─ mymodule.py
└─ test
└─ test.py
# In test.py
from importmonkey import add_path
add_path("../src/project") # relative to current __file__
import mymodule
# add as many paths as needed, absolute or relative
# unix path conventions work so you can use '..' and '.'
# add_path validates the paths and returns added path as string
该模块无需__init__.py
与其关联即可工作。
隶属关系披露:我制作了 importmonkey。
解决方案 10:
根据 Lars 的建议,我将这种方法包装在一个实验性的新导入库中:ultraimport
它使程序员能够更好地控制导入,并允许基于文件系统的导入。因此,您可以从脚本中进行相对导入。父包不是必需的。ultraimports 始终有效,无论您如何运行代码或当前工作目录是什么,因为 ultraimport 使导入变得明确。您不需要更改 sys.path,也不需要 try/except 块来有时进行相对导入,有时进行绝对导入。
然后你可以在 somefile.py 中写入类似以下内容:
import ultraimport
foo = ultraimport('__dir__/foo.py')
dir 是 somefile.py 的目录,ultraimport() 的调用者。foo.py 将与 somefile.py 位于同一目录中。
导入此类脚本时需要注意的一个问题是,如果它们包含进一步的相对导入。ultraimport 有一个内置预处理器,可以将后续的相对导入重写为 ultraimport,以便它们继续工作。不过,目前此功能有些受限,因为原始 Python 导入不明确,而且您能做的非常有限。
解决方案 11:
__name__
根据相关代码是在全局命名空间中运行还是作为导入模块的一部分运行而变化。
如果代码不在全局空间中运行,__name__
则为模块的名称。如果它在全局命名空间中运行——例如,如果您将其输入到控制台中,或者使用脚本运行模块,python.exe yourscriptnamehere.py
则__name__
变为"__main__"
。
您会看到很多 Python 代码用于 if __name__ == '__main__'
测试代码是否从全局命名空间运行 - 这允许您拥有一个兼作脚本的模块。
您是否尝试过从控制台进行这些导入?
解决方案 12:
相对导入使用模块的名称属性来确定模块在包层次结构中的位置。如果模块的名称不包含任何包信息(例如,它被设置为“main”),则相对导入将被解析为模块是顶级模块,而不管模块在文件系统上的实际位置。
我为PyPI编写了一个小型 Python 包,可能对这个问题的查看者有所帮助。如果希望能够在包/项目内运行包含上层包的导入的 Python 文件,而无需直接进入导入文件的目录,则该包可以作为解决方法。
解决方案 13:
对于像我这样可怜的人来说,这个方法根本行不通,以下是FEMista发布的解决方案
添加一个中间父文件夹,作为两个兄弟文件夹的公共分支:
package/
__init__.py
SUBPACKAGES/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py
记得将文件夹添加SUBPACKAGES
到import
路径中。
解决方案 14:
我将展示一个文件夹结构,它可以从同一文件夹、子文件夹和父文件夹进行相对导入。文件结构如下所示:
.
├── main.py
└── bardir/
├── __init__.py
├── bar.py
└── foodir/
├── __init__.py
├── foo.py
├── foo2.py
└── bazdir/
├── __init__.py
└── baz.py
选择此文件夹结构,因此foo.py
首先调用它并按顺序显示 foo、foo2、bar 和 baz。同时显示相对导入的三种方式。
内容main.py
:
print('__name__:',__name__)
print('__package__:', __package__)
print('main')
from bardir.foodir import foo
内容foo.py
:
print(' __name__:',__name__)
print(' __package__:', __package__)
print(' foo')
from . import foo2
from .. import bar
from . bazdir import baz
每个__init__
文件都是空的,其他文件包含类似的打印语句。运行时,main.py
您将获得以下输出:
__name__: __main__
__package__: None
main
__name__: bardir.foodir.foo
__package__: bardir.foodir
foo
__name__: bardir.foodir.foo2
__package__: bardir.foodir
foo2
__name__: bardir.bar
__package__: bardir
bar
__name__: bardir.foodir.bazdir.baz
__package__: bardir.foodir.bazdir
baz
您可以使用以下规则来推理此类文件结构:
相对导入是相对于包变量进行的。一个点表示相对于 进行查看
__package__
。两个点表示相对于第一个点(从右侧)之前的包进行查看。三个点表示相对于第二个点之前的包进行查看。__package__
不包含点?您无法使用..
,并且会收到错误。您的__package__
等于 吗None
?您无法进行相对导入!只要有
__init__.py
文件,文件夹就会变成包。init 文件可能为空。如果直接运行脚本,即按下 IDE 中的运行按钮,变量
__name__
将被设置为__main__
,__package__
变量将被设置为None
。这意味着使用点的相对导入将不起作用。
解决方案 15:
大多数情况下,当我看到ValueError: attempted relative import beyond top-level package
并拔掉头发时,解决方案如下:
您需要在文件层次结构中迈向更高的级别!
#dir/package/module1/foo.py
#dir/package/module2/bar.py
from ..module1 import foo
bar.py
尽管导入过程从未超出当前目录,但在启动解释器时进行导入仍会dir/package/
导致错误。
bar.py
当解释器启动时导入就会dir/
成功。
对于单元测试来说也是如此:python3 -m unittest discover --start-directory=.
从 可以成功运行dir/
,但从 则不行dir/package/
。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件