2026年7月18日
声明:我并非Python专家,本文只是我在探索Python类型检查器时的一些记录。或许已经有更好的方案,只是我不知道罢了。我也不确定是否已经有人写过类似思路的文章,尽管我没能找到。
先从一个表达式AST的类型定义开始。为了简单起见,这里只包含加法和乘法。本文使用Python 3.14.6和mypy 2.1.0。
from dataclasses import dataclass
from typing import Callable, assert_never
@dataclass
class Add:
lhs: Expr
rhs: Expr
@dataclass
class Mul:
lhs: Expr
rhs: Expr
type Expr = int | Add | Mul
我们可以为Expr写一个evaluate操作:
def evaluate(e: Expr) -> int:
match e:
case int(): return e
case Add(l, r): return eval(l) + eval(r)
case Mul(l, r): return eval(l) * eval(r)
case _ as never: assert_never(never)
设想这段代码位于某个库中,用户无法修改它,却又想扩展它。用户想要的是:
Add和Mul之外再加一个Minus);pretty_print);维基百科上的定义也与此类似:
目标是定义一种数据抽象,使其在表示和行为两方面都可扩展:既能为该数据抽象添加新的表示,也能添加新的行为,同时无需重新编译现有代码,并且保持静态类型安全(例如,不使用类型转换)。
虽然同时达成上述三个目标有些困难,但达成其中任意两个都相当简单。
在上面的例子中,我们其实已经实现了类型安全和操作可扩展性:只需定义一个对Expr进行模式匹配的函数即可。
使用传统的面向对象方法——定义一个基类并对其派生子类——可以轻松实现类型安全和数据类型的可扩展性。然而,它不擅长实现操作的可扩展性。你必须定义新的visitor来扩展新操作,而visitor没有穷尽性检查。这篇博客文章用非常漂亮的图示说明了它们的缺陷。
Python还引入了@singledispatch。它与Haskell中的typeclass或Lisp中的defgeneric非常相似。借助@singledispatch,你可以同时获得数据类型和操作两方面的可扩展性。然而,类型安全丢失了——被singledispatch修饰的函数,其参数类型会变成Any,因此你完全可以用非法的数据类型作为参数去调用它。而在像Haskell这样带有类型约束的语言中,对操作的递归调用会成为一个问题,况且Haskell的数据类型是封闭的。这正是Haskell为解决表达式问题不得不诉诸诸如Data Types à la Carte这样复杂方案的原因。
让我们从上面Expr的例子开始。我们首先修改这两个AST节点的定义,把它们变成泛型类型:
@dataclass
class Add[T]:
lhs: T
rhs: T
@dataclass
class Mul[T]:
lhs: T
rhs: T
然后我们定义一个新的泛型联合类型:
type ExprBase[T] = int | Add[T] | Mul[T]
再定义Expr,使其自引用:
type Expr = ExprBase[Expr]
可以看到,这里新定义的Expr本质上和之前的Expr是一样的。但泛型为可扩展性留出了空间。
如果我们像之前那样直接写“evaluate(expr: Expr) -> int”,那么evaluate就会封闭在Expr之上,失去扩展的余地。因此,我们把递归调用的操作作为一个参数传入,并像上面的数据类型那样,把这个操作也变成泛型:
def evaluate_base[T](recur: Callable[[T], int], expr: ExprBase[T]) -> int:
match expr:
case int():
return expr
case Add(lhs, rhs):
return recur(lhs) + recur(rhs)
case Mul(lhs, rhs):
return recur(lhs) * recur(rhs)
case _ as never:
assert_never(never)
然后我们定义一个包装用的操作:
def evaluate(expr: Expr) -> int:
return evaluate_base(evaluate, expr)
现在我们添加一个Minus节点:
@dataclass
class Minus[T]:
lhs: T
rhs: T
新的Expr类型将是:
type Expr_v2= Minus[Expr_v2] | ExprBase[Expr_v2]
为了对Minus求值,我们扩展evaluate函数:
def evaluate_v2(e: Expr_v2) -> int:
match e:
case Minus(lhs, rhs):
return evaluate_v2(lhs) - evaluate_v2(rhs)
case _:
return evaluate_base(evaluate_v2, e)
现在数据类型得到了扩展,而且类型安全完好无缺。
如前所述,在Union类型上扩展操作是一个已经解决的问题:
def pretty_print(e: Expr2) -> None:
match e:
case int(): pass
case Add(l, r): pass
case Mul(l, r): pass
case Minus(l, r): pass
case _ as never: assert_never(never)
虽然我认为这是解决表达式问题的一个不错的方案,达成了全部三个目标,但另一方面,它也引入了相当多的面条样板代码。同时,正如我前面所说,或许已经有更好的方案只是我不知道,又或者这个方法存在某些我尚未意识到的缺陷。
Email: i (at) mistivia (dot) com