在Python包间正确导入的方法

1 投票
4 回答
1144 浏览
提问于 2025-04-18 06:27

假设我有几个Python包。

/package_name
    __init__.py
    /dohickey
        __init__.py
        stuff.py
        other_stuff.py
        shiny_stuff.py
    /thingamabob
        __init__.py
        cog_master.py
        round_cogs.py
        teethless_cogs.py
    /utilities
        __init__.py
        important.py
        super_critical_top_secret_cog_blueprints.py

那么,使用这个工具包的最佳方法是什么呢?比如说,shiny_stuff.py需要导入important.py,最好的做法是什么?

目前我在考虑

from .utilities import important

但这样做真的是最好的方法吗?把工具包添加到路径中,然后这样导入会不会更合理呢?

import sys
sys.path.append(os.path.basename(os.path.basename(__file__)))

import utilities.super_critical_top_secret_cog_blueprints

不过,把这个添加到我每个文件里感觉有点麻烦。

4 个回答

0

如果你需要把代码移动到其他地方,建议使用绝对导入。

1

其实没有绝对的“最佳”选择,但你可以根据自己的需求来判断哪种方式更好。为了做出这样的判断,你需要了解不同的方法及其特点。最好的信息来源可能就是PEP 328,里面有关于声明不同可能性的理由。

一种常见的方法是使用“绝对导入”,在你的情况下,可能像这样:

from package_name.utilities import important

这样,你可以把这个文件当作一个脚本。它在某种程度上是独立于其他模块和包的,主要是通过它的位置来固定。如果你有一个包结构,并且需要改变某个模块的位置,使用绝对路径可以帮助这个文件保持不变,但所有使用这个模块的文件都需要进行更改。当然,你也可以像这样导入__init__.py文件:

from package_name import utilities

这些导入的特性是一样的。要注意的是,utilities.important会在__init__.py中寻找一个变量important,而不是在important.py中,所以在__init__.py中有一个“import important”可以帮助避免由于文件结构和命名空间结构之间的区别而导致的错误。

另一种方法是使用相对导入,像这样:

from ..utilities import important

第一个点(from .stuff import ___from . import ___)表示“这个[子]包中的模块”,如果只有一个点则表示__init__.py。第二个点则表示父目录。一般来说,在脚本或可执行文件中不允许以点开头的导入,但如果你对相对导入感兴趣,可以阅读一下显式相对导入(PEP 366)

PEP 328中也有关于相对导入的理由:

随着绝对导入的推广,是否应该允许相对导入的问题随之而来。提出了几个使用案例,其中最重要的是能够在不编辑子包的情况下重新安排大型包的结构。此外,包内的模块如果没有相对导入,无法轻松导入自身。

无论哪种情况,这些模块都与子包相关联,意思是package_name会优先被导入,无论用户先尝试导入哪个,除非你使用sys.path将子包视为包进行搜索(也就是说,在sys.path中使用包的根目录)……但这听起来有点奇怪,为什么要这样做呢?

__init__.py可以自动导入模块名,因此需要关注它的命名空间内容。例如,假设important.py中有一个叫top_secret的对象,它是一个字典。要从任何地方找到它,你需要:

from package_name.utilities.important import top_secret

也许你想要不那么具体:

from package_name.utilities import top_secret

这可以通过在__init__.py中添加以下行来实现:

from .important import top_secret

这可能是将相对导入和绝对导入混合在一起,但对于__init__.py来说,你可能知道子包作为子包是有意义的,也就是说,它本身就是一个抽象。如果它只是一些位于同一位置的文件,并且需要明确的模块名称,可能__init__.py会是空的(或几乎空的)。但为了避免用户需要明确的模块名称,同样的想法也可以在根__init__.py中实现,像这样:

from .utilities import top_secret

虽然这种方式完全间接,但命名空间变得扁平化,而文件则为了某种内部组织而嵌套。例如,wx包(wxPython)就是这样做的:所有内容都可以直接通过from wx import ___找到。

如果你想遵循这种方法,也可以使用一些元编程来查找内容,例如,使用__all__来检测模块中所有的名称,或者查看文件位置以了解可以导入哪些模块/子包。不过,一些简单的代码补全工具在这样做时可能会迷失方向。

在某些情况下,你可能会有其他类型的限制。例如,macropy在导入时做了一些“魔法”,并且在你将其作为脚本调用的文件上无法工作,所以你至少需要两个模块才能使用这个包。

无论如何,你应该始终问自己,嵌套到子包中是否真的对你的代码或API组织是必要的。PEP 20告诉我们“扁平比嵌套更好”,这不是一条法律,而是一种观点,建议你保持扁平的包结构,除非出于某种原因需要嵌套。同样,你也不需要为每个类或类似的东西创建一个模块。

3

根据Nick Coghlan(他是Python的核心开发者)所说:

“绝不要直接将一个包的目录,或者包内的任何目录,添加到Python的路径中。”(在“重复导入陷阱”这一部分)

把包的目录添加到路径中,会让这个模块有两种不同的引用方式。上面的链接是关于Python导入系统的一个很棒的博客文章。直接把它添加到路径中意味着你可能会有同一个模块的两个副本,这是不想要的。你的相对导入 from .utilities import important 是没问题的,绝对导入 import package_name.utilities.important 也是可以的。

3

我觉得最安全的方法就是始终使用绝对导入,所以在你的情况下:

from package_name.utilities import important

这样的话,如果你决定把你的 shiny_stuff.py 文件移动到其他包里,你就不需要修改代码了(前提是那个包的名字仍然在你的 sys.path 里)。

撰写回答