CQ9节拍

使用Python的类型注解构建健壮的代码库
主题
发表

2024年2月16日

CQ9电子游戏(CQ9)的Python代码库很大*并且不断发展. 数百万行Python代码反映了数百名开发人员在过去十年中的工作. 我们在全球200多个市场进行交易,包括几乎所有的电子市场,因此我们需要定期更新我们的代码,以应对不断变化的规则和法规.

*要了解如何处理大规模代码组织,请参见: 构建Python代码库的依赖关系图.

我们的代码库提供了命令行界面(CLI)工具, 图形用户界面* (gui), 以及事件触发流程,帮助我们的交易者, 工程师, 以及操作人员. 代码库的外层由共享业务逻辑的内层提供支持. 商业逻辑往往比表面上看起来要复杂得多:即使是一个简单的问题,比如“纳斯达克的下一个交易日是什么时候”?,涉及查询市场日历数据库(需要定期维护的数据库)。. So, 通过将此业务逻辑集中到单个事实来源中, 我们确保代码库中所有不同系统的行为一致.

*要了解我们的GUI设计过程,请参见: 优化UX/UI设计在CQ9交易.

即使对共享业务逻辑进行很小的更改也会影响许多系统, 我们需要检查这些系统不会因为我们的改变而出现问题. 人工验证没有损坏是低效且容易出错的. Python的类型注释显著提高了我们更新和验证共享业务逻辑更改的速度.

类型注释允许您描述代码处理的数据类型. “类型检查器”是将您的描述与代码的实际使用方式相协调的工具. 当我们更新共享业务逻辑时, 我们更新类型注释并使用类型检查器来识别受影响的任何下游系统.

我们还对代码库进行了全面的记录和测试. 但是编写的文档不会自动与底层代码同步, 因此,维护文档需要高度警惕,容易出现人为错误. 另外, 自动化测试仅限于我们测试的场景,这意味着在我们添加新的测试之前,共享业务逻辑的新用途将无法验证.

*生成测试 假设 我们正在探索的解决传统自动化测试的一些缺陷的有前途的新途径是什么.

本文的其余部分将探讨:

  • 类型注释在Python中是如何工作的
  • CQ9使用的类型检查工具
  • 一个检测下游问题的示例
  • 有效使用类型注释的一些技巧和技巧

类型注释在Python中是如何工作的

让我们看一个类型注释的例子,看看如何使用它们来描述数据的形状. 下面是一些带类型注释的Python代码,用于计算a的校验和数字 CUSIP:

def cusip_checksum(cusip8: str) -> int:
    断言len(cusip8) == 8
    字符:str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ*@#"
    Charmap: dict[str, int] = {
        字符:价值
        对于值,枚举中的char (识字课, start=0)
    }
    总数:int = 0
    对于idx, char in enumerate(cusip8, start=0):
        取值:int = charmap[char]
        If (idx % 2) == 1:
            值*= 2
        Total += (价值 // 10) + (价值 % 10)
    返回(10 -总数% 10)% 10

下面是类型注解告诉我们的:

  • cusip_checksum () 一个函数接受一个字符串作为输入,并返回一个整数作为输出.
  • 识字课 是一个字符串.
  • charmap 字典是字符串键和整数值.
  • 总计。价值 都是整数.

您可能会注意到,类型注释并没有描述数据的所有内容. 例如:

  • 的输入 cusip_checksum () 应该是一个8个字符的字符串吗.
  • 的输出 cusip_checksum () 应该是0到9之间的整数吗.
  • charmap 字典的键是单字符字符串吗.

尽管Python不支持 细分类型 (然而),你可以用几种方法解决这个问题. 首先,这对您来说可能不是问题:只是提供的粒度级别 strint 对你的代码来说是否足够. 此外,Python的 打字 模块提供了用于更高级类型约束的工具,例如 NewType (), 文字[], TypeGuard [].

类型注解是在Python 3中添加的.通过Python增强提案(PEP):  PEP 484 -类型提示. 从那时起,已经出现了许多pep来进一步改进类型注释:

CQ9使用的类型检查工具

荷尔蒙替代疗法使用 mypy 来分析Python类型注释. Mypy的工作原理是分析一个或多个Python文件中的类型注释,并确定是否存在任何问题或不一致.

考虑到这个Python…我的朋友会说……
x = 3成功:在1个源文件中没有发现问题
X: int = 3成功:在1个源文件中没有发现问题
X: STR = 3错误:赋值中不兼容的类型(表达式的类型为“int”), 变量类型为“str”)[赋值]
在1个文件中发现1个错误(检查了1个源文件)

如果某些内容没有类型注释,mypy将尝试推断适当的类型. 如果您想调试它,可以使用特殊函数 reveal_type ()(仅在mymyy中可用).

考虑到这个Python…我的朋友会说……
x = 3
reveal_type (x)
注:显示的类型是“内置的”.int”
成功:在1个源文件中没有发现问题
Hex_digits = {
    '0': 0, '1': 1, '2': 2, '3': 3,
    '4': 4, '5': 5, '6': 6, '7': 7,
    '8': 8, '9': 9, 'A': 10, 'B': 11,
    ' c ': 12, ' d ': 13, ' e ': 14, ' f ': 15,
    “a”:10,“b”:11,“c”:12,“d”:13
    'e': 14, 'f': 15
}
reveal_type (hex_digits)

注:显示的类型是“内置的”.dict(内置命令.str,内置.int] "成功:在1个源文件中未发现任何问题 

大多数时候, Mypy擅长类型推断, 因此,最好将重点放在注释函数的参数和返回值上,而不是注释函数中使用的内部变量.

如果你的代码库还没有类型检查器,那么也值得评估mymyy的竞争对手: pytype(谷歌) pyright (微软), 火葬用的 (元). 要向现有Python代码中添加类型注释,请查看 MonkeyType复写.

一个检测下游问题的示例

这是一个新函数, validate_cusip (),这取决于 cusip_checksum () 前面的函数:

def cusip_checksum(cusip8: str) -> int:
    ...

def validate_cusip(cusip: str) -> str | 没有一个:
    校验和:int
    如果len(cusip) == 9:
        Checksum = cusip_checksum(cusip[:8])
        如果str(checksum) == cusip[8]:
            返回cusip
        其他:
            回来没有
    Elif len(cusip) == 8:
        Checksum = cusip_checksum(cusip)
        返回f”{cusip}{校验和}”
    其他:
        回来没有

我对这段代码很满意:

成功:在1个源文件中没有发现问题

现在,假设我们决定要更新 cusip_checksum () 返回 没有一个 如果检测到CUSIP无效:

def cusip_checksum(cusip8: str) -> int | 没有一个:
    如果len (cusip8) != 8:
        回来没有
    字符:str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ*@#"
    Charmap: dict[str, int] = {
        字符:价值
        对于值,枚举中的char (识字课, start=0)
    }
    总数:int = 0
    对于idx, char in enumerate(cusip8, start=0):
        试一试:
            取值:int = charmap[char]
        除了KeyError:
            回来没有
        If (idx % 2) == 1:
            值*= 2
        Total += (价值 // 10) + (价值 % 10)
    返回(10 -总数% 10)% 10

Mypy自动检测问题的方式 validate_cusip () 正在使用 cusip_checksum ():

错误:赋值时类型不兼容(表达式类型为"int | 没有一个"), 变量类型为“int”)[赋值]

既然我们收到了警报,就可以更新了 validate_cusip () 要处理这些变化:

def cusip_checksum(cusip8: str) -> int | 没有一个:
    ...

def validate_cusip(cusip: str) -> str | 没有一个:
    如果len(cusip) == 9:
        匹配cusip_checksum (cusip [8]):
            Case int(checksum) 如果str(checksum) == cusip[8]:
                返回cusip
    Elif len(cusip) == 8:
        匹配cusip_checksum (cusip):
            int(校验和):
                返回f”{cusip}{校验和}”
    回来没有

在这个例子中,这些函数在源代码中是紧挨着的. 但是当函数分布在代码库中的许多文件中时,mypy才真正发挥了作用.

有效使用类型注释的一些技巧和技巧 

以下是我从经验中学到的一些具体建议和技巧:


  1. 理解代码的层次. 在关注外层的类型注释之前,先关注在内层中使用类型注释.

  2. 了解类型注释的限制以及何时需要添加运行时验证. 例如,当您处理来自外部API的数据时, 打字.TypedDict 可能对类型注释有用, 但它不能保证外部API将实际提供该数据—您需要在运行时验证数据是否与预期的模式匹配.

  3. 注意使用基本类型(例如.g. Str, int)来表示领域概念(例如.g. 证券价格). 原始类型, 像str / int /浮动, 很容易理解, 但往往缺乏领域思想所需的约束. 例如,您可以像这样表示CUSIP my_cusip: str. 尽管CUSIP可以表示为字符串,但并非所有字符串都是CUSIP. CUSIP是9个字符,使用字母数字字符集,第9个字符是校验和. 对于这个例子, 您可以使用CUSIP类型注释来增加代码的健壮性, 通过:
    • 创建CUSIP dataclass (or attrs 类)它接受一个字符串,并在创建时验证该字符串是否为有效的CUSIP.
    • 用打字.表示CUSIP是一种特殊类型的字符串. 例如, CusipStr = NewType(" CusipStr ", str),和 打字.TypeGuard 功能类似于 is_cusip(text: str) -> TypeGuard[CusipStr].

  4. 打字.新类型和打字.文字是用于提高类型安全性的轻量级工具.In (3.b), CusipStr 是轻量级的,因为在运行时类型仍然是str,没有额外的包袱 dataclass / attrs 类. 和 文字 可以作为轻量级枚举吗.枚举. 例如,
    • order_status: 文字["open", "cancel ", "fill "]
    • Order_status = "open".
      • Mypy说: 成功:在1个源文件中没有发现问题.
    • Order_status = "已取消". (注意错别字.)
      • Mypy说:错误:赋值时类型不兼容(表达式类型为"文字['canceled']"), 变量类型为“文字['open'”, “取消”, '了']”)(作业).

类型注释对于提高代码库的健壮性有很大的好处. 它们并不是一个全有或全无的命题——您可以专注于将类型注释添加到代码库的一小部分,并随着时间的推移增加类型注释代码的数量. 以及其他技术, Python的类型注释帮助CQ9在快节奏的全球贸易世界中继续蓬勃发展.

认识作者

约翰Lekberg - Python工程师

约翰Lekberg在CQ9从事Python和gRPC系统的一系列工作. 他主要开发和改进用于监视和警报的内部工具. 他还领导了将静态分析工具应用于CQ9代码库的计划, 捕获错误并减少审查代码所需的手工工作.

不要错过任何一个节拍

关注我们,了解CQ9在工程、数学和自动化方面的最新信息.