Python 是為誰設計的?
幾年前,我在 python-dev 郵件列表中,以及在活躍的 CPython 核心開發人員和認為參與這一過程不是有效利用個人時間和精力的人中強調說,“CPython 的發展太快了也太慢了” 是很多衝突的原因之一。
我一直認為事實確實如此,但這也是一個要點,在這幾年中我也花費了很多時間去反思它。在我寫那篇文章的時候,我還在波音防務澳大利亞公司(Boeing Defence Australia)工作。下個月,我離開了波音進入紅帽亞太(Red Hat Asia-Pacific),並且開始在大企業的開源供應鏈管理方面取得了再分發者redistributor層面的視角。
Python 的參考解析器使用情況
我嘗試將 CPython 的使用情況分解如下,儘管看起來有些過於簡化(注意,這些分類的界線並不是很清晰,他們僅關注於考慮新軟體特性和版本釋出後不同因素的影響):
- 教育類:教育工作者的主要興趣在於建模方法的教學和計算操作方面,不會去編寫或維護生產級別的軟體。例如:
- 澳大利亞的數字課程
- Lorena A. Barba 的 AeroPython
- 個人類的自動化和愛好者的專案:主要且經常是一類自寫自用的軟體。例如:
- my Digital Blasphemy 圖片下載器
- Paul Fenwick 的 (Inter)National Rick Astley Hotline
- 組織organisational過程自動化:主要且經常是為組織利益而編寫的。例如:
- CPython 的核心工作流工具
- Linux 發行版的開發、構建和發行管理工具
- “一勞永逸Set-and-forget” 的基礎設施:這類軟體在其生命週期中幾乎不會升級,但在底層平臺可能會升級(這種說法有時候有些爭議)。例如:
- 大多數自我管理的企業或機構的基礎設施(在那些資金充足的可持續工程計劃中,這種情況是讓人非常不安的)
- 撥款資助的軟體(當最初的撥款耗盡時,維護通常會終止)
- 有嚴格認證要求的軟體(如果沒有絕對必要的話,從經濟性考慮,重新認證比常規升級來說要昂貴很多)
- 沒有自動升級功能的嵌入式軟體系統
- 持續升級的基礎設施:具有健壯支撐的工程學模型的軟體,對於依賴和平臺升級通常是例行的,不必關心原始碼變更。例如:
- Facebook 的 Python 服務基礎設施
- 滾動釋出的 Linux 分發版
- 大多數的公共 PaaS 無伺服器環境(Heroku、OpenShift、AWS Lambda、Google Cloud Functions、Azure Cloud Functions 等等)
- 長週期性升級的標準的操作環境:對其核心元件進行常規升級,但這些升級以年為單位進行,而不是周或月。例如:
- VFX 平臺
- 長期支援(LTS)的 Linux 分發版
- CPython 和 Python 標準庫
- 基礎設施管理和編排工具(如 OpenStack、Ansible)
- 硬體控制系統
- 短生命週期的軟體:軟體僅被使用一次,然後就丟棄或忽略,而不是隨後接著升級。例如:
- 臨時Ad hoc自動化指令碼
- 被確定為 “終止” 的單使用者遊戲(你玩了一次後,甚至都忘了去解除安裝它們,或許在一個新的裝置上都不打算再去安裝了)
- 不具備(或不完整)狀態儲存的單使用者遊戲(如果你解除安裝並重安裝它們,遊戲體驗也不會有什麼大的變化)
- 特定事件的應用程式(這些應用程式與特定的事件捆綁,一旦事件結束,這些應用程式就不再有用了)
- 常規用途的應用程式:部署後定期升級的軟體。例如:
- 業務管理軟體
- 個人和專業的生產力應用程式(如 Blender)
- 開發工具和服務(如 Mercurial、Buildbot、Roundup)
- 多使用者遊戲,和其它明顯處於持續狀態還沒有被定義為 “終止” 的遊戲
- 有自動升級功能的嵌入式軟體系統
- 共享的抽象層:在一個特定的問題領域中,設計用於讓工作更高效的軟體元件。即便是你沒有親自掌握該領域的所有錯綜複雜的東西。例如:
- 大多數的執行時runtime庫和框架都歸入這一類(如 Django、Flask、Pyramid、SQL Alchemy、NumPy、SciPy、requests)
- 適合歸入這一類的許多測試和型別推斷工具(如 pytest、Hypothesis、vcrpy、behave、mypy)
- 其它應用程式的外掛(如 Blender plugins、OpenStack hardware adapters)
- 本身就代表了 “Python 世界” 基準的標準庫(那是一個難以置信的複雜的世界觀)
CPython 主要服務於哪些受眾?
從根本上說,CPython 和標準庫的主要受眾是哪些呢?是那些不管出於什麼原因,將有限的標準庫和從 PyPI 顯式宣告安裝的第三方庫組合起來所提供的服務還不能夠滿足需求的那些人。
為了更進一步簡化上面回顧的不同用法和部署模型,巨集觀地將最大的 Python 使用者群體分開來看,一類是在一些感興趣的環境中將 Python 作為一種 指令碼語言 使用的人;另外一種是將它用作一個 應用程式開發語言 的人,他們最終釋出的是一種產品而不是他們的指令碼。
把 Python 作為一種指令碼語言來使用的開發者的典型特性包括:
- 主要的工作單元是由一個 Python 檔案組成的(或 Jupyter notebook),而不是一個 Python 和後設資料檔案的目錄
- 沒有任何形式的單獨的構建步驟 —— 是作為一個指令碼分發的,類似於分發一個獨立的 shell 指令碼的方式
- 沒有單獨的安裝步驟(除了下載這個檔案到一個合適的位置),因為在目標系統上要求預配置執行時環境
- 沒有顯式的規定依賴關係,除了最低的 Python 版本,或一個預期的執行環境宣告。如果需要一個標準庫以外的依賴項,他們會通過一個環境指令碼去提供(無論是作業系統、資料分析平臺、還是嵌入 Python 執行時的應用程式)
- 沒有單獨的測試套件,使用 “通過你給定的輸入,這個指令碼是否給出了你期望的結果?” 這種方式來進行測試
- 如果在執行前需要測試,它將以試執行dry run>和預覽preview模式來向使用者展示軟體將怎樣執行
- 如果使用靜態程式碼分析工具,則通過整合到使用者的軟體開發環境中,而不是為每個指令碼單獨設定
相比之下,使用 Python 作為一個應用程式開發語言的開發者特徵包括:
- 主要的工作單元是由 Python 和後設資料檔案組成的目錄,而不是單個 Python 檔案
- 在釋出之前有一個單獨的構建步驟去預處理應用程式,哪怕是把它的這些檔案一起打包進一個 Python sdist、wheel 或 zipapp 中
- 應用程式是否有獨立的安裝步驟做預處理,取決於它是如何打包的,和支援的目標環境
- 外部的依賴明確存在於專案目錄下的一個後設資料檔案中,要麼是直接在專案目錄中(如
pyproject.toml
、requirements.txt
、Pipfile
),要麼是作為生成的發行包的一部分(如setup.py
、flit.ini
) - 有獨立的測試套件,或者作為一個 Python API 的一個單元測試,或者作為功能介面的整合測試,或者是兩者都有
- 靜態分析工具的使用是在專案級配置的,並作為測試管理的一部分,而不是作為依賴
作為以上分類的一個結果,CPython 和標準庫的主要用途是,在相應的 CPython 特性發布後,為教育和臨時ad hoc的 Python 指令碼環境提供 3-5 年基礎維護服務。
對於臨時指令碼使用的情況,這個 3-5 年的延遲是由於再分發者給使用者開發新版本的延遲造成的,以及那些再分發版本的使用者們花在修改他們的標準操作環境上的時間。
對於教育環境中的情況是,教育工作者需要一些時間去評估新特性,然後決定是否將它們包含進教學的課程中。
這些相關問題的原因是什麼?
這篇文章很大程度上是受 Twitter 上對我的這個評論的討論的啟發,它援引了定義在 PEP 411 中臨時Provisional API 的情形,作為一個開源專案的例子,對使用者發出事實上的邀請,請其作為共同開發者去積極參與設計和開發過程,而不是僅被動使用已準備好的最終設計。
這些回覆包括一些在更高階別的庫中支援臨時 API 的困難程度的一些沮喪性表述,沒有這些庫做臨時狀態的傳遞,因此而被限制為只有臨時 API 的最新版本才支援這些相關特性,而不是任何早期版本的迭代。
我的主要回應是,建議開源提供者應該強制實施有限支援,通過這種強制的有限支援可以讓個人的維護努力變得可持續。這意味著,如果對臨時 API 的老版本提供迭代支援是非常痛苦的,那麼,只有在專案開發人員自己需要、或有人為此支付費用時,他們才會去提供支援。這與我的這個觀點是類似的,那就是,志願者提供的專案是否應該免費支援老的、商業性質的、長週期的 Python 版本,這對他們來說是非常麻煩的事,我不認為他們應該這樣做,正如我所期望的那樣,大多數這樣的需求都來自於管理差勁的慣性,而不是真正的需求(真正的需求,應該去支付費用來解決問題)。
而我的第二個回應是去實現這一點,儘管多年來一直在討論這個問題(比如,在上面連結中最早在 2011 年的一篇的文章中,以及在 Python 3 問答的回覆中的這裡、這裡、和這裡,以及去年的這篇文章 Python 包生態系統中也提到了一些),但我從來沒有真實地嘗試直接去解釋它在標準庫設計過程中的影響。
如果沒有這些背景,設計過程中的一部分,比如臨時 API 的引入,或者是受啟發而不同於它inspired-by-not-the-same-as的引入,看起來似乎是完全沒有意義的,因為他們看起來似乎是在嘗試對 API 進行標準化,而實際上並沒有。
適合進入 PyPI 規劃的方面有哪些?
任何提交給 python-ideas 或 python-dev 的提案所面臨的第一個門檻就是清楚地回答這個問題:“為什麼 PyPI 上的模組不夠好?”。絕大多數的提案都在這一步失敗了,為了通過這一步,這裡有幾個常見的話題:
- 比起下載一個合適的第三方庫,新手一般可能更傾向於從網際網路上 “複製貼上” 錯誤的指導。(這就是為什麼存在
secrets
庫的原因:它使得人們很少去使用random
模組,由於安全敏感的原因,它預期用於遊戲和模擬統計) - 該模組旨在提供一個參考實現,並允許與其它的競爭實現之間提供互操作性,而不是對所有人的所有事物都是必要的。(如
asyncio
、wsgiref
、unittest
、和logging
都是這種情況) - 該模組是預期用於標準庫的其它部分(如
enum
就是這種情況,像unittest
一樣) - 該模組是被設計用於支援語言之外的一些語法(如
contextlib
、asyncio
和typing
) - 該模組只是普通的臨時的指令碼用途(如
pathlib
和ipaddress
) - 該模組被用於一個教育環境(例如,
statistics
模組允許進行互動式地探索統計的概念,儘管你可能根本就不會用它來做完整的統計分析)
只通過了前面的 “PyPI 是不是明顯不夠好” 的檢查,一個模組還不足以確保被納入標準庫中,但它已經足以將問題轉變為 “在未來幾年中,你所推薦的要包含的庫能否對一般的入門級 Python 開發人員的經驗有所提升?”
標準庫中的 ensurepip
和 venv
模組的引入也明確地告訴再分發者,我們期望的 Python 級別的打包和安裝工具在任何平臺的特定分發機制中都予以支援。
當新增它們到標準庫中時,為什麼一些 API 會被修改?
現有的第三方模組有時候會被批量地採用到標準庫中,在其它情況下,實際上新增的是吸收了使用者對現有 API 體驗之後進行重新設計和重新實現的 API,但是會根據另外的設計考慮和已經成為其中一部分的語言實現參考來進行一些刪除或細節修改。
例如,與流行的第三方庫 path.py
、pathlib
的前身不同,它們並沒有定義字串子類,而是以獨立的型別替代。作為解決檔案互操作性問題的結果,定義了檔案系統路徑協議,它允許使用檔案系統路徑的介面去使用更多的物件。
為了在 “IP 地址” 這個概念的教學上提供一個更好的工具,ipaddress
模組設計調整為明確地將主機介面定義與地址和網路的定義區分開(IP 地址被關聯到特定的 IP 網路),而最原始的 ipaddr
模組中,在網路術語的使用方式上不那麼嚴格。
另外的情況是,標準庫將綜合多種現有的方法的來構建,以及為早已存在的庫定義 API 時,還有可能依賴不存在的語法特性。比如,asyncio
和 typing
模組就全部考慮了這些因素,雖然在 PEP 557 中正在考慮將後者所考慮的因素應用到 dataclasses
API 上。(它可以被總結為 “像屬性一樣,但是使用可變註釋作為欄位宣告”)。
這類修改的原理是,這類庫不會消失,並且它們的維護者對標準庫維護相關的那些限制通常並不感興趣(特別是相對緩慢的釋出節奏)。在這種情況下,在標準庫文件的更新版本中使用 “See Also” 連結指向原始模組的做法非常常見,尤其是在第三方版本額外提供了標準庫模組中忽略的那些特性時。
為什麼一些 API 是以臨時的形式被新增的?
雖然 CPython 維護了 API 的棄用策略,但在沒有正當理由的情況下,我們通常不會去使用該策略(在其他專案試圖與 Python 2.7 保持相容性時,尤其如此)。
然而在實踐中,當新增這種受已有的第三方啟發而不是直接精確拷貝第三方設計的新 API 時,所承擔的風險要高於一些正常設計決定可能出現問題的風險。
當我們考慮到這種改變的風險比平常要高,我們將相關的 API 標記為臨時,表示保守的終端使用者要避免完全依賴它們,而共享抽象層的開發者可能希望對他們準備去支援的那個臨時 API 的版本考慮實施比平時更嚴格的限制。
為什麼只有一些標準庫 API 被升級?
這裡簡短的回答得到升級的主要 API 有哪些:
- 不太可能有大量的外部因素干擾的附加更新的
- 無論是對臨時指令碼用例還是對促進將來多個第三方解決方案之間的互操作性,都有明顯好處的
- 對這方面感興趣的人提交了一個可接受的建議的
如果在將模組用於應用程式開發目的時(如 datetime
),現有模組的限制主要是顯而易見的,如果再分發者通過第三方方案很容易地實現了改進,(如 requests
),或者如果標準庫的釋出節奏與所需要的包之間真的存在衝突,(如 certifi
),那麼,建議對標準庫版本進行改變的因素將顯著減少。
從本質上說,這和上面關於 PyPI 問題正好相反:因為從應用程式開發人員體驗的角度來說,PyPI 的分發機制通常已經夠好了,這種分發方式的改進是有意義的,允許再分發者和平臺提供者自行決定將哪些內容作為他們預設提供的一部分。
假設在 3-5 年時間內,預設出現了被認為是改變帶來的可感知的價值時,才會將這些改變納入到 CPython 和標準庫中。
標準庫任何部分都有獨立的版本嗎?
是的,就像是 ensurepip
使用的捆綁模式(CPython 發行了一個 pip
的最新捆綁版本,而並沒有把它放進標準庫中),將來可能被應用到其它模組中。
最有可能的第一個候選者是 distutils
構建系統,因為切換到這種模式將允許構建系統在多個發行版本之間保持一致。
這種處理方式的其它可能候選者是 Tcl/Tk 圖形套件和 IDLE 編輯器,它們已經被拆分,並且一些開發者將其改為可選安裝項。
這些注意事項為什麼很重要?
從本質上說,那些積極參與開源開發的人就是那些致力於開源應用程式和共享抽象層的人。
那些寫一些臨時指令碼或為學生設計一些教學習題的人,通常不認為他們是軟體開發人員 —— 他們是教師、系統管理員、資料分析人員、金融工程師、流行病學家、物理學家、生物學家、市場研究員、動畫師、平面設計師等等。
對於一種語言,當我們全部的擔心都是開發人員的經驗時,那麼我們就可以根據人們所知道的內容、他們使用的工具種類、他們所遵循的開發流程種類、構建和部署他們軟體的方法等假定,來做大量的簡化。
當應用程式執行時作為指令碼引擎廣泛流行時,事情會變得更加複雜。做好任何一項工作已經很困難,並且作為單個專案的一部分來平衡兩個受眾的需求會導致雙方經常不理解和不相信。
這篇文章不是為了說明我們在開發 CPython 過程中從來沒有做出過不正確的決定 —— 它只是去合理地回應那些對新增到 Python 標準庫中的看上去很荒謬的特性的質疑,它將是 “我不是那個特性的預期目標受眾的一部分”,而不是 “我對它沒有興趣,因此它對所有人都是毫無用處和沒有價值的,新增它純屬騷擾我”。