使用 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 代码库的倡议,以捕获错误并减少审查代码所需的手动工作。