用 __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")
执行通配符导入时,只有 Circle 和 area 会进入调用方的命名空间:
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__ 负责表达库的边界,具体导入语句负责表达调用方的依赖,两者分工清楚,包的内部结构也更容易演进。