用 __all__ 管好 Python 模块的公开 API

2026-07-28 35 预计阅读时间: 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.

预计阅读时间:8 分钟

__all__ 管好 Python 模块的公开 API

Python 模块里的函数、类和常量,默认都可以通过属性访问。但“可以访问”不等于“应该公开”。当调用方执行 from module import * 时,模块级变量 __all__ 决定哪些名称会被导入;在包的 __init__.py 中,它也能帮助维护者明确包级 API 的边界。

这使 __all__ 不只是通配符导入的开关,更是一份可执行的公开接口清单。不过,它不是访问控制机制,也不会让内部对象真正变成私有对象。

__all__ 实际控制什么

__all__ 通常是一个由字符串组成的列表或元组,每个字符串对应模块命名空间中的一个名称:

# geometry.py

__all__ = ["Circle", "area"]

from math import pi

DEFAULT_UNIT = "cm"


class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius


def area(circle: Circle) -> float:
    return pi * circle.radius**2


def _validate_radius(radius: float) -> None:
    if radius < 0:
        raise ValueError("radius must be non-negative")

执行通配符导入时,只有 Circlearea 会进入调用方的命名空间:

from geometry import *

circle = Circle(2)
print(area(circle))

print(DEFAULT_UNIT)  # NameError:不在 __all__ 中

如果模块没有定义 __all__from geometry import * 通常会导入所有不以下划线开头的名称。因此,上例中的 DEFAULT_UNIT 以及导入进来的 pi 都可能被意外暴露,而 _validate_radius 会因下划线前缀被排除。

显式导入不受这份清单限制:

from geometry import DEFAULT_UNIT

print(DEFAULT_UNIT)

这段代码仍然有效。由此可以看出,__all__ 描述的是模块承诺公开的名称,主要控制通配符导入,而不是阻止外部访问。

在包入口统一公开接口

真实项目经常把实现拆到多个模块,却希望用户只从包根目录导入稳定接口。可以在 __init__.py 中重新导出对象,并用 __all__ 记录包级 API。

下面是一个可以直接创建并运行的最小项目:

shapes_demo/
├── demo.py
└── shapes/
    ├── __init__.py
    ├── circle.py
    └── rectangle.py

创建 shapes/circle.py

from math import pi


class Circle:
    def __init__(self, radius: float) -> None:
        if radius < 0:
            raise ValueError("radius must be non-negative")
        self.radius = radius

    def area(self) -> float:
        return pi * self.radius**2

创建 shapes/rectangle.py

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        if width < 0 or height < 0:
            raise ValueError("dimensions must be non-negative")
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

shapes/__init__.py 中建立包级入口:

from .circle import Circle
from .rectangle import Rectangle

__all__ = ["Circle", "Rectangle"]

然后编写 demo.py

from shapes import *

items = [Circle(2), Rectangle(3, 4)]

for item in items:
    print(f"{type(item).__name__}: {item.area():.2f}")

shapes_demo 目录运行:

python demo.py

输出类似:

Circle: 12.57
Rectangle: 12.00

调用方现在不需要知道 Circle 位于 circle.py。以后即使调整内部文件结构,只要 from shapes import Circle 保持有效,包的公开接口就可以继续兼容。

需要注意,单独写下 __all__ = ["Circle"] 并不会自动创建 Circle 这个名称。包入口仍然需要执行 from .circle import Circle,否则通配符导入时可能遇到缺失属性或名称的错误。__all__ 是清单,不是导入语句的替代品。

为什么业务代码仍应避免 import *

维护 __all__ 并不意味着项目应该广泛使用通配符导入。显式导入通常更容易阅读和重构:

from shapes import Circle, Rectangle

看到这行代码,开发者立刻知道当前文件依赖哪些对象。相比之下,from shapes import * 会把名称来源隐藏起来,还可能与当前模块中的变量或其他通配符导入发生冲突。

__all__ 更重要的价值是让库作者主动定义 API:

  • 防止辅助函数、依赖模块和临时常量被通配符导入意外带出。
  • 在包根目录集中重新导出常用对象,缩短用户的导入路径。
  • 让代码审查者能直接看到公开接口发生了哪些变化。
  • 为模块文档、自动补全和 API 稳定性提供更清晰的意图信号,但具体工具是否读取它取决于工具实现。

维护时容易踩的边界

__all__ 中的名称必须与模块中真实存在的名称保持同步。重命名类或删除函数时,如果忘记更新清单,问题可能直到执行通配符导入才暴露。可以给公开接口增加一个小测试:

# test_public_api.py
import shapes


def test_public_api_is_complete() -> None:
    assert set(shapes.__all__) == {"Circle", "Rectangle"}
    assert all(hasattr(shapes, name) for name in shapes.__all__)

使用 pytest 运行:

python -m pytest -q

还有几个边界需要明确:

  • 下划线前缀只是“不公开”的惯例,不是权限控制。
  • __all__ 同样不是安全机制;调用方仍可显式导入或访问内部名称。
  • 把一个名称加入 __all__,通常意味着维护者准备把它当作稳定 API 对待。
  • 不必在每个内部模块机械地添加 __all__。它更适合公共模块、包入口,以及容易泄露大量实现名称的模块。
  • 动态拼装 __all__ 会让审查和静态分析更困难,公开 API 较小时应优先使用明确的字符串列表。

一份实用的采用清单

引入 __all__ 时,可以先从包的 __init__.py 和用户直接导入的模块开始。确保列表中的每个名称都已绑定,在测试中校验清单,并在删除名称前考虑兼容性。

与此同时,应用代码继续使用显式导入。这样,__all__ 负责表达库的边界,具体导入语句负责表达调用方的依赖,两者分工清楚,包的内部结构也更容易演进。


相关推荐