【问题标题】:beyond top level package error in relative import超出相对导入中的顶级包错误
【发布时间】:2021-03-21 12:41:12
【问题描述】:

这里似乎已经有很多关于 python 3 中的相对导入的问题,但是在经历了许多问题之后,我仍然没有找到我的问题的答案。 所以这就是问题所在。

我有一个如下所示的包裹

package/
   __init__.py
   A/
      __init__.py
      foo.py
   test_A/
      __init__.py
      test.py

我在 test.py 中有一行:

from ..A import foo

现在,我在package的文件夹中,我运行

python -m test_A.test

我收到消息

"ValueError: attempted relative import beyond top-level package"

但如果我在package 的父文件夹中,例如,我运行:

cd ..
python -m package.test_A.test

一切都很好。

现在我的问题是: 当我在package的文件夹中,并且我以test_A.test运行test_A子包中的模块时,根据我的理解,..A只上升了一级,仍然在package文件夹内,为什么它给出消息说beyond top-level package。导致此错误消息的具体原因是什么?

【问题讨论】:

  • 我这里有个想法,所以当 test_A.test 作为模块运行时,'..' 超出了 test_A,这已经是导入 test_A.test 的最高级别了,我认为包级别是不是目录级别,而是您导入包的级别。
  • 我保证看完这个回答stackoverflow.com/a/14132912/8682868,你会明白关于相对导入的一切。
  • 有没有办法避免进行相对导入?比如 Eclipse 中的 PyDev 如何查看 /src? 中的所有包
  • 您的工作目录是否也有一个 init.py?

标签: python import package


【解决方案1】:

编辑:在其他问题中有更好/更连贯的答案:


为什么不起作用?这是因为 python 不记录从哪里加载包。所以当你做python -m test_A.test时,它基本上只是丢弃了test_A.test实际上存储在package中的知识(即package不被视为一个包)。尝试from ..A import foo 正在尝试访问它不再拥有的信息(即加载位置的同级目录)。它在概念上类似于在math 中的文件中允许from ..os import path。这会很糟糕,因为您希望包是不同的。如果他们需要使用另一个包中的某些东西,那么他们应该使用from os import path 全局引用它们,并让python 使用$PATH$PYTHONPATH 来解决它。

当您使用python -m package.test_A.test 时,使用from ..A import foo 可以很好地解析,因为它会跟踪package 中的内容,并且您只是在访问已加载位置的子目录。

为什么 python 不将当前工作目录视为一个包? 没有线索,但天哪,它会很有用。

【讨论】:

  • 我已经编辑了我的答案,以引用一个更好的答案来回答一个相同的问题。只有解决方法。我真正看到的唯一工作是 OP 所做的,即使用 -m 标志并从上面的目录运行。
  • 需要注意的是,来自Multihunter给出的链接的this answer不涉及sys.path hack,而是setuptools的使用,我认为这更有趣。
  • 所以相对导入中的“..”不是我们通常认为的os上下文中的“..”?在 os 中,.. 只是意味着跳出当前目录并移动到父目录,这在 python 的相对导入中不是这种情况。正如您所提到的,解释丢弃了当前工作目录的包信息。对我来说,作为一个习惯于操作系统路径概念的开发人员,这有点不直观:-(
【解决方案2】:
import sys
sys.path.append("..") # Adds higher directory to python modules path.

试试这个。 为我工作。

【讨论】:

  • 嗯...这将如何工作?每个测试文件都会有这个?
  • 在添加 sys.path.append("..") 后,我必须从“from ..A import...”中删除 ..
  • 如果脚本从它存在的目录之外执行,这将不起作用。相反,您必须将此答案调整为specify absolute path of the said script
  • 这是最好的,最简单的选择
  • @AlexR,我宁愿说这是最简单的短期解决方案,但肯定不是最好的。
【解决方案3】:

假设:
如果您在package 目录中,则Atest_A 是单独的包。

结论:
..A 只允许在包中导入。

补充说明:
如果您想强制将包放置在位于sys.path 的任何路径上,则使相对导入仅在包中可用是很有用的。

编辑:

只有我一个人认为这很疯狂!?为什么当前的工作目录不被认为是一个包? – Multihunter

当前工作目录通常位于 sys.path 中。因此,那里的所有文件都是可导入的。这是自 Python 2 以来包尚不存在时的行为。使运行目录成为一个包将允许将模块导入为“import .A”和“import A”,这将是两个不同的模块。也许这是一个需要考虑的不一致之处。

【讨论】:

  • 只有我一个人认为这很疯狂!?为什么运行目录不被认为是一个包?
  • 这不仅很疯狂,而且没有帮助......那么你如何运行测试呢? 显然是 OP 所问的问题,以及为什么我敢肯定很多人也在这里。
  • 运行目录通常位于sys.path中。因此,那里的所有文件都是可导入的。这是自 Python 2 以来包尚不存在时的行为。 - 编辑答案。
  • 我不遵循不一致之处。 python -m package.test_A.test 的行为似乎符合我们的要求,我的论点是这应该是默认值。那么,你能给我举个这种不一致的例子吗?
  • 我其实在想,有没有这方面的功能要求?这确实很疯狂。 C/C++ 风格#include 会非常有用!
【解决方案4】:

这些解决方案在 3.6 中都不适合我,其文件夹结构如下:

package1/
    subpackage1/
        module1.py
package2/
    subpackage2/
        module2.py

我的目标是从模块 1 导入模块 2。奇怪的是,最终对我有用的是:

import sys
sys.path.append(".")

请注意单点解决方案,而不是到目前为止提到的两点解决方案。


编辑:以下内容帮助我澄清了这一点:

import os
print (os.getcwd())

在我的例子中,工作目录(意外地)是项目的根目录。

【讨论】:

  • 它在本地工作但不能在 aws ec2 实例上工作,这有什么意义吗?
  • 这也适用于我——在我的情况下,工作目录同样是项目根目录。我正在使用编程编辑器 (TextMate) 中的运行快捷方式
  • @thebeancounter 一样!在我的 mac 上本地工作,但在 ec2 上不起作用,然后我意识到我在 ec2 的子目录中运行命令并在本地以 root 运行它。一旦我在 ec2 上从 root 运行它,它就可以工作了。
  • sys.path.append(".") 有效,因为您在父目录中调用它,请注意. 始终代表您在其中运行 python 命令的目录。
  • 这被称为“巧合编程”,这绝对是可怕的。不要仅仅因为代码有效而做你不理解的事情。这个答案有这么多票,真是太糟糕了。
【解决方案5】:

这在 Python 中非常棘手

我将首先评论您为什么会遇到这个问题,然后我会提到两种可能的解决方案。

发生了什么事?

您必须考虑 Python documentation 中的这一段:

请注意,相对导入是基于当前的名称 模块。由于主模块的名称总是“ma​​in”, 用作 Python 应用程序主模块的模块 必须始终使用绝对导入。

还有来自PEP 328的以下内容:

相对导入使用模块的 name 属性来确定 模块在包层次结构中的位置。如果模块的名称 不包含任何包信息(例如,它设置为 'ma​​in') 然后解析相对导入,就好像模块是顶级的 模块,无论模块实际位于文件的哪个位置 系统。

相对导入通过文件名(__name__ 属性)起作用,它可以采用两个值:

  1. 它是文件名,前面是文件夹结构,用点分隔。 例如:package.test_A.test 这里 Python 知道父目录:在 test 之前是 test_A,然后是 package因此您可以使用点符号进行相对导入。
#  package.test_A/test.py
from ..A import foo

然后您可以在根目录中拥有一个调用test.py 的根文件:

#  root.py
from package.test_A import test
  1. 当你直接运行模块(test.py)时,它成为程序的入口点,所以__name__ == __main__。文件名没有指示目录结构,所以 Python 不知道如何在目录中上去。对于 Python,test.py 成为顶级脚本,上面没有任何内容。这就是您不能使用相对导入的原因。

可能的解决方案

A) 解决此问题的一种方法是创建一个调用模块/包的根文件(在根目录中),如下所示:

  • root.py 导入 test.py。 (入口点,__name__ == __main__)。
  • test.py(相对)导入 foo.py
  • foo.py 表示模块已导入。

输出是:

package.A.foo has been imported
Module's name is:  package.test_A.test

B)如果您想将代码作为模块而不是作为顶级脚本执行,您可以从命令行尝试:

python -m package.test_A.test

欢迎提出任何建议。

您还应该检查:Relative imports for the billionth time,特别是 BrenBarn 的回答。

【讨论】:

    【解决方案6】:

    from package.A import foo

    我觉得比

    import sys
    sys.path.append("..")
    

    【讨论】:

    • 它确实更具可读性,但仍需要sys.path.append("..")。在 python 3.6 上测试
    • 与旧答案相同
    【解决方案7】:

    正如最受欢迎的答案所暗示的那样,基本上是因为您的 PYTHONPATHsys.path 包含 . 但不是您的包裹路径。并且相对导入是相对于您当前的工作目录,而不是发生导入的文件;奇怪。

    您可以通过首先将相对导入更改为绝对导入,然后以以下方式启动它来解决此问题:

    PYTHONPATH=/path/to/package python -m test_A.test
    

    或以这种方式调用时强制使用 python 路径,因为:

    使用python -m test_A.test,您正在使用__name__ == '__main__'__file__ == '/absolute/path/to/test_A/test.py' 执行test_A/test.py

    这意味着在test.py 中,您可以在主要情况下使用绝对的import 半保护,并且还可以进行一些一次性 Python 路径操作:

    from os import path
    …
    def main():
    …
    if __name__ == '__main__':
        import sys
        sys.path.append(path.join(path.dirname(__file__), '..'))
        from A import foo
    
        exit(main())
    

    【讨论】:

      【解决方案8】:

      编辑:2020-05-08:似乎我引用的网站不再由撰写建议的人控制,因此我删除了指向该网站的链接。感谢您让我知道 baxx。


      如果有人在已经提供了很好的答案后仍然有点挣扎,我在一个不再可用的网站上找到了建议。

      我提到的网站的基本引用:

      "同样可以通过编程方式指定:

      导入系统

      sys.path.append('..')

      当然上面的代码必须在其他导入之前写好 声明。

      很明显,它必须是这样,事后考虑。我试图在我的测试中使用 sys.path.append('..') ,但遇到了 OP 发布的问题。通过在我的其他导入之前添加导入和 sys.path 定义,我能够解决问题。

      【讨论】:

        【解决方案9】:

        如果您在上层文件夹中有__init__.py,则可以将导入初始化为 import file/path as alias 在该初始化文件中。然后你可以在较低的脚本上使用它:

        import alias
        

        【讨论】:

          【解决方案10】:

          这实际上比其他答案要简单得多。

          TL;DR:直接导入 A 而不是尝试相对导入。

          当前工作目录不是一个包,除非您从其他文件夹导入文件夹package。因此,如果您打算将其导入其他应用程序,您的包的行为将正常工作。不工作的是测试......

          无需更改目录结构中的任何内容,只需更改test.py 导入foo.py 的方式。

          from A import foo
          

          现在从package 目录运行python -m test_A.test 将在没有ImportError 的情况下运行。

          为什么会这样?

          您当前的工作目录不是一个包,但它添加到了路径中。因此,您可以直接导入文件夹A 及其内容。这与您可以导入已安装的任何其他软件包的原因相同......它们都包含在您的路径中。

          【讨论】:

            【解决方案11】:

            就我而言,我不得不改为: 解决方案1(更好,取决于当前的py文件路径。易于部署) 使用pathlib.Path.parents make code cleaner

            import sys
            import os
            import pathlib
            target_path = pathlib.Path(os.path.abspath(__file__)).parents[3]
            sys.path.append(target_path)
            from utils import MultiFileAllowed
            

            解决方案 2

            import sys
            import os
            sys.path.append(os.getcwd())
            from utils import MultiFileAllowed
            

            【讨论】:

              【解决方案12】:

              以我的拙见,我是这样理解这个问题的:

              [CASE 1] 当你开始像这样的绝对导入时

              python -m test_A.test
              

              import test_A.test
              

              from test_A import test
              

              您实际上将 import-anchor 设置为 test_A,换句话说,顶级包是 test_A 。所以,当我们有 test.py 做from ..A import xxx 时,你就是在逃离锚点,而 Python 不允许这样做。

              [案例 2] 当你这样做时

              python -m package.test_A.test
              

              from package.test_A import test
              

              您的锚点变为package,因此package/test_A/test.py 执行from ..A import xxx 不会逃脱锚点(仍在package 文件夹中),Python 很乐意接受这一点。

              简而言之:

              • Absolute-import 更改当前锚点(=重新定义什么是顶级包);
              • 相对导入不会更改锚点,而是限制它。

              此外,我们可以使用full-qualified module name(FQMN) 来检查这个问题。

              在每种情况下检查 FQMN:

              • [CASE2] test.__name__ = package.test_A.test
              • [CASE1] test.__name__ = test_A.test

              因此,对于 CASE2,from .. import xxx 将生成 FQMN=package.xxx 的新模块,这是可以接受的。

              而对于 CASE1,from .. import xxx 中的.. 将跳出test_A起始节点(锚点),这是 Python 不允许的。

              【讨论】:

              • 这比它需要的复杂得多。对 Python 之禅来说就是这样。
              【解决方案13】:

              在 python 2.x 中不确定,但在 python 3.6 中,假设您尝试运行整个套件,您只需要使用 -t

              -t, --top-level-directory 目录 项目顶层目录(默认为启动目录)

              所以,在这样的结构上

              project_root
                |
                |----- my_module
                |          \
                |           \_____ my_class.py
                |
                \ tests
                    \___ test_my_func.py
              
              

              例如可以使用:

              python3 unittest discover -s /full_path/project_root/tests -t /full_path/project_root/

              并且仍然导入my_module.my_class 没有大戏。

              【讨论】:

                【解决方案14】:

                拥有

                package/
                   __init__.py
                   A/
                      __init__.py
                      foo.py
                   test_A/
                      __init__.py
                      test.py
                
                

                A/__init__.py 导入foo

                
                from .foo import foo
                

                当从test_A/导入A/

                
                import sys, os
                sys.path.append(os.path.abspath('../A'))
                # then import foo
                import foo
                
                

                【讨论】:

                  【解决方案15】:

                  这个对我不起作用,因为我使用的是 Django 2.1.3:

                  import sys
                  sys.path.append("..") # Adds higher directory to python modules path.
                  

                  我选择了一个自定义解决方案,我在服务器启动脚本中添加了一个命令,以将我的共享脚本复制到需要共享 python 脚本的 django“应用程序”中。 这并不理想,但由于我只是在开发一个个人网站,所以它适合我。如果我能找到在单个网站内的 Django 应用程序之间共享代码的 django 方式,我将再次在这里发布。

                  【讨论】:

                    猜你喜欢
                    • 2020-03-07
                    • 2020-12-03
                    • 1970-01-01
                    • 2020-11-03
                    • 2017-02-22
                    • 1970-01-01
                    • 2020-04-06
                    相关资源
                    最近更新 更多