相对导入: 千载难逢

2024-04-23 11:34:09 发布

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

我来过这里:

还有很多我没有复制的网址,有些是这样,有些是在其他网站上,回到我以为我能很快找到解决方案的时候。

永远反复出现的问题是:对于Windows7,32位Python2.7.3,我如何解决这个“试图在非包中进行相对导入”的消息?我在pep-0328上做了一个精确的复制品:

package/
    __init__.py
    subpackage1/
        __init__.py
        moduleX.py
        moduleY.py
    subpackage2/
        __init__.py
        moduleZ.py
    moduleA.py

导入是从控制台完成的。

我确实在相应的模块中创建了名为spam和eggs的函数。很自然,它不起作用。答案显然在我列出的第四个网址上,但对我来说都是校友。我访问的其中一个URL上有此响应:

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.

上面的回答看起来很有希望,但对我来说都是象形文字。所以我的问题是,如何使Python不返回给我“试图在非包中进行相对导入”?有一个答案可能涉及-m。

有人能告诉我为什么Python会给出这个错误消息,它的意思是“non-package”,为什么以及如何定义一个“package”,并用一个幼儿园学生能够理解的术语给出精确的答案。


Tags: theto答案pyorgimporthttppackage
3条回答

脚本与模块

这是一个解释。简短的版本是,直接运行Python文件和从其他地方导入该文件之间有很大的区别。仅仅知道文件在哪个目录并不确定Python认为它在哪个包中。这还取决于如何将文件加载到Python中(通过运行或导入)。

加载Python文件有两种方法:作为顶层脚本,或作为 模块。如果直接执行文件,例如在命令行上键入python myfile.py,则该文件将作为顶级脚本加载。如果执行python -m myfile,或者在其他文件中遇到import语句时加载,则将其作为模块加载。一次只能有一个顶级脚本;顶级脚本是您运行以开始工作的Python文件。

命名

加载文件时,将为其指定一个名称(存储在其__name__属性中)。如果它是作为顶级脚本加载的,那么它的名称是__main__。如果它是作为模块加载的,则其名称是文件名,前面是它所属的任何包/子包的名称,用点分隔。

例如在你的例子中:

package/
    __init__.py
    subpackage1/
        __init__.py
        moduleX.py
    moduleA.py

如果导入moduleX(注意:imported,而不是直接执行),则其名称将为package.subpackage1.moduleX。如果导入moduleA,则其名称为package.moduleA。但是,如果从命令行直接运行moduleX,则其名称将改为__main__,如果从命令行直接运行moduleA,则其名称将为__main__。当一个模块作为顶层脚本运行时,它将丢失其正常名称,取而代之的是__main__

非通过其包含包访问模块

还有一个额外的问题:模块的名称取决于它是“直接”从它所在的目录导入的,还是通过包导入的。只有在目录中运行Python并尝试导入同一目录(或其子目录)中的文件时,这才有区别。例如,如果在目录package/subpackage1中启动Python解释器,然后执行import moduleX,那么moduleX的名称将只是moduleX,而不是package.subpackage1.moduleX。这是因为Python在启动时将当前目录添加到其搜索路径中;如果在当前目录中找到要导入的模块,它将不知道该目录是包的一部分,并且包信息将不会成为模块名称的一部分。

一种特殊情况是,如果以交互方式运行解释器(例如,只需键入python,然后开始动态输入Python代码)。在这种情况下,交互会话的名称是__main__

现在,这里是错误消息的关键:如果模块名没有点,则它不被视为包的一部分。文件在磁盘上的实际位置并不重要。重要的是它的名字是什么,它的名字取决于你如何加载它。

现在看看你的问题中的报价:

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.

相对进口量…

相对导入使用模块的名称来确定它在包中的位置。当您使用像from .. import foo这样的相对导入时,这些点表示要在包层次结构中提升一些级别。例如,如果当前模块的名称是package.subpackage1.moduleX,那么..moduleA表示package.moduleA。要使from .. import工作,模块的名称必须至少与import语句中的点一样多。

<强>。。。仅在包中是相对的

但是,如果模块名为__main__,则不认为它在包中。它的名称没有点,因此不能在其中使用from .. import语句。如果您尝试这样做,您将得到“相对导入在非包”错误。

脚本无法导入相对的g>

您可能做的是尝试从命令行运行moduleX或类似的命令。当您这样做时,它的名称被设置为__main__,这意味着它内部的相对导入将失败,因为它的名称不能显示它在包中。请注意,如果从模块所在的同一目录运行Python,然后尝试导入该模块,也会发生这种情况,因为如上所述,Python将在当前目录中“太早”找到该模块,而不会意识到它是包的一部分。

还要记住,当您运行交互式解释器时,该交互式会话的“名称”总是__main__。因此,不能直接从交互式会话进行相对导入。相对导入仅在模块文件中使用。

两种解决方案:

  1. 如果确实要直接运行moduleX,但仍希望将其视为包的一部分,则可以执行python -m package.subpackage1.moduleX-m告诉Python将其作为模块加载,而不是作为顶层脚本加载。

  2. 或者您实际上不想运行运行moduleX,您只想运行一些其他脚本,例如myfile.py,这些脚本moduleX中使用函数。如果是这样的话,将myfile.py放在package目录中的其他位置-而不是并运行它。如果在myfile.py内部进行类似from package.moduleA import spam的操作,它将工作正常。

注释

  • 对于这两种解决方案之一,必须从Python模块搜索路径(sys.path)访问包目录(package)。否则,您将无法可靠地使用包中的任何内容。

  • 自Python 2.6以来,用于包解析目的的模块“名称”不仅由其__name__属性确定,还由__package__属性确定。这就是为什么我避免使用显式符号__name__来引用模块的“名称”。因为Python2.6模块的“名称”实际上是__package__ + '.' + __name__,或者如果__package__None,则只是__name__。)

这在python中确实是个问题。混淆的根源在于人们错误地将相对重要性视为路径相对性,而路径相对性则不是。

例如,当您在faa.py中编写时:

from .. import foo

只有在python在执行期间将faa.py标识并加载为包的一部分时,这才有意义。在这种情况下,模块的名称 对于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

这里有一个通用的方法,作为一个例子进行了修改,我现在使用它来处理作为包编写的Python库,其中包含相互依赖的文件,我希望能够在其中逐段测试其中的部分。让我们称之为lib.foo,并说它需要访问lib.fileA函数f1f2,以及lib.fileBClass3

我已经包含了一些print调用来帮助说明这是如何工作的。实际上,您需要删除它们(也可以删除from __future__ import print_function行)。

当我们真正需要在sys.path中插入一个条目时,这个特定的示例太简单了,无法显示。(请参见Lars' answer了解我们需要它的情况,当我们有两个或更多级别的包目录时,然后我们使用os.path.dirname(os.path.dirname(__file__))-但它也不会真正伤害到^{。)在没有if _i in sys.path测试的情况下这样做也足够安全。但是,如果每个导入的文件都插入相同的路径,例如,如果fileAfileB都希望从包中导入实用程序,则会多次用相同的路径将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)

这里的想法是这样的(注意,在python2.7和python 3.x中,所有这些函数都是相同的):

  1. 如果作为import libfrom lib import foo作为从普通代码导入的常规包运行,__packagelib__name__lib.foo。我们采用第一个代码路径,从.fileA等处导入。

  2. 如果以python lib/foo.py运行,__package__将为None,__name__将为__main__

    我们走第二条代码路径。lib目录已经在sys.path中,因此不需要添加它。我们从fileA等处导入。

  3. 如果在lib目录中作为python foo.py运行,则行为与案例2相同。

  4. 如果在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__'

也就是说,该模块被导入两次,一次通过包,然后再次作为__main__导入,以便它运行main代码。Python3.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__'

警告是新的,但对行为的警告不是。它是某些人所说的the double import trap的一部分。(更多细节见issue 27487)尼克·科格兰说:

This next trap exists in all current versions of Python, including 3.3, and can be summed up in the following general guideline: "Never add a package directory, or any directory inside a package, directly to the Python path".

注意,虽然我们违反了这里的规则,但是我们只在加载的文件是作为包的一部分加载而不是作为包的一部分加载时才执行操作,而且我们的修改是专门为允许我们访问包中的其他文件而设计的。(而且,正如我所指出的,我们可能根本不应该为单级包这样做。)如果我们想变得更干净,我们可以将其重写为,例如:

    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)。

相关问题 更多 >