使用 Python 的类型注解构建稳健的代码库
Hudson River Trading (HRT) 的 Python 代码库庞大且不断发展。数百万行 Python 代码凝聚了数百名开发人员在过去十年中的辛勤工作。我们在全球 200 多个市场进行交易——包括几乎所有电子市场——因此我们需要定期更新代码以应对不断变化的规则和法规。
我们的代码库提供命令行界面 (CLI) 工具、图形用户界面 (GUI) 和事件触发进程,以协助我们的交易员、工程师和运营人员。代码库的这一外层由共享业务逻辑的内层支持。业务逻辑通常比看起来更复杂:即使是“纳斯达克的下一个工作日是哪一天?”这样一个简单的问题,也需要查询市场日历数据库(一个需要定期维护的数据库)。因此,通过将此业务逻辑集中到一个单一的事实来源,我们确保代码库中所有不同的系统都保持一致的行为。
即使是共享业务逻辑的一个小改动也可能影响许多系统,我们需要检查这些系统不会因我们的改动而出现问题。人工手动验证没有问题是低效且容易出错的。Python 的类型注解显著提高了我们更新和验证共享业务逻辑更改的速度。
类型注解允许您描述代码处理的数据类型。“类型检查器”是根据代码的实际使用方式来协调您的描述的工具。当我们更新共享业务逻辑时,我们会更新类型注解并使用类型检查器来识别受影响的任何下游系统。
我们还对代码库进行了全面的文档编写和测试。但是书面文档不会自动与底层代码同步,因此维护文档需要高度的警惕性,并且容易出现人为错误。此外,自动化测试仅限于我们测试的场景,这意味着我们共享业务逻辑的新用途在添加新测试之前将无法验证。
让我们看一个类型注解的例子,了解它们如何用于描述数据的形状。这里是一些类型注解的 Python 代码,用于计算 CUSIP 的校验位。
def cusip_checksum(cusip8: str) -> int:
assert len(cusip8) == 8
chars: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ*@#"
charmap: dict[str, int] = {
char: value
for value, char in enumerate(chars, start=0)
}
total: int = 0
for idx, char in enumerate(cusip8, start=0):
value: int = charmap[char]
if (idx % 2) == 1:
value *= 2
total += (value // 10) + (value % 10)
return (10 - total % 10) % 10
以下是类型注解告诉我们的内容:
cusip_checksum()是一个函数,接受字符串作为输入,返回整数作为输出。chars是一个字符串。charmap是一个字典,键为字符串,值为整数。total和value是整数。
HRT 使用 mypy 来分析我们的 Python 类型注解。Mypy 通过分析一个或多个 Python 文件中的类型注解,并确定是否存在任何问题或不一致之处来工作。

大多数情况下,mypy 擅长类型推断,因此最好关注函数参数和返回值的注解,而不是函数中使用的内部变量。
这里有一个新函数 validate_cusip(),它依赖于前面提到的 cusip_checksum() 函数:
def cusip_checksum(cusip8: str) -> int:
...
def validate_cusip(cusip: str) -> str | None:
checksum: int
if len(cusip) == 9:
checksum = cusip_checksum(cusip[:8])
if str(checksum) == cusip[8]:
return cusip
else:
return None
elif len(cusip) == 8:
checksum = cusip_checksum(cusip)
return f"{cusip}{checksum}"
else:
return None
Mypy 对这段代码表示满意。
Success: no issues found in 1 source file
现在,假设我们决定更新 cusip_checksum(),如果它检测到 CUSIP 无效,则返回 None。
def cusip_checksum(cusip8: str) -> int | None:
if len(cusip8) != 8:
return None
chars: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ*@#"
charmap: dict[str, int] = {
char: value
for value, char in enumerate(chars, start=0)
}
total: int = 0
for idx, char in enumerate(cusip8, start=0):
try:
value: int = charmap[char]
except KeyError:
return None
if (idx % 2) == 1:
value *= 2
total += (value // 10) + (value % 10)
return (10 - total % 10) % 10
Mypy 自动检测到 validate_cusip() 使用 cusip_checksum() 的问题。
error: Incompatible types in assignment (expression has type "int | None", variable has type "int") [assignment]
既然我们已经收到警告,我们可以更新 validate_cusip() 以处理这些更改。
def cusip_checksum(cusip8: str) -> int | None:
...
def validate_cusip(cusip: str) -> str | None:
if len(cusip) == 9:
match cusip_checksum(cusip[:8]):
case int(checksum) if str(checksum) == cusip[8]:
return cusip
elif len(cusip) == 8:
match cusip_checksum(cusip):
case int(checksum):
return f"{cusip}{checksum}"
return None
在这个例子中,函数在源代码中是相邻的。但是当函数分布在代码库中的许多文件中时,Mypy 真正发挥了作用。
总而言之,类型注解对于使您的代码库更加健壮具有显著的优势。它们并非全有或全无的提议——您可以专注于向代码库的一小部分添加类型注解,并随着时间的推移增加类型注解代码的数量。与其他技术一起,Python 的类型注解帮助 HRT 在快节奏的全球交易世界中继续蓬勃发展。
本文最初发表于 HRT Beat。
认识作者
John Lekberg 在 HRT 负责各种 Python 和 gRPC 系统。他主要开发和完善用于监控和警报的内部工具。他还领导了将静态分析工具应用于 HRT 代码库的倡议,从而捕获错误并减少了代码审查所需的人工工作。
