Python Namespace Package:一个看似简单,却容易踩坑的机制
原创 kingname 2026-09-18 20:20 新加坡

Python 为什么允许没有 __init__.py?

摄影:产品经理
小甜品
如果你写过 Python,应该对 __init__.py 并不陌生。
我们创建一个 Python 包时,通常都会在目录中放一个 __init__.py。但如果你使用过一些现代 Python 项目,可能会发现一个有意思的现象:
有些目录明明没有 __init__.py,却依然可以正常被 import。
例如:
my_package/
├── module_a.py
└── module_b.py即使没有 my_package/__init__.py,下面这样的代码依然可能正常运行:
import my_package.module_a这是为什么?
答案是 Python 的 Namespace Package(命名空间包)。
它解决了一个非常实际的问题:允许一个 Python 包分布在多个目录中。
但与此同时,它也让 Python 的包系统变得更加复杂。如果不是确实有这个需求,开发者最好不要主动使用它。
什么是 Namespace Package?
先来看一个实际场景。
假设有一家大型公司叫 Gooble,公司内部有很多 Python 项目。为了让这些项目具有统一的命名空间,他们希望所有模块都以 gooble 开头,例如:
gooble.storage
gooble.functions
gooble.firestore
gooble.firebase最直接的做法,是把所有代码放进同一个 gooble 包中。
但随着项目越来越多,这种方式并不方便。比如 storage、functions 和 firestore 实际上可能分别由不同的团队维护,也应该作为不同的 distribution package 独立发布。
于是,理想的目录结构可能变成这样:
gooble-storage/
└── gooble/
└── storage/
gooble-firestore/
└── gooble/
└── firestore/这时候虽然存在两个不同的 gooble 目录,但我们希望 Python 最终能够把它们看成同一个包。
安装之后,我们依然希望能够直接使用:
import gooble.storage
import gooble.firestore这就是 Namespace Package 要解决的问题。
简单来说:
Namespace Package 允许一个逻辑上的 Python 包,由磁盘上的多个目录共同组成 [1]。
为什么需要 Namespace Package?
如果所有 Python 包最终都安装到同一个 site-packages 目录,那么这个问题其实并不明显。
但 Python 的模块并不一定只来自一个目录。
Python 会根据 sys.path 中的路径寻找模块。例如,一个 Python 环境可能同时包含项目目录、虚拟环境目录,以及其他额外的模块搜索路径。
当我们执行:
import gooblePython 会按照自己的 import 机制,在这些路径中寻找 gooble。
找到 gooble 之后,如果继续执行:
import gooble.firestorePython 还需要知道应该去哪里寻找 firestore。
问题就出现在这里。
假设我们把不同的 distribution package 分别放到不同的缓存目录中:
/cache/gooble-storage/gooble/storage/
/cache/gooble-firestore/gooble/firestore/那么 Python 已经从第一个目录找到了 gooble,接下来怎么知道另一个目录里还有一个 gooble.firestore?
Namespace Package 的作用,就是告诉 Python:这个包可能分布在多个不同的位置,继续搜索其他路径。
这对于一些强调缓存复用、并行安装的 Python 环境管理工具来说尤其重要。
Namespace Package 有两种实现方式
Namespace Package 并不是 Python 最近才出现的概念。
历史上,Python 主要通过一种“显式”的方式实现 Namespace Package;后来 PEP 420 又引入了“隐式”的实现方式 [2]。
也就是我们经常看到的:
Explicit Namespace Package
Implicit Namespace Package
两者最终解决的是类似的问题,但实现方式并不相同。
传统方式:Explicit Namespace Package
早期的 Namespace Package,需要在 __init__.py 中明确告诉 Python:
“这个包不是一个普通的包,你还需要去其他目录寻找同名的子模块。”
最常见的写法是使用 pkgutil.extend_path:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)历史上也可以使用 pkg_resources.declare_namespace() 来实现类似的效果。
这种方式的核心其实就是修改包的 __path__,让 Python 在导入子模块时能够搜索更多目录。
不过,它有一个比较麻烦的要求:
所有参与同一个 Namespace Package 的 distribution package,都必须遵守相同的规则 [3]。
例如:
gooble-storage/
└── gooble/
└── __init__.py
gooble-firestore/
└── gooble/
└── __init__.py如果其中一个 __init__.py 正确扩展了 __path__,而另一个 __init__.py 什么都没有做,那么最终的 import 行为就可能受到 sys.path 顺序的影响。
也就是说,这种方案不仅需要开发者理解它,还要求不同项目之间保持一致。
这也是传统 Namespace Package 比较容易出问题的地方。
现代方式:Implicit Namespace Package
后来,Python 3.3 引入了 PEP 420,为 Namespace Package 提供了一种更加简单的实现方式。
它的规则非常简单:
不要创建 __init__.py。
例如:
gooble-storage/
└── gooble/
└── storage/
gooble-firestore/
└── gooble/
└── firestore/这里的 gooble 目录都没有 __init__.py。
Python 的 import 机制会识别这种情况,并允许多个路径下的 gooble 共同组成一个 Namespace Package [4]。
因此下面的代码仍然可以正常使用:
import gooble.storage
import gooble.firestore如果你以前遇到过“忘记创建 __init__.py,但程序依然可以 import”的情况,那么很可能就是因为 Python 把这个目录当成了 Implicit Namespace Package。
从开发者的角度来看,这种方式确实简单了很多。
但问题也随之而来。
Namespace Package 为什么容易让人困惑?
Namespace Package 最大的问题并不是它不能工作,而是它增加了很多额外的复杂性。
首先,同一个问题存在两种实现方式。
你可以通过 __init__.py 显式扩展包路径,也可以直接省略 __init__.py,让 Python 使用 PEP 420 提供的隐式机制。
对于普通开发者来说,这意味着:
看到一个没有 __init__.py 的目录时,你并不能马上知道这是开发者有意设计的 Namespace Package,还是单纯忘记创建这个文件。
对于开发工具来说,这个问题更加明显。
如果工具需要判断某个 import package 属于哪个 distribution package,那么面对一个没有 __init__.py 的目录,它很难准确知道开发者的真实意图。
它可以根据项目结构做推测,但这种推测并不一定可靠。
一个更现实的问题
Implicit Namespace Package 还有一个比较容易被忽略的问题:它很容易被无意中破坏。
假设项目当前是这样:
gooble/
├── storage/
└── firestore/由于没有 __init__.py,Python 把它作为 Namespace Package 处理。
某一天,一位开发者提交了一个 PR,顺手添加了:
gooble/__init__.py也许他只是觉得:
“Python 包不是应该有
__init__.py吗?”
但这个看似普通的文件,实际上可能改变整个包的 import 行为。
更麻烦的是,这类问题在 Code Review 中很容易被忽略。
我们平时 Review PR 时,很少会因为“新增了一个 __init__.py”就专门检查它是否会改变 Namespace Package 的语义。
所以,从维护角度来看,Implicit Namespace Package 并没有想象中那么稳妥。
为什么环境管理工具尤其在意这个问题?
前面提到,如果所有 distribution package 都安装到同一个 site-packages 目录,那么很多问题可能不会马上出现。
但如果 Python 环境管理工具采用另外一种设计:
每个 distribution package 单独存放,然后通过 sys.path 组合出最终的 Python 环境。
那么 Namespace Package 的行为就会变得非常重要。
例如:
/cache/
├── gooble-storage/
│ └── gooble/
│ └── storage/
│
└── gooble-firestore/
└── gooble/
└── firestore/如果 Python 只根据第一个路径找到 gooble,却没有继续搜索其他路径,那么 gooble.firestore 就可能无法正常导入。
Namespace Package 正是为了解决这种跨目录组合的问题。
但反过来,这也意味着环境管理工具必须正确处理 Namespace Package 的各种情况。
一旦不同 distribution package 对 namespace 的处理方式不一致,就可能出现一些非常难排查的问题。
而这也是为什么一些环境管理工具并不愿意依赖这种机制。
所以,__init__.py 到底该不该写?
如果你只是开发普通的 Python 项目,那么答案其实很简单:
如果没有明确的 Namespace Package 需求,就把 __init__.py 写上。
例如:
my_package/
├── __init__.py
├── module_a.py
└── module_b.py这样做的好处是项目结构更加明确。
看到 __init__.py,开发者和工具都可以比较明确地知道:
这里就是一个普通的 Python Package。
而如果没有这个文件,就需要进一步判断它究竟是一个有意设计的 Namespace Package,还是开发者不小心漏掉了。
对于大型项目和基础设施工具来说,这种差异可能会带来额外的复杂性。
Namespace Package 是不是一个糟糕的设计?
倒也不能这么简单地说。
Namespace Package 解决的是一个真实存在的问题,而且对于大型组织、复杂的 Python 包生态,以及需要将一个逻辑包拆分到多个 distribution package 的场景来说,它确实很有价值。
问题在于:
这个机制的灵活性,也带来了额外的认知成本。
普通开发者需要理解两套 Namespace Package 机制,IDE 和静态分析工具需要考虑 Namespace Package,打包工具和环境管理工具也需要正确处理它们。
所以,对于大多数普通 Python 项目来说,没有必要为了“更现代”而主动使用 Namespace Package。
如果你的项目并不需要让多个 distribution package 共享同一个 import package,那么一个简单明确的 __init__.py,往往就足够了。
最后
Python 的 Namespace Package 是一个很典型的工程设计。
它并不是为了炫技,而是确实解决了一个实际问题:让一个逻辑上的 Python Package 可以分布在多个目录,甚至由多个独立的 distribution package 共同提供。
但与此同时,它也引入了新的复杂性。
尤其是 Explicit Namespace Package 和 Implicit Namespace Package 并存之后,开发者、打包工具以及环境管理工具都需要额外理解这套规则。
所以,如果你今天正在创建一个普通的 Python 包,不妨记住一个简单的原则:
没有明确的 Namespace Package 需求,就把
__init__.py写上。
少一个文件看起来很简单,但在 Python 的 import 系统里,它可能意味着完全不同的语义。
有时候,明确地写出来,比依赖 Python 的隐式行为更容易维护。
本文基于博客Python Namespace Packages are a pain[5]整理而成。
参考资料
[1] Glossary — Python 3.14.7 documentation: https://docs.python.org/3/glossary.html#term-namespace-package PEP 420 – Implicit Namespace Packages | peps.python.org: https://peps.python.org/pep-0420/ Packaging namespace packages - Python Packaging User Guide: https://packaging.python.org/en/latest/guides/packaging-namespace-packages/ 5. The import system — Python 3.14.7 documentation: https://docs.python.org/3/reference/import.html#reference-namespace-package Python Namespace Packages are a pain: https://joshcannon.me/2025/08/16/py-namespace-packages.html
END

未闻 Code·知识星球开放啦!
一对一答疑爬虫相关问题
职业生涯咨询
面试经验分享
每周直播分享
......
未闻 Code·知识星球期待与你相见~
