使用 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 程式碼庫的倡議,從而捕獲錯誤並減少了程式碼審查所需的人工工作。
