Python Namespace Package:一个看似简单,却容易踩坑的机制

· 2026-09-18 20:20 · 3 阅读

原创 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 包中。

但随着项目越来越多,这种方式并不方便。比如 storagefunctions 和 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 gooble

Python 会按照自己的 import 机制,在这些路径中寻找 gooble

找到 gooble 之后,如果继续执行:

import gooble.firestore

Python 还需要知道应该去哪里寻找 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

[2]

PEP 420 – Implicit Namespace Packages | peps.python.org: https://peps.python.org/pep-0420/

[3]

Packaging namespace packages - Python Packaging User Guide: https://packaging.python.org/en/latest/guides/packaging-namespace-packages/

[4]

5. The import system — Python 3.14.7 documentation: https://docs.python.org/3/reference/import.html#reference-namespace-package

[5]

Python Namespace Packages are a pain: https://joshcannon.me/2025/08/16/py-namespace-packages.html

END

未闻 Code·知识星球开放啦!

一对一答疑爬虫相关问题

职业生涯咨询

面试经验分享

每周直播分享

......

未闻 Code·知识星球期待与你相见~

跳转微信打开