写一个「能跑」的脚本容易,写一个「好用」的库难。本文从 Python 库作者视角,拆解公开 API 面设计、兼容性保障与文档工程的完整方法论。
目录
1. 库 vs 应用:设计目标差异
| 维度 | 应用 | 库 |
|---|---|---|
| 使用者 | 你(可控) | 陌生用户(不可控) |
| 变更 | 随意改 | 必须兼容 |
| 日志 | 随便打 | 用 logger = logging.getLogger(__name__) 但要克制 |
| 依赖 | 随意 | 尽量少,避免依赖地狱 |
| 错误 | 可崩溃 | 抛明确异常 |
| 接口 | 内部实现 | 长期契约 |
第一原则:库作者要「克制」。每个新增功能都是未来要维护的契约。
2. 包结构与模块划分
my_library/
├── pyproject.toml
├── src/
│ └── my_library/ # src 布局(推荐)
│ ├── __init__.py # 公开 API 出口(薄)
│ ├── _core.py # 内部实现(下划线 = 私有约定)
│ ├── api/
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── models.py
│ └── py.typed # 标记类型标注存在
├── tests/
├── docs/
└── README.md
# src/my_library/__init__.py —— 只导出公开 API
from .client import Client
from .models import User, Config
__all__ = ["Client", "User", "Config"]
__version__ = "1.2.0"
结构要点:
| 约定 | 原因 |
|---|---|
src/ 布局 | 避免测试误 import 安装前代码 |
_private.py | 下划线表示内部,不承诺兼容 |
薄 __init__ | 减少 import 副作用与命名空间污染 |
py.typed | 让类型检查器识别库的类型信息 |
3. 公开 API 面设计
3.1 一致性命名
# ✅ 动词 + 宾语、返回语义一致
client.get_user(id)
client.create_user(user)
client.delete_user(id)
# ❌ 同一库风格混乱
fetchUser(id) # camelCase
user = getUser(id) # 另一个
3.2 参数设计
def fetch_data(
url: str,
*,
timeout: float = 30.0, # 关键字-only,防误传
retries: int = 3,
headers: dict | None = None, # None 表示「用默认」
) -> Response:
...
| 设计点 | 最佳实践 |
|---|---|
| 必选参数 | 位置参数,语义清晰 |
| 可选参数 | 关键字参数 * 之后 |
| 可变参数 | 用 *args/**kwargs 但要文档化 |
| 布尔参数 | 避免裸 flag=True,用枚举或关键字 |
3.3 返回与副作用
# ✅ 查询不改变状态,纯函数
def get_status(client) -> Status: ...
# ✅ 明确的就地修改要命名清楚
def sort_inplace(items) -> None: ...
# ❌ 隐含副作用
def process(data): ... # 到底改没改 data?
4. 类型标注与 Protocol
4.1 完整标注
from typing import TypeVar, Protocol, Generic
T = TypeVar("T")
class Repository(Protocol[T]):
def get(self, id: int) -> T: ...
def save(self, obj: T) -> None: ...
class UserRepo(Repository[User]):
def get(self, id: int) -> User: ...
def save(self, obj: User) -> None: ...
4.2 泛型与重载
from typing import overload
@overload
def load(path: str) -> str: ...
@overload
def load(path: str, binary: bool) -> bytes: ...
def load(path: str, binary: bool = False) -> str | bytes:
mode = "rb" if binary else "r"
with open(path, mode) as f:
return f.read()
4.3 标注的意义
| 受众 | 收益 |
|---|---|
| IDE | 自动补全、跳转、重构 |
| 类型检查器 | 静态发现错误 |
| 文档 | 可直接生成 API 文档 |
| 读者 | 读代码不迷茫 |
5. 语义化版本与兼容性
MAJOR.MINOR.PATCH:
| 版本 | 变更 | 含义 |
|---|---|---|
MAJOR | 破坏性变更 | API 不再兼容 |
MINOR | 新增向后兼容功能 | 新 API |
PATCH | Bug 修复 | 内部修正 |
Python 特有:
| 变更类型 | 属于 |
|---|---|
| 新增函数/类 | MINOR |
| 新参数(带默认值) | MINOR |
| 新异常类型 | MINOR |
| 移除参数/函数 | MAJOR |
| 行为变化 | MAJOR |
加 py.typed | MINOR(但可能让用户暴露类型错误) |
0.x 版本:
0.1→0.2允许破坏性变更(尚未稳定)。
6. 向后兼容策略
6.1 弃用(Deprecation)流程
import warnings
def old_api():
warnings.warn(
"old_api 已弃用,请使用 new_api",
DeprecationWarning,
stacklevel=2,
)
return new_api()
弃用时间线:弃用(带警告)→ 保留 2+ minor → 下个 MAJOR 移除。
6.2 保留参数的兼容垫片
def connect(host: str, port: int, *, password=None):
# 老签名 password 是位置参数,新版本改为关键字
...
6.3 谨慎默认值
# 默认值一旦发布就是契约
def timeout_parse(text: str, default: float = 30.0) -> float:
...
# ❌ 不要后续悄悄改默认值(用户可能依赖)
6.4 用 __slots__ 控制数据类兼容
from dataclasses import dataclass
@dataclass(frozen=True) # 冻结:用户不能乱改,契约稳定
class Config:
api_key: str
timeout: float = 30.0
7. 文档工程
7.1 docstring 规范(Google/Numpy style)
def create_user(client, name, *, age=None):
"""创建用户。
Args:
client: 已认证的客户端实例。
name: 用户名。
age: 可选年龄。
Returns:
User 对象。
Raises:
APIError: 服务端返回错误。
"""
7.2 文档生成
# 安装
pip install mkdocs-material mkdocstrings
# mkdocs.yml
site_name: My Library
plugins:
- mkdocstrings:
handlers:
python:
options: { show_source: false }
theme:
name: material
features: [navigation.tabs]
7.3 好文档的四个组成部分
| 部分 | 内容 |
|---|---|
| 快速开始 | 3 行跑起来的示例 |
| 核心概念 | 一图说明设计意图 |
| API 参考 | 每个公开函数/类 |
| 迁移指南 | 各版本升级注意 |
8. 错误设计
8.1 异常层级
class MyLibraryError(Exception):
"""库内所有异常基类"""
class ConfigError(MyLibraryError): ...
class ConnectionError(MyLibraryError): ...
class TimeoutError(MyLibraryError): ...
class ValidationError(MyLibraryError):
def __init__(self, errors: list[str]):
self.errors = errors
super().__init__(f"校验失败: {errors}")
8.2 错误设计原则
| 原则 | 做法 |
|---|---|
| 基类捕获 | 用户可 except MyLibraryError 一把抓 |
| 具体子类 | 精细处理 |
| 不吞异常 | 让用户决定怎么处理 |
| 附带上下文 | from e 保留原因 |
| 错误信息 | 说清楚「什么、在哪、怎么修」 |
# ✅ 错误信息可行动
raise ConfigError("配置缺少 api_key,请在 config.toml 中设置")
# ❌ 无信息
raise Exception("failed")
9. 发布与社区治理
9.1 发布清单
# 发布前
python -m pytest && ruff check && mypy .
python -m build && twine check dist/*
# 发布
twine upload dist/*
# 版本号(semantic-release 或 bumpver)
bumpver update --patch
9.2 pyproject 完整示例
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-library"
version = "1.2.0"
description = "A great library"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
dependencies = ["httpx>=0.27"]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy", "mkdocs-material"]
9.3 社区治理
| 实践 | 说明 |
|---|---|
| CONTRIBUTING.md | 贡献流程、开发环境 |
| CODE_OF_CONDUCT | 社区准则 |
| ISSUE 模板 | 引导有效反馈 |
| 自动化 | CI 跑测试 + 发布 |
| 变更日志 | CHANGELOG 记录每版本 |
| 维护节奏 | 定期 triage issue |
10. 案例拆解与速查表
高质量库共同点(requests / pydantic / httpx):
| 库 | 可学点 |
|---|---|
| requests | 优雅的顶层 API(一个函数做一件事) |
| pydantic | 类型安全 + 验证 + 优秀错误信息 |
| httpx | 同步/异步双 API 并存 |
| click | 装饰器驱动,参数风格统一 |
| rich | 示例丰富,文档漂亮 |
API 设计速查:
| 场景 | 做法 |
|---|---|
| 顶层面 | from lib import X 直达 |
| 内部实现 | lib._internal 或子模块 |
| 参数默认值 | 发布即锁定 |
| 新增能力 | 新函数/新参数(带默认) |
| 破坏性变更 | 弃用 + MAJOR |
| 用户疑问 | 改善文档,不改变实现 |
| 异步支持 | 同步为主,异步可另加 |
最佳实践:
__init__薄,只导出公开 API。- 完整类型标注 +
py.typed。 - 所有公开项有 docstring。
- 异常有清晰层级。
- 版本升级走弃用流程。
- README 快速开始 ≤ 10 行。
一句话记忆:好库 = 薄接口 + 强类型 + 稳兼容 + 全文档 + 明异常;每次写库都假设「五年后有人依赖它」。
延伸阅读
- pyproject.toml 配置完全手册 —— 打包与发布配置
- Python 类型系统与 Pydantic V2 —— 泛型与 Protocol
- Python 测试与质量工程 —— 库质量保障
- [[java-enterprise]] —— Java 库设计对比
- [[typescript]] —— TS 库作者指南对比
库设计是「克制」的艺术:功能越少越稳,接口越薄越好用,兼容越久越受信任。当你开始写库,你就进入了与整个 Python 生态做朋友的关系。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。