如何构建我的 Python 项目以允许从子目录导入命名模块

How do I structure my Python project to allow named modules to be imported from sub directories

这是我的目录结构:

Projects
    + Project_1
    + Project_2
    - Project_3
        - Lib1
            __init__.py # empty
            moduleA.py
        - Tests
            __init__.py # empty
            foo_tests.py
            bar_tests.py
            setpath.py
        __init__.py     # empty
        foo.py
        bar.py

目标:

  1. 有一个有组织的项目结构
  2. 必要时能够独立运行每个.py文件
  3. 能够reference/import兄弟姐妹和堂兄弟模块
  4. 将所有 import/from 语句保留在每个文件的开头。

我使用上述结构获得了第一名

我通过执行以下操作(根据 this excellent guide 的建议)取得了 2、3 和 4 的大部分成绩

在任何需要访问父模块或堂兄弟模块的包中(例如上面的 Tests 目录),我包含一个名为 setpath.py 的文件,其中包含以下代码:

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('...'))

然后,在每个需要 parent/cousin 访问的模块中,例如 foo_tests.py,我可以像这样写一个干净的导入列表:

import setpath      # Annoyingly, PyCharm warns me that this is an unused import statement
import foo.py

在 setpath.py 中,第二个和第三个插入对于此示例而言并非绝对必要,但作为故障排除步骤包括在内。

我的问题是这只适用于直接引用模块名称的导入,而不适用于引用包的导入。例如,在 bar_tests.py 中,当 运行ning bar_tests.py 直接 .

时,下面两个语句都不起作用
import setpath

import Project_3.foo.py  # Error
from Project_3 import foo  # Error

我收到错误 "ImportError: No module named 'Project_3'"。

奇怪的是,我可以直接从 PyCharm 中 运行 文件,而且工作正常。我知道 PyCharm 正在使用 Python Path 变量在幕后施展魔法,使一切正常,但我不知道它是什么。由于 PyCharm 只是 运行s python.exe 并设置了一些环境变量,因此应该可以从 Python 脚本本身中克隆此行为。

由于与这个问题不太相关的原因,我必须使用 Project_3 限定符来引用 bar

我对任何能够实现上述目标同时仍能满足我早先目标的解决方案持开放态度。如果有更好的目录结构,我也愿意使用另一种目录结构。我已经阅读了 Python doc on imports 和软件包,但仍然不知所措。我认为一种可能的方法是手动设置 __path__ 变量,但我不确定需要更改哪个或将其设置为什么。

我觉得你的目标不合理。具体来说,第 2 个目标是个问题:

  1. 必要时能够独立运行每个 .py 文件

这不适用于包中的模块。至少,如果您天真地 运行 宁 .py 文件(例如,在命令行上使用 python foo_tests.py ),则不会。当您 运行 那样处理文件时,Python 无法判断包层次结构应该从哪里开始。

有两种可行的替代方法。第一个选项是 运行 你的脚本从顶级文件夹(例如 projects)使用 -m 标志给解释器,给它一个到主模块的虚线路径,并使用显式相对导入以获得兄弟和堂兄弟模块。因此,不是直接 运行ning python foo_tests.py,运行 python -m project_3.tests.foo_tests 来自 projects 文件夹(或者 python -m tests.foo_tests 来自 project_3 文件夹) , 并 foo_tests.py 使用 from .. import foo.

另一个(不太好)选项是在系统范围内将顶级文件夹添加到 Python 安装的模块搜索路径(例如,将 projects 文件夹添加到 PYTHON_PATH 环境变量),然后对所有模块使用绝对导入(例如 import project3.foo)。这实际上是您的 setpath 模块所做的,但是作为系统配置的一部分在系统范围内进行,而不是在 运行 时,它更清晰。它还避免了 setpath 允许您用于导入模块的多个名称(例如尝试 import foo_tests, tests.foo_tests 并且您将获得两个 separate 相同的副本模块)。

这些类型的问题符合 "primarily opinion based",所以让我谈谈我的看法。

首先"be able to independently run each .py file when necessary":要么文件是一个模块,所以不应该直接调用,要么是独立的可执行文件,那么它应该从顶层开始导入它的依赖项(你可以在代码中避免它或者更确切地说,通过使用 setup.py entry_points 将其移动到公共位置,但随后您的前可执行文件有效地转换为模块)。是的,这是 Python 模块模型的弱点之一,导致误解。

其次,使用 virtualenv(或 Python3 中的 venv)并将每个 Project_x 放入单独的一个。这样项目的名称就不会成为 Python 模块路径的一部分。

第三,link 你提供了提及 setup.py – 你可以利用它。将您的自定义代码放入 Project_x/src/mylib1,创建 src/mylib1/setup.py,最后将您的模块放入 src/mylib1/mylib1/module.py。然后你可以像安装任何其他包一样通过 pip 安装你的代码(或者 pip -e 所以你可以直接处理代码而无需重新安装它,尽管不幸的是它有一些限制)。

最后,正如您已经在评论中确认的那样 ;)。您当前模型的问题是,在 sys.path.insert(0, os.path.abspath('...')) 中,您错误地使用了 Python 模块的符号,这对于系统路径不正确,应替换为 '../..' 以按预期工作。