Python setuptools中如何隐藏子包但仍可在顶层模块使用其功能
原操作的逻辑误区
使用find_packages(exclude=["main_package.subpackage_to_hide"])配置无法实现需求的核心原因是:exclude参数不仅会让setuptools不把该子包列为公开可导入的注册包,更会直接将该子包的所有文件从构建产物中剔除。安装到目标环境后,main_package/__init__.py里的相对导入找不到对应文件,自然触发模块缺失的报错。
要同时满足“顶层包正常引用内部子包逻辑”“外部无法直接导入内部子路径”两个需求,需要把「文件打包存储」和「导入路径可见性」两个环节完全解耦处理。
第一步:调整打包配置,保证内部文件正常安装
放弃自动遍历所有子包的find_packages逻辑,手动声明公开包列表,同时通过package_data把需要使用的内部子模块文件完整打进分发包,不将其列为公开可导入的注册包。
修改后的setup.py核心配置示例:
import setuptools setuptools.setup( name="main_package", # 仅声明顶层包为公开可导入的包,不自动收集内部子包 packages=["main_package"], # 手动指定需要随包安装的内部非公开模块文件,这些文件会存放在对应目录下,但不被注册为公开包 package_data={ "main_package": [ "subpackage_to_hide/__init__.py", "subpackage_to_hide/hidden_script.py" ] }, include_package_data=True, # 其余原有配置(版本号、依赖、作者信息等)保持不变 )
该配置下的构建产物,会把subpackage_to_hide目录下的所有py文件完整安装到site-packages的main_package路径下,顶层包里的相对导入from .subpackage_to_hide.hidden_script import hidden_func可以正常运行——因为相对导入仅依赖目录下的实际文件结构,不依赖setuptools的公开包注册记录。
第二步:增加导入拦截逻辑,阻止外部直接访问内部路径
仅靠打包配置无法完全阻断外部导入:只要目录结构存在,Python默认的导入机制会自动把带__init__.py的目录识别为可导入包。需要在顶层包的初始化逻辑里增加自定义导入钩子,拦截所有针对内部子包的导入请求。
修改main_package/__init__.py的内容:
# 保留原有对外接口的导入逻辑 from .subpackage_to_hide.hidden_script import hidden_func # 新增导入拦截逻辑 import sys import importlib.abc class _HiddenModuleBlocker(importlib.abc.MetaPathFinder): # 定义需要拦截的内部模块路径前缀 _blocked_prefixes = ( "main_package.subpackage_to_hide", ) def find_spec(self, fullname, path, target=None): # 匹配到拦截前缀的导入请求,直接抛出导入错误 for prefix in self._blocked_prefixes: if fullname == prefix or fullname.startswith(prefix + "."): raise ImportError( f"Module '{fullname}' is internal private implementation, not accessible via public import." ) # 非拦截路径的请求不处理,交给正常导入流程 return None # 将拦截器注册到导入流程最前端,优先处理导入请求 sys.meta_path.insert(0, _HiddenModuleBlocker())
最终效果验证
- 执行
import main_package as mp后,可直接调用mp.hidden_func(),正常输出"I am hidden" - 执行
import main_package.subpackage_to_hide.hidden_script as hs时,会直接触发ImportError,提示该模块为内部私有实现,无法直接导入
注意事项
- 不要使用下划线前缀命名内部目录(如
_subpackage_to_hide)作为隐藏手段:这仅为PEP8的命名约定,不具备强制拦截导入的能力,知晓路径的用户依然可以正常导入 - 不要通过
exclude参数处理内部子包:该参数会直接剔除文件,导致顶层包的相对导入报错
内容的提问来源于stack exchange,提问作者Andreas Forslów

