Python 语法与工程骨架

想在大型项目里写出“既优雅又不掉坑”的 Python?本文用 抽象基类 → 生成器 → 类型提示 → 模块组织 四步,给你一套可复用的工程骨架。

📚 省流图

主题 一句话记忆
抽象基类 from abc import ABC, abstractmethod → 逼子类覆写接口
生成器 yield → 一次返回多批结果,for 自动迭代
类型提示 Tuple[int, ...] 读作“不可变整型序列”
模块路径 package.sub.module → 对应 package/sub/module.py

1️⃣ 面向对象:ABC、继承、重写

1
2
3
4
5
6
7
8
9
10
11
12
13
from abc import ABC, abstractmethod

class AbsSolver(ABC):
"""抽象求解器:定义统一接口"""

@abstractmethod
def solve(self, *args, **kwargs):
"""计算一次解,子类必须实现"""
pass

def dump(self):
"""可选钩子:子类按需重写"""
print("<default dump>\n", self.__dict__)
  • ABCAbsSolver 继承自 ABC,内部 至少有一个 @abstractmethod
  • 强制实现:实例化子类前,Python 会检查是否实现了所有抽象方法;否则 TypeError
  • 可选覆写dump 不是抽象的,子类可按需扩展。

@abstractmethod 用法速览

1
2
3
4
5
6
7
8
9
10
class Base(ABC):
@abstractmethod
def foo(self): # 无实现体或仅 raise NotImplementedError
raise NotImplementedError

class Impl(Base):
def foo(self): # ✅ 覆写,才能实例化
print("ok")

Impl().foo() # 若省略 foo → TypeError: Can't instantiate abstract class

省流:ABC = “接口”,“抽象方法” = “必须实现”。

🔗 什么是“钩子 (Hook)”?

  • 非抽象、带默认实现的方法,子类可选地覆写。
  • 框架在合适时机回调它,让开发者“插入自定义逻辑”。
  • 例:dump() 就是调试钩子,PyTorch 的 forward_hook 同理。

🦆 什么是“鸭子接口 / Duck Typing”?

  • 不关心对象真实类型,只要 行为/look like a duck 就当成合法。
  • ops.pypass 而非 @abstractmethod,就是“鸭子接口”:谁实现了同名方法谁就能用。
  • 优点:灵活、少耦合;缺点:漏实现时运行期才炸。

记忆:Hook = 可选扩展点Duck Typing = 不验血统,只看会不会叫、会不会游。


2️⃣ 生成器与迭代:持续产出多解

ops.py 里,materialize()yield 按需吐出 不同框架的层对象:

1
2
3
4
5
6
7
8
9
class AbsBaseOp(ABC):
# ...
def materialize(self, framework):
solver = z3.Solver()
while solver.check() == z3.sat:
self.model = solver.model()
yield self._to_framework(framework) # 产出一次解
self._add_diff_constraint(solver) # 追加“与上次不同”的约束
yield None # 无更多解
  • yield:函数变生成器,for layer in op.materialize('torch'): 可逐层迭代。
  • 多解采样:在循环内 动态给求解器加“不等”约束,驱动 Z3 返回新模型。
  • 终止信号:最后 yield None 让上层知道“解完了”。

yield from 简洁重用

1
2
3
def pipeline(frameworks):
for fw in frameworks:
yield from self.materialize(fw) # 直接把子生成器产物向上透传

🏃 如何读取 / 消费生成器?

  1. for-loop 最省心
    1
    2
    3
    4
    for layer in op.materialize('torch'):
    if layer is None:
    break # 无更多解
    print(layer)
  2. next() 手动拉取(需要捕获 StopIteration
    1
    2
    3
    4
    5
    6
    7
    8
    9
    g = op.materialize('paddle')
    try:
    while True:
    layer = next(g)
    if layer is None:
    break
    use(layer)
    except StopIteration:
    pass
  3. 切片取前 N 个import itertools; first5 = list(itertools.islice(op.materialize('tf'), 5))

记忆:yield 负责“产”,for / next / islice 负责“取”。


3️⃣ 类型提示速读

写法 读法 示例
List[int] 可变 整型列表 [1, 2, 3]
Tuple[str, int] 长度固定 (str, int) ("id", 3)
Tuple[int, ...] 不定长整型元组 (1,2,3,4)
Dict[str, Any] key 是 str, value 任意 {"a":1}
Union[int, str] int str 42 / "42"
Optional[Foo] Union[Foo, None] 可能返回 None

检查工具:mypy, pyright,CI 一跑便知类型是否对。


4️⃣ 模块组织与导入路径

1
2
3
4
5
6
7
8
project/
├─ ops/ # 功能包
│ ├─ __init__.py # 导入门面,暴露 API
│ ├─ base.py # 抽象基类与通用工具
│ ├─ nn.py # 神经网络相关 Op
│ └─ shape.py # 形状求解逻辑
├─ scripts/ # CLI / demo / benchmark
└─ tests/ # pytest 单元测试
  • 相对导入:包内部用 from .base import AbsBaseOp,防止顶层路径污染。
  • 绝对导入:外部调用写 from ops.nn import ConvOp,IDE/类型检查友好。
  • __init__.py:在包级暴露高层 API,隐藏实现细节。


flowchart TD
    subgraph "ops 包"
      base["base.py\nAbsBaseOp"]
      nn["nn.py\nConvOp / PoolOp"]
      shape["shape.py\nZ3 求解"]
    end
    scripts["scripts/train.py"] --> nn
    scripts --> base
    nn --> shape


📝 小结

  1. ABC + 抽象方法 → 先定接口,再写实现。
  2. 生成器 (yield) → 惰性产出多解/多批数据,节省内存。
  3. TypingTuple[int, ...]=不定长;Union=多选;配合 IDE CI 早点发现错。
  4. 模块化 → “包内相对、包外绝对”,在 __init__.py 只输出你想给用户看的接口。

熟练掌握这四招,你的 Python 项目就能又 清晰可维护 🚀