如何在Python包内引用顶层模块?

31
在下面的层次结构中,有没有一种方便且通用的方法可以使用一个通用术语引用top_package,在所有.py文件中都能够使用呢?我希望有一种一致的导入其他模块的方式,这样即使"top_package"更改名称,也不会出现问题。
我不赞成使用相对导入,比如"..level_one_a",因为相对路径在每个Python文件中都不同。我正在寻找一种方法,以满足以下要求:
1. 每个Python文件可以为包中相同模块具有相同的导入声明。 2. 在包内任何.py文件中进行解耦式的"top_package"引用,使得无论"top_package"更改名称为何,都不会出现问题。
top_package/
  __init__.py
  level_one_a/
    __init__.py
    my_lib.py
    level_two/
      __init__.py
      hello_world.py
  level_one_b/
    __init__.py
    my_lib.py
  main.py

1
你为什么想要一个指向顶层模块的引用?(有时这个答案揭示了真正的问题/解决方案。) - Jon-Eric
3
我希望让这个软件包尽可能具有可重用性,这样当我重新命名顶层模块时,每个导入名称都不需要改变,一切都可以正常工作。 - hllau
6个回答

22

这应该可以完成任务:

top_package = __import__(__name__.split('.')[0])
这里的技巧在于,对于每个模块,__name__变量包含由点分隔的模块完整路径,例如top_package.level_one_a.my_lib。因此,如果想要获取顶级包名称,只需要获取路径的第一个组件并使用__import__导入它即可。
尽管用于访问该软件包的变量名仍称为top_package,但您可以重命名该软件包,它仍将正常工作。

1
-1 这个问题要求一个不依赖于名称“top_package”的解决方案。 - Jon-Eric
1
@Jon-Eric 感谢您的评论。我已经修复了我的答案,使其可以在不考虑顶级包名称的情况下工作。 - jcollado
1
这并不完全正确;如果当前模块不是从顶层导入的,它的 __name__ 将不包含此模块的完整路径,而只包含部分路径,该部分路径将从您导入此模块的位置开始。 - Daniel

11

将您的包和main脚本放入外部容器目录中,如下所示:

container/
    main.py
    top_package/
        __init__.py
        level_one_a/
            __init__.py
            my_lib.py
            level_two/
                __init__.py
                hello_world.py
        level_one_b/
            __init__.py
            my_lib.py
当运行main.py时,它的父目录(container)会自动添加到sys.path的开头。而且由于top_package现在位于相同的目录中,因此可以从包树中的任何位置导入它。
因此hello_world.py可以这样导入level_one_b/my_lib.py:
from top_package.level_one_b import my_lib
无论容器目录的名称是什么,或者它位于哪里,使用这种安排导入都将始终有效。但请注意,在您原始的示例中,top_package 可以很容易地充当容器目录本身。您只需要删除 top_package/__init__.py,就可以得到实际上相同的安排。
然后,前面的导入语句将更改为:
from level_one_b import my_lib

而且你可以自由地更改top_package的名称。


1
如果您删除 top_package/init.py,则 level_one_a 和 level_one_b 将无法相互导入。 - DonGar
@DonGar。不,它将完美地运行。但请注意,我是在参考OP问题中的结构,而不是我回答中显示的那个。关键区别在于main.py脚本的位置。 - ekhumoro
我同意你回答的主要部分,但是我不同意以“请注意”开头的那一段。除非你也是指将level_one_a和level_one_b都移动到top_container的一个新子模块中。 - DonGar
@DonGar,显然你并没有真正测试过实际代码。我回答中的所有内容都能够按照所述方式正常工作。我不明白你的异议基于何种理由。 - ekhumoro
是的,我有。事实上,我刚刚经历了一个痛苦的过程,第一次设置了一个不使用路径混淆的项目。但是反思一下,如果由主程序导入,库之间可以相互导入。但如果是在 level_one 模块内部的测试程序中调用,则无法导入,即使这些测试程序是通过“discover”机制调用的。您提出的顶层包装器包的想法最终解决了我各种导入问题(我需要让 main/tests/lint 都工作)。 - DonGar
FYI,此示例正在利用 Python 的 "intra-package-reference" 功能。 - omni

1

这个可以在库模块内部使用:

import __main__ as main_package
TOP_PACKAGE = main_package.__package__.split('.')[0]

1
你可以使用__import__()函数和包的__path__属性的组合。
例如,假设你希望从包中的其他地方导入<whatever>.level_one_a.level_two.hello_world。你可以这样做:
import os
_temp = __import__(__path__[0].split(os.sep)[0] + ".level_one_a.level_two.hello_world")
my_hello_world = _temp.level_one_a.level_two.hello_world

这段代码与顶层包的名称无关,可以在包的任何地方使用。但它看起来相当丑陋。


0

我认为如果不使用相对导入或命名包,#2是不可能的。您必须明确指定要导入的模块,无论是通过显式调用其名称还是使用相对导入。否则解释器怎么知道您想要什么?

如果您将应用程序启动器放在top_level/的上一级,并使用import top_level导入它,则可以从top_level包内的任何位置引用top_level.*

(我可以向您展示我正在开发的软件的示例:http://github.com/toddself/beerlog/


0
这是我在最近的Python版本(>=3.11)中最有效的方法:
def program_name():
    import __main__ as main  # noqa
    if package := main.__package__:
        return package
    return Path(main.__file__).name

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接