Python 的生态之所以能持续演进,靠的不是某个核心团队的独断,而是 PEP(Python Enhancement Proposal)这套公开提案机制。Real Python 第 297 期播客里,Brett Cannon 回来聊了他最近参与的几项 PEP——从模块命名混乱、虚拟环境目录约定,到 Protocol(结构化子类型)。这些问题看似琐碎,但每天都在消耗开发者的注意力。下面逐个拆开看。
模块命名:为什么你总在 import 时犹豫
打开一个典型项目,顶层目录可能同时存在 utils.py 和 utils/ 包,还有 __init__.py 里塞满重导出。调用者写 from utils import load_config,脑子里要过一遍:这是从文件来的,还是从包的 __init__.py 来的?如果包里又有个 utils.py,循环导入就埋下了。
Brett Cannon 在播客里提到,这类混乱源于社区缺少一套被广泛采纳的命名约定。PEP 8 只管了变量和函数的风格,对包与模块的层级组织几乎没有硬性规定。实践中可以遵循几条自洽原则:
- 顶层模块用短小、无歧义的名字——
cli.py、config.py、models.py,不要叫myapp_utils.py。 - 包目录和同名模块不要共存——要么
validators.py,要么validators/包,别两者都有。 __init__.py只做显式重导出——用__all__列表声明公开 API,其余逻辑放到子模块。
下面是一个可改造的包结构示例,把混乱的 utils 整理干净:
# 原始混乱结构
myapp/
utils.py # 和 utils/ 包共存,导入时到底走哪个?
utils/
__init__.py # 塞了 200 行代码
io.py
strings.py
# 整理后
myapp/
io.py # 顶层模块,职责单一
strings.py # 顶层模块,职责单一
validators/ # 包,__init__.py 只做重导出
__init__.py
email.py
phone.py
validators/__init__.py 的写法:
"""Validators package — 只做重导出,不放业务逻辑。"""
from .email import validate_email
from .phone import validate_phone
__all__ = ["validate_email", "validate_phone"]
调用侧就干净了:
from validators import validate_email # 明确来自包的公开 API
from io import read_csv # 明确来自顶层模块
虚拟环境命名:.venv 正在成为事实标准
播客另一个话题是虚拟环境的目录命名。你一定见过这些:env/、venv/、.env/、myproject-env/……团队里每个人习惯不同,.gitignore 要写一堆路径,IDE 配置也要反复适配。
Brett Cannon 推动的一个方向是让 .venv 成为社区约定:以点号开头(被 ls 默认忽略、大多数 .gitignore 模板已覆盖),名字固定(工具链和 IDE 可以硬编码识别)。这不是一个正式 PEP,但已经在多个工具中形成共识。
实际操作:
# 创建虚拟环境,统一用 .venv
python -m venv .venv
# 激活
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# .gitignore 只需一行(大多数模板已包含)
echo ".venv/" >> .gitignore
如果你用 direnv 或 pyenv,可以配合自动激活:
# .envrc(direnv 配置,放在项目根目录)
layout python
# 或手动指定:
source .venv/bin/activate
这样 cd 进项目目录就自动进入虚拟环境,不用每次手动 source。
Protocol:不靠继承的接口约定
播客标题里的 "Protocols" 指的是 PEP 544 引入的结构化子类型(structural subtyping)。传统做法是定义一个抽象基类(ABC),让子类显式继承。问题在于:第三方库的类没法让你的 ABC 插进去,跨库协作时继承链很快就拧成麻花。
Protocol 的思路是鸭子类型的正式版——只要你的类有协议要求的方法,类型检查器就认为它满足协议,不需要注册或继承。
一个实际场景:你写了一个数据导出系统,要求对象提供 to_dict() 方法。用 ABC 的做法:
from abc import ABC, abstractmethod
class Exportable(ABC):
@abstractmethod
def to_dict(self) -> dict: ...
# 第三方库的 User 类没法回头继承 Exportable
# 你只能写 adapter 或用 register() hack
换成 Protocol:
from typing import Protocol
class Exportable(Protocol):
def to_dict(self) -> dict: ...
# 任何有 to_dict() -> dict 的类自动满足协议
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def to_dict(self) -> dict:
return {"name": self.name, "email": self.email}
def export(obj: Exportable) -> str:
import json
return json.dumps(obj.to_dict())
user = User("Alice", "alice@example.com")
print(export(user)) # ✅ User 没继承任何东西,mypy 也认可
运行输出:
{"name": "Alice", "email": "alice@example.com"}
关键区别:User 完全不知道 Exportable 的存在,但 export() 函数的签名约束已经生效。mypy 在检查 export(user) 时,会验证 User 是否有 to_dict() -> dict 方法——有就通过,没有就报错。
如果想让运行时也有显式检查(比如做防御性编程),可以用 runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Exportable(Protocol):
def to_dict(self) -> dict: ...
# 运行时可以用 isinstance 做粗略检查(只验证方法存在,不验证签名)
assert isinstance(user, Exportable)
注意:runtime_checkable 的 isinstance 只检查方法名是否存在,不检查参数和返回类型。真正的类型安全还是靠静态检查。
落地清单
把这些想法用到项目里,不需要一次全改。以下是按优先级排列的实操清单:
| 优先级 | 动作 | 成本 | 收益 |
|---|---|---|---|
| 🔴 高 | 虚拟环境统一用 .venv,更新 .gitignore 和文档 |
5 分钟 | 团队不再纠结 env 名,IDE 一致识别 |
| 🔴 高 | 消除包与同名模块共存,__init__.py 只做重导出 |
半小时 | 导入路径不再有歧义 |
| 🟡 中 | 新代码用 Protocol 替代 ABC 定义接口约定 |
写法微调 | 跨库协作不再需要继承 hack |
| 🟢 低 | 给现有 ABC 补 Protocol 版本,逐步迁移 |
渐进式 | 类型检查覆盖面扩大 |
PEP 的价值不只是"将来 Python 会加什么特性",而是把社区正在踩的坑变成可讨论、可收敛的提案。模块命名和 venv 目录看起来是小事,但每个项目都在重复决策;Protocol 则直接改变了你写接口的方式。下次遇到"这个 import 到底从哪来"或"怎么让第三方类满足我的接口"时,先看看有没有对应的 PEP 已经给出了方向。