用 Python 的 __all__ 管好模块与包的公开 API

2026-07-28 18 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:6 分钟

Python 的 from module import * 会把哪些名字带入当前命名空间?答案不只取决于函数和变量是否存在,还取决于模块是否声明了 __all__。理解这套规则,不只是为了答对通配符导入的问题,更重要的是明确模块和包的公开边界,让调用方知道哪些名字可以稳定依赖。

通配符导入到底会导入什么

当模块没有定义 __all__ 时,from module import * 通常会导入所有不以下划线开头的名字。以下划线开头的名字会被视为内部实现细节。

# metrics.py
DEFAULT_WINDOW = 60


def average(values):
    return sum(values) / len(values)


def _validate(values):
    if not values:
        raise ValueError("values must not be empty")

执行:

from metrics import *

print(DEFAULT_WINDOW)  # 60
print(average([2, 4, 6]))  # 4.0
print("_validate" in globals())  # False

下划线只是一种默认过滤规则,不是访问控制机制。调用方仍然可以显式导入内部名字:

from metrics import _validate

因此,Python 的“私有”主要依靠约定,而不是语言层面的强制隔离。

__all__ 是一份明确的导出清单

定义 __all__ 后,通配符导入会按照这份字符串列表选择名字。它既能隐藏普通名称,也能公开以下划线开头的名称。

# reports.py
__all__ = ["build_report", "ReportError"]


class ReportError(Exception):
    pass


def build_report(rows):
    if not rows:
        raise ReportError("rows must not be empty")
    return {"count": len(rows)}


def normalize_rows(rows):
    return [dict(row) for row in rows]


_INTERNAL_VERSION = 2

现在运行下面的代码:

from reports import *

print(build_report([{"id": 1}, {"id": 2}]))
print(ReportError)
print("normalize_rows" in globals())  # False
print("_INTERNAL_VERSION" in globals())  # False

这里的关键边界是:__all__ 主要控制 from reports import *。它不会阻止以下用法:

import reports

print(reports.normalize_rows([{"id": 1}]))

它也不会让未列出的名字变成真正不可访问的私有成员。更准确地说,__all__ 是模块作者发布的 API 清单,而不是权限系统。

在包级别提供稳定入口

__all__ 在包的 __init__.py 中尤其有价值。可以把分散在多个子模块中的常用对象重新导出,让用户只依赖一个稳定入口。

可以这样实践,创建一个最小项目:

example/
├── demo.py
└── toolkit/
    ├── __init__.py
    ├── errors.py
    └── formatting.py

toolkit/formatting.py

def format_name(first, last):
    return f"{first.strip().title()} {last.strip().title()}"


def _clean(value):
    return value.strip()

toolkit/errors.py

class InvalidNameError(ValueError):
    pass

toolkit/__init__.py

from .errors import InvalidNameError
from .formatting import format_name

__all__ = ["format_name", "InvalidNameError"]

demo.py

from toolkit import InvalidNameError, format_name


def main():
    try:
        print(format_name("  ada", "lovelace  "))
    except InvalidNameError as exc:
        print(f"invalid input: {exc}")


if __name__ == "__main__":
    main()

example 目录中运行:

python demo.py

预期输出:

Ada Lovelace

调用方不必知道 format_name 位于 formatting.py。以后即使内部模块重新组织,只要 toolkit 的公开导入路径保持不变,使用方通常不需要修改代码。

需要注意,包的 __all__ 只列出名称还不够。相应名称必须已经在包命名空间中存在,因此示例先执行了 from .formatting import format_name,再把 format_name 写入 __all__

不要让 __all__ 变成重复维护的陷阱

公开 API 最适合使用显式、静态的列表:

__all__ = [
    "Client",
    "ClientError",
    "connect",
]

不建议根据 globals() 动态生成导出列表。动态写法可能意外公开导入的依赖、辅助常量或未来新增的内部对象,也会让代码审查者难以判断兼容性变化。

维护时可以检查以下事项:

  • __all__ 中的每个字符串都对应真实存在的名称。
  • 删除或重命名已公开名称时,按 API 破坏性变更处理。
  • 内部帮助函数使用前导下划线,同时不要放入 __all__
  • 包级 __init__.py 只重新导出真正需要稳定支持的对象。
  • 业务代码尽量使用显式导入,避免 import * 引发名称冲突和来源不明。

__all__ 的价值不在于鼓励通配符导入,而在于迫使模块作者回答一个关键问题:这个模块承诺长期支持哪些名字?当答案被写进代码,模块重构、文档生成和兼容性审查都会更清晰。


相关推荐