如何更精细控制setuptools包导入?解决其贪婪导入问题
问题背景
我的代码结构如下:
tstpkg/ pyproject.toml tstpkg/ __init__.py modA/ fnA1 fnA2 modB/ fnB1 fnB2 modC/ fnC1 fnC2
我希望导入tstpkg后,仅能调用tstpkg.fnA1()和tstpkg.fnC2(),于是做了以下配置:pyproject.toml内容:
[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] namespaces = false
tstpkg/__init__.py内容:
from tstpkg.modA import fnA1 from tstpkg.modC import fnC2
但实际安装后导入tstpkg,却能直接访问tstpkg.modA和tstpkg.modC整个子模块,不符合预期。用del(modA, modC)可以解决但不够优雅,同时我注意到执行from scipy.special import gammaln as sgl时,并不会让整个scipy.special变得可访问,因此有两个疑问:
1. 为什么会出现这种情况?
这是Python模块导入机制的默认行为,和setuptools无关:
- 当执行
from tstpkg.modA import fnA1时,Python会先完整加载tstpkg.modA子模块,再将fnA1的引用挂载到tstpkg的命名空间中。 - 加载后的子模块会被自动添加到父模块(
tstpkg)的命名空间里,作为父模块的属性存在,因此可以通过tstpkg.modA直接访问。
而scipy.special的例子不同:scipy的__init__.py没有触发scipy.special的加载,也没有将其作为属性暴露到自身命名空间中。即便你执行from scipy.special import gammaln加载了子模块,scipy的命名空间里也不会自动生成special属性,自然无法通过scipy.special访问。
2. 是否有更自然的方法实现我想要的行为?
有几种更优雅的方案:
方案一:动态导入避免子模块挂载
使用importlib动态导入函数,不会将子模块添加到父模块命名空间:
from importlib import import_module fnA1 = import_module("tstpkg.modA").fnA1 fnC2 = import_module("tstpkg.modC").fnC2
方案二:用__all__限制导出范围
在tstpkg/__init__.py中定义__all__,明确指定from tstpkg import *时能导入的名称:
__all__ = ["fnA1", "fnC2"] from tstpkg.modA import fnA1 from tstpkg.modC import fnC2
注:__all__仅限制import *的行为,用户仍可通过import tstpkg.modA直接导入子模块,但这是Python模块系统的常规开放特性。
方案三:调整代码结构
如果子模块无需独立存在,可将fnA1、fnC2直接写入tstpkg/__init__.py,或放在主包下的单独模块(如tstpkg/core.py)中导入,从根源避免子模块暴露。
内容的提问来源于stack exchange,提问作者user1451632

