最简单有效的第一步是用\_前缀重命名子模块,如\_internal.py,它能向python社区发出私有模块信号,避免被from package import *暴露和ide默认补全。

用 _ 前缀重命名子模块是最简单有效的第一步
Python 社区公认:以单下划线开头的模块名(如 _internal.py 或 _utils.py)是“私有模块”信号。它不会被 from package import * 暴露,也不会出现在 IDE 自动补全的默认建议列表里。
常见错误现象:有人只把文件改成 internal.py,但没加下划线,结果外部仍能轻松 from mypkg import internal —— 这完全没效果。
- 必须重命名为
_internal.py,不是internal.py或Internal.py - 重命名后,检查所有上级
__init__.py,确保里面没有from . import _internal或from ._internal import helper - 如果该模块仅用于内部逻辑,连
_internal.py本身都不应出现在setup.py的packages列表或pyproject.toml的[project.optional-dependencies]之外的包声明中——否则打包时仍会被安装并可导入
__all__ 在子模块里设为空列表不起作用,但在父模块 __init__.py 中必须清理导入
__all__ = [] 放在 _internal.py 顶部,对阻止 from mypkg._internal import * 有用;但它**完全无法阻止** import mypkg._internal 或 from mypkg import _internal —— 因为 __all__ 只约束 import * 行为。
真正关键的是父模块的 __init__.py:
- 删掉所有类似
from ._internal import something或from . import _internal的语句 - 避免在
__init__.py中写import *,比如from ._internal import *—— 这会把符号拖进父命名空间 - 如果父模块内部确实需要使用
_internal,改用局部导入:在函数体或类方法内写from ._internal import helper,而不是放在模块顶层
禁用 import 的运行时拦截不推荐,除非强沙箱场景
有人试图在 _internal.py 开头加 inspect.stack() 检查调用来源,发现非授权模块就抛 ImportError。这看似彻底,但问题很多:
-
inspect.stack()性能开销大,不能出现在高频路径(比如每次 HTTP 请求都触发) - 单元测试、REPL、调试器常会误报,导致开发体验断裂
- 用户仍可通过
importlib.util.spec_from_file_location绕过,实际封不住 - 违背 Python “我们都是 consenting adults” 哲学:靠命名和文档提示,比 runtime 报错更可持续
打包配置遗漏是线上最常踩的坑
本地测试时一切正常,一发版到 PyPI,用户却能 import mypkg._internal —— 很大概率是 pyproject.toml 或 setup.py 里漏掉了排除逻辑。
例如,在 pyproject.toml 中:
[[tool.setuptools.package-data]] "mypkg.*" = ["*.py"] # ❌ 这样会把 _internal.py 也打包进去
正确做法是显式声明只包含公有模块:
[tool.setuptools.packages.find] where = ["src"] include = ["mypkg", "mypkg.*"] exclude = ["mypkg._*", "mypkg.tests*"]
或者更稳妥:不用自动发现,手动列明 packages = ["mypkg", "mypkg.utils", "mypkg.api"],彻底不提 _internal。
名字加了 _ 只是约定,真正让它“消失”的,是打包时没把它放进分发包里。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











