在知乎上看到一個很有意思的問題,為什麼 TypeScript 如此流行,卻少見有人寫帶類型標註的 Python?
雖然我沒忍住在知乎上輸出了答案,但是為了以防萬一,我在博客上擴展,與更新一下
BTW 最近上線真的心力交瘁,寫個文章放鬆下
開始#
其實這個答案很簡單,歷史包袱與 ROI,在了解為什麼有這樣的現象之前,首先我們要去了解 Type Hint 能給我們帶來什麼,然後我們需要去了解 Type Hint 的前世今生
在現在這個時間點(2020.03)來看,Type Hint 能給我們帶來肉眼可見的收益是
- 透過 annotation ,配合 IDE 的支持,能讓我們在代碼編輯的時候的體驗更好
- 透過 mypy/pytype 等工具的支持,我們能在 CI/CD 流程中去集成靜態類型檢查
- 透過 pydantic 以及很多新式框架的支持,我們能夠減少很多重複的工作
可能大家以為從 Python 3.5 引入 PEP 484 開始,Python Type Hint 便已經成熟。但是實際上,這個時間比大家想象的短的多
好了,我們現在要去回顧一下整個 Type Hint 發展史上的關鍵節點
- PEP 3107 Function Annotations
- PEP 484 Type Hints
- PEP 526 Syntax for Variable Annotations
- PEP 563 Postponed Evaluation of Annotations
PEP 3107#
如同前面所說,大家最開始認識 Type Hint 的時間應該是 14 年 9 月提出,15 年 5 月通過的 PEP 484 。但是實際上雛形早的多,PEP 484 的語法實際上來自於 06 年提出,3.0 引入的 PEP 3107 所設計的語法,參見 PEP 3107 -- Function Annotations
在 PEP 3107 中,對於這個提案的目標,有這樣一段描述
因為 Python 的 2.x 系列缺乏一種標準的方式來註解函數的參數和返回值,各種工具和庫應運而生以填補這一空白。有些利用了在 "PEP 318" 中引入的裝飾器,而其他則解析函數的 docstring,尋找那裡的註解。
這個 PEP 旨在提供一種單一的、標準的方式來指定這些信息,減少由於到目前為止存在的機制和語法的廣泛變化所造成的困惑。
說人話就是,為了能夠給函數的參數或者返回值添加額外的元信息,大家五花八門各顯神通,有用 PEP 318 裝飾器的,有用 docstring 來做的。社區為了緩解這個現象,決定推出新的語法糖,來讓用戶能夠方便的為參數簽名和返回值添加額外的信息
最後形成的語法如下
def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
pass
是不是很眼熟? 沒錯,3107 實際上奠定了後續 Type Hint 的基調
- 可標註
- 作為 function/method 信息的一部分,可 inspect
- runtime
但是新的疑惑就來了,為什麼這個提案經常被人忽略?還是,我們需要放在具體的時間點來看
這個提案提出時間最早可以追溯至 06 年,在 PEP3000 這個可能是 Python 歷史上最著名的提案(即宣告 Python 3 的誕生)中確定在 Python 3 中引入,08 年正式發布
在這個時間點下,3107 面臨著兩個問題:
- 在 06-08 這個時間點上,社區最主要的精力都在友 (ji) 好 (lie) 的討 (si) 論 (bi),我們為什麼要 Python 3?以及為什麼我們要遷到 Python 3
- 3107 實際上只是告訴大家,你可以標註,你可以方便的獲取標註信息,但是怎麼樣去抽象一個類型的表示,如一個 int 類型的 list ,這種事,還是依靠社區自行發展,換句話說,叫做放養
問題 1,無解,只能依靠時間去慢慢推動。問題 2,促成了 PEP 484 的誕生
PEP 484#
PEP 484 這個提案大家應該都有一定程度上的了解了,在此不再描述提案的具體內容
PEP 484 最大的意義在於, 在繼承了 PEP 3107 奠定的語法和基調之上,將 Python 的類型系統進行了合理的抽象,這也是重要的產物 typing
,直到這時,Python 中的 type hint 才有了基本的官方規範,同時達到了基本的可用性,這個時間點是 15 年 9 月(9 月 13,Python 3.5.0 正式 Release)
但是實際上 PEP 484 在這個時間點也只能說基本滿足使用,我來舉幾個被詬病的例子
首先看一段代碼
from typing import Optional
class Node:
left: Optional[Node]
right: Optional[Node]
這段代碼實際上很簡單對吧,一個標準的二叉樹節點的描述,但是放在 PEP 484 中,這段代碼暴露出兩個問題
- 無法對變量進行標註。如同我前面所說的一樣,PEP 484 本質上是 PEP 3107 的一個擴展,這個時候 hint 的範圍僅限於 function/method ,而在上面的代碼中,在 3.5 時期,我是無法對我的 left 和 right 的變量進行標註的,一個編程語言的基本要素之一的變量,無法被 Type Hint ,那麼一定程度上我們可以說這樣一個 type hint 的功能沒有閉環
- 循環引用,字面意義,在社區 / StackOverflow 上如何解決 Type Hint 中的循環引用這個問題,一度讓人十分頭大。社區:What the fuck?
所幸,Python 社區意識到了這個問題,推出了兩個提案來解決這樣的問題
PEP 526#
問題 1 促成了 PEP 526 -- Syntax for Variable Annotations 的誕生,16 年 8 月提出,16 年 9 月被接受。16 年 9 月在 BPO-27985 實現。在我印象裡,這應該是 Python 社區中數的出來的爭議小,接收快,實現快的 PEP 了
在 526 中,Python 正式允許大家對變量進行標註,無論是 class attribute
還是普通的 variable
class Node:
left: str
這樣是可以的,
def abc():
a:int = 1
這樣也是可以的
在這個提案的基礎上,Python 官方也推動了 PEP 557 -- Data Classes 的落地,當然這是後話
話說回來,526 只解決了上面的问题 1,沒有解決問題 2,這個事情,將會由 PEP 563 來解決
PEP 563#
為了解決循環引用的問題,Python 引入了 PEP 563 -- Postponed Evaluation of Annotations,17 年 9 月社區提出,17 年 11 月被接受,18 年 1 月在 GH-4390 中實現。
在 563 之後,我們上面的代碼可以這麼寫了
from typing import Optional
class Node:
left: Optional["Node"]
right: Optional["Node"]
嗯,484 中的兩個問題,終於被解決了
總結#
以 PEP 563 作為重要分割點,Python 最早在 18 年 1 月之後才初步具備完整的生態和生產可用性,如果考慮 release version,那麼應該是 18 年 6 月,Python 3.7 正式發布之後的事了。
在 Python 3.6/7 之後,社區也才開始圍繞 Type Hint 去構建一套生態體系,
比如利用 PEP 526 來高效的驗證數據格式,參見 pydantic
順帶一提,這貨也是目前很火的一個新型框架(也是我目前最喜歡的一個框架)FastAPI 的根基
各大公司也開始跟進,例如 Google 的 pytype ,微軟推出了 pyright 來提供在 VSCode 上的支持
還有許許多多優秀的如 starlette 這樣庫
直到這時,Python + Type Hint 的真正的威力才開始揮發出來。這樣才開始能回答大家這樣一個問題:“我為什麼要切換到 Type Hint”,我猜在 IDE 裡寫的爽肯定不是一個重要原因
要知道,我們在做技術決策時候,一定是因為這個決策能給我們帶來足夠的 benefit,換句話說,有足夠的 ROI,而不是單純的因為,我們喜歡它
這樣看起來,到現在,滿打滿算一年半不超過兩年的時間。對於一個用戶習慣養成周期來說,這太短了。更何況還有一大堆的 Python 2 代碼在那放著 23333
話說回來,作為對比,TypeScript Release 時間可以上溯至 12 年 10 月,發布 0.8 版本,當時的 TS 應該是具備了相對完整地類型系統。
TS 用了 8 年,Python 可能也還有很長的路要走
當然,這個答案也只是從技術和歷史的角度聊聊這個問題。至於其餘的很多因素,包括社區的博奕與妥協等,暫還不在這個答案的範圍內,大家有興趣的話,可以去 python-idea,python-dev,discuss-python 這幾個地方去找一找歷史上關於這幾個提案的討論,非常有意思。
最後,TS 成功還有一個原因,它有個好爸爸 && 它爸爸有錢(逃
嗯,差不多就這樣吧,最近幹活幹的心裡憔悴的我,也就只能寫點垃圾水文了壓壓驚,平復心情了。