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