Python src-layout 项目结构添加子包层级及导入问题详解
2025-02-27 23:01:21
Python src-layout 项目结构下添加子包层级
最近在处理多个 Python 项目,它们都采用了 src-layout
的结构,而且都有相同的顶层包名。 项目结构大概长这样:
package-subpackage1
├── .git/
├── src/
│ └── package/
│ └── subpackage1/
│ ├── __init__.py
│ └── code_subpackage1.py
└── pyproject.toml
package-subpackage2/
├── .git/
├── src/
│ └── package/
│ └── subpackage2/
│ ├── __init__.py
│ └── code_subpackage2.py
└── pyproject.toml
每个仓库的 .toml
文件都设置成:
[tool.setuptools.packages.find]
where = ["src"]
include = ["package.*"]
这样就能用下面的方式导入模块:
import package.subpackage1
import package.subpackage2
遇到的问题
现在,我想给 subpackage1
添加一个子包层级,变成这样:
package-subpackage1/
├── .git/
├── src/
│ └── package/
│ └── subpackage1/
│ ├── __init__.py
│ ├── code_subpackage1.py
│ └── subsubpackage1/
│ ├── __init__.py
│ └── code_subsubpackage1.py
└── pyproject.toml
理想情况下,我想这样调用:
import package.subpackage1
subpackage1.subsubpackage1.SomeClass
但当我尝试通过下面这种方式访问 subsubpackage1
时:
import package.subpackage1 as pack
pack.subsubpackage1
出现了错误:
AttributeError: module 'package.subpackage1' has no attribute 'subsubpackage1'
这倒也正常,毕竟我改变了原有的 src-layout
结构,自动发现机制似乎失效了。但奇怪的是,如果我之后手动导入 subsubpackage1
:
import package.subpackage1.subsubpackage1
subsubpackage1.py
里的所有类就"神奇地"可以通过以下方式访问了:
pack.subsubpackage1.SomeClass
我不太明白,为什么手动导入 subsubpackage1
后,它就能在之前导入的 subpackage1
里访问到了? 有没有人知道这是怎么回事?
问题原因分析
这个问题核心在于 Python 的模块导入机制和 setuptools
的包发现机制的交互。
-
Python 模块导入机制: 当你执行
import package.subpackage1
时,Python 会:- 找到
package
目录。 - 执行
package/__init__.py
(如果存在)。 - 找到
package/subpackage1
目录。 - 执行
package/subpackage1/__init__.py
(如果存在)。 - 将
subpackage1
作为一个模块对象添加到package
的命名空间。
此时,
package.subpackage1
是一个模块对象,但它并不知道subsubpackage1
的存在,因为subpackage1/__init__.py
里没有显式地导入或定义subsubpackage1
。 - 找到
-
手动导入的影响: 当你执行
import package.subpackage1.subsubpackage1
时:- Python 找到了
subsubpackage1
。 - 然后它执行了
package/subpackage1/subsubpackage1/__init__.py
。 - 这个操作 修改 了已经导入的
package.subpackage1
模块。 Python 会把subsubpackage1
作为属性添加到 已经存在 的package.subpackage1
模块对象里. 这就是"魔法"发生的地方. 因为package.subpackage1
对象已经在内存里了,第二次导入并没有创建新的对象,而是在已有的对象上添加了新的属性。
- Python 找到了
-
setuptools
的包发现:setuptools
在打包时,会根据pyproject.toml
的配置,自动查找需要包含的包。include = ["package.*"]
告诉它包含package
下的所有直接子包。它并不会递归地查找更深层级的子包。
解决方案
为了解决这个问题,让 package.subpackage1.subsubpackage1
能正常导入,有几个办法:
1. 修改 __init__.py
最直接的方法是修改 package/subpackage1/__init__.py
,显式地导入 subsubpackage1
:
# package/subpackage1/__init__.py
from . import subsubpackage1
或者直接导入里面的类:
#package/subpackage1/__init__.py
from .subsubpackage1 import SomeClass
原理: 这样,在导入 package.subpackage1
时,subsubpackage1
也会被导入,并成为 subpackage1
的一个属性。
代码示例: 上面给出的就是代码。
安全建议: 没啥特别需要注意的。
2. 使用相对导入 (Recommended)
可以在code_subpackage1.py
里面, 用相对路径引入同级的其他模块,和子目录下的模块:
# in code_subpackage1.py
from . import other_module_in_subpackage1 #同级目录下.
from .subsubpackage1 import SomeClass #subsubpackage1子目录下.
原理: Python 推荐, 也是最常用的包管理方案, 不需要再进行复杂的配置。 易于管理和维护。
代码示例: 上面给出的就是代码。
3. 修改 pyproject.toml
(不太推荐)
可以修改 pyproject.toml
,让 setuptools
递归地包含所有子包。可以修改配置, 指定更详细的匹配规则:
[tool.setuptools.packages.find]
where = ["src"]
include = ["package.subpackage1*"]
或者,用packages
精确的指定哪些模块要包括进来:
[tool.setuptools]
packages = ["package","package.subpackage1", "package.subpackage1.subsubpackage1", "package.subpackage2"]
[tool.setuptools.packages.find]
where = ["src"]
原理:
include = ["package.subpackage1*"]
: 通配符匹配更广。packages
: 手动列出.
代码示例: 就是上面 toml
文件的配置。
安全建议: 注意不要包含不必要的模块,防止意外暴露代码。 过于精确的配置, 后续不便于管理和维护.
4. 使用 Namespace Packages (进阶)
如果你的 package
是一个比较大的项目,并且希望有更灵活的结构,可以考虑使用 Namespace Packages。
原理: Namespace Packages 允许多个不同的目录,作为同一个顶层包的一部分。
实现步骤 (使用 native namespace packages
PEP 420):
- 移除所有
__init__.py
文件(顶层包、子包都移除)。 这是最关键的一步! - 每个 repository 的
pyproject.toml
保持不变(或者做最小限度的修改):
[tool.setuptools.packages.find]
where = ["src"]
include = ["package*"]
或
[tool.setuptools.packages.find]
where = ["src"]
- 文件结构可以保持不变,无需额外调整。
代码示例:
假设有两个仓库:
repo1
:src/package/subpackage1/module1.py
repo2
:src/package/subpackage2/module2.py
安装这两个仓库后,可以这样导入:
import package.subpackage1.module1
import package.subpackage2.module2
安全建议: Namespace Packages 适合大型、分布式项目,使用前需要仔细规划包结构。
Namespace Package的好处:
- 结构可以分布在不同的仓库中。
- 非常高的可拓展性.
希望以上内容可以帮你更好地理解这个问题, 和构建你的包!