新四季網

api原理圖解(演進的正確方式)

2023-04-29 20:26:53 1

負責任的庫作者與其用戶的十個約定。

-- A. Jesse(作者)

想像一下你是一個造物主,為一個生物設計一個身體。出於仁慈,你希望它能隨著時間進化:首先,因為它必須對環境的變化作出反應;其次,因為你的智慧在增長,你對這個小東西想到了更好的設計,它不應該永遠保持一個樣子。

Serpents

然而,這個生物可能有賴於其目前解剖學的特徵。你不能無所顧忌地添加翅膀或改變它的身材比例。它需要一個有序的過程來適應新的身體。作為一個負責任的設計者,你如何才能溫柔地引導這種生物走向更大的進步呢?

對於負責任的庫維護者也是如此。我們向依賴我們代碼的人保證我們的承諾:我們會發布 bug 修復和有用的新特性。如果對庫的未來有利,我們有時會刪除某些特性。我們會不斷創新,但我們不會破壞使用我們庫的人的代碼。我們怎樣才能一次實現所有這些目標呢?

添加有用的特性

你的庫不應該永遠保持不變:你應該添加一些特性,使你的庫更適合用戶。例如,如果你有一個爬行動物類,並且如果有個可以飛行的翅膀是有用的,那就去添加吧。

class Reptile: @property def teeth(self): return 'sharp fangs' # 如果 wings 是有用的,那就添加它! @property def wings(self): return 'majestic wings'

但要注意,特性是有風險的。考慮 Python 標準庫中以下功能,看看它出了什麼問題。

bool(datetime.time(9, 30)) == Truebool(datetime.time(0, 0)) == False

這很奇怪:將任何時間對象轉換為布爾值都會得到 True,但午夜時間除外。(更糟糕的是,時區感知時間的規則更加奇怪。)

我已經寫了十多年的 Python 了,但直到上周才發現這條規則。這種奇怪的行為會在用戶代碼中引起什麼樣的 bug?

比如說一個日曆應用程式,它帶有一個創建事件的函數。如果一個事件有一個結束時間,那麼函數也應該要求它有一個開始時間。

def create_event(day, start_time=None, end_time=None): if end_time and not start_time: raise ValueError("Can't pass end_time without start_time") # 女巫集會從午夜一直開到凌晨 4 點create_event(datetime.date.today, datetime.time(0, 0), datetime.time(4, 0))

不幸的是,對於女巫來說,從午夜開始的事件無法通過校驗。當然,一個了解午夜怪癖的細心程式設計師可以正確地編寫這個函數。

def create_event(day, start_time=None, end_time=None): if end_time is not None and start_time is None: raise ValueError("Can't pass end_time without start_time")

但這種微妙之處令人擔憂。如果一個庫作者想要創建一個傷害用戶的 API,那麼像午夜的布爾轉換這樣的「特性」很有效。

Man being chased by an alligator

但是,負責任的創建者的目標是使你的庫易於正確使用。

這個功能是由 Tim Peters 在 2002 年首次編寫 datetime 模塊時造成的。即時是像 Tim 這樣的奠基 Python 的高手也會犯錯誤。 這個怪異之處後來被消除了 ,現在所有時間的布爾值都是 True。

# Python 3.5 以後bool(datetime.time(9, 30)) == Truebool(datetime.time(0, 0)) == True

不知道午夜怪癖的古怪之處的程式設計師現在可以從這種晦澀的 bug 中解脫出來,但是一想到任何依賴於古怪的舊行為的代碼現在沒有注意變化,我就會感到緊張。如果從來沒有實現這個糟糕的特性,情況會更好。這就引出了庫維護者的第一個承諾:

第一個約定:避免糟糕的特性

最痛苦的變化是你必須刪除一個特性。一般來說,避免糟糕特性的一種方法是少添加特性!沒有充分的理由,不要使用公共方法、類、功能或屬性。因此:

第二個約定:最小化特性

特性就像孩子:在充滿激情的瞬間孕育,但是它們必須要支持多年(LCTT 譯註:我懷疑作者在開車,可是我沒有證據)。不要因為你能做傻事就去做傻事。不要畫蛇添足!

Serpents with and without feathers

但是,當然,在很多情況下,用戶需要你的庫中尚未提供的東西,你如何選擇合適的功能給他們?以下另一個警示故事。

一個來自 asyncio 的警示故事

你可能知道,當你調用一個協程函數,它會返回一個協程對象:

async def my_coroutine: passprint(my_coroutine)

你的代碼必須 「 等待(await)」 這個對象以此來運行協程。人們很容易忘記這一點,所以 asyncio 的開發人員想要一個「調試模式」來捕捉這個錯誤。當協程在沒有等待的情況下被銷毀時,調試模式將列印一個警告,並在其創建的行上進行回溯。

當 Yury Selivanov 實現調試模式時,他添加了一個「協程裝飾器」的基礎特性。裝飾器是一個函數,它接收一個協程並返回任何內容。Yury 使用它在每個協程上接入警告邏輯,但是其他人可以使用它將協程轉換為字符串 「hi!」。

import sysdef my_wrapper(coro): return 'hi!'sys.set_coroutine_wrapper(my_wrapper)async def my_coroutine: passprint(my_coroutine)hi!

這是一個地獄般的定製。它改變了 「 異步(async)「 的含義。調用一次 set_coroutine_wrapper 將在全局永久改變所有的協程函數。正如 Nathaniel Smith 所說 :「一個有問題的 API」 很容易被誤用,必須被刪除。如果 asyncio 開發人員能夠更好地按照其目標來設計該特性,他們就可以避免刪除該特性的痛苦。負責任的創建者必須牢記這一點:

第三個約定:保持特性單一

幸運的是,Yury 有良好的判斷力,他將該特性標記為臨時,所以 asyncio 用戶知道不能依賴它。Nathaniel 可以用更單一的功能替換 set_coroutine_wrapper,該特性只定製回溯深度。

import syssys.set_coroutine_origin_tracking_depth(2)async def my_coroutine: passprint(my_coroutine)RuntimeWarning:'my_coroutine' was never awaitedCoroutine created at (most recent call last) File "script.py", line 8, in print(my_coroutine)

這樣好多了。沒有可以更改協程的類型的其他全局設置,因此 asyncio 用戶無需編寫防禦代碼。造物主應該像 Yury 一樣有遠見。

第四個約定:標記實驗特徵「臨時」

如果你只是預感你的生物需要犄角和四叉舌,那就引入這些特性,但將它們標記為「臨時」。

Serpent with horns

你可能會發現犄角是無關緊要的,但是四叉舌是有用的。在庫的下一個版本中,你可以刪除前者並標記後者為正式的。

刪除特性

無論我們如何明智地指導我們的生物進化,總會有一天想要刪除一個正式特徵。例如,你可能已經創建了一隻蜥蜴,現在你選擇刪除它的腿。也許你想把這個笨拙的傢伙變成一條時尚而現代的蟒蛇。

Lizard transformed to snake

刪除特性主要有兩個原因。首先,通過用戶反饋或者你自己不斷增長的智慧,你可能會發現某個特性是個壞主意。午夜怪癖的古怪行為就是這種情況。或者,最初該特性可能已經很好地適應了你的庫環境,但現在生態環境發生了變化,也許另一個神發明了哺乳動物,你的生物想要擠進哺乳動物的小洞穴裡,吃掉裡面美味的哺乳動物,所以它不得不失去雙腿。

A mouse

同樣,Python 標準庫會根據語言本身的變化刪除特性。考慮 asyncio 的 Lock 功能,在把 await 作為一個關鍵字添加進來之前,它一直在等待:

lock = asyncio.Lockasync def critical_section: await lock try: print('holding lock') finally: lock.release

但是現在,我們可以做「異步鎖」:

lock = asyncio.Lockasync def critical_section: async with lock: print('holding lock')

新方法好多了!很短,並且在一個大函數中使用其他 try-except 塊時不容易出錯。因為「儘量找一種,最好是唯一一種明顯的解決方案」, 舊語法在 Python 3.7 中被棄用 ,並且很快就會被禁止。

不可避免的是,生態變化會對你的代碼產生影響,因此要學會溫柔地刪除特性。在此之前,請考慮刪除它的成本或好處。負責任的維護者不會願意讓用戶更改大量代碼或邏輯。(還記得 Python 3 在重新添加會 u 字符串前綴之前刪除它是多麼痛苦嗎?)如果代碼刪除是機械性的動作,就像一個簡單的搜索和替換,或者如果該特性是危險的,那麼它可能值得刪除。

是否刪除特性

Balance scales

反對支持代碼必須改變改變是機械性的邏輯必須改變特性是危險的

就我們飢餓的蜥蜴而言,我們決定刪除它的腿,這樣它就可以滑進老鼠洞裡吃掉它。我們該怎麼做呢?我們可以刪除 walk 方法,像下面一樣修改代碼:

class Reptile: def walk(self): print('step step step')

變成這樣:

class Reptile: def slither(self): print('slide slide slide')

這不是一個好主意,這個生物習慣於走路!或者,就庫而言,你的用戶擁有依賴於現有方法的代碼。當他們升級到最新庫版本時,他們的代碼將會崩潰。

# 用戶的代碼,哦,不!Reptile.walk

因此,負責任的創建者承諾:

第五條預定:溫柔地刪除

溫柔地刪除一個特性需要幾個步驟。從用腿走路的蜥蜴開始,首先添加新方法 slither。接下來,棄用舊方法。

import warningsclass Reptile: def walk(self): warnings.warn( "walk is deprecated, use slither", DeprecationWarning, stacklevel=2) print('step step step') def slither(self): print('slide slide slide')

Python 的 warnings 模塊非常強大。默認情況下,它會將警告輸出到 stderr,每個代碼位置只顯示一次,但你可以禁用警告或將其轉換為異常,以及其它選項。

一旦將這個警告添加到庫中,PyCharm 和其他 IDE 就會使用刪除線呈現這個被棄用的方法。用戶馬上就知道該刪除這個方法。

Reptile.walk

當他們使用升級後的庫運行代碼時會發生什麼?

$ python3 script.pyDeprecationWarning: walk is deprecated, use slither script.py:14: Reptile.walkstep step step

默認情況下,他們會在 stderr 上看到警告,但腳本會成功並列印 「step step step」。警告的回溯顯示必須修復用戶代碼的哪一行。(這就是 stacklevel 參數的作用:它顯示了用戶需要更改的調用,而不是庫中生成警告的行。)請注意,錯誤消息有指導意義,它描述了庫用戶遷移到新版本必須做的事情。

你的用戶可能會希望測試他們的代碼,並證明他們沒有調用棄用的庫方法。僅警告不會使單元測試失敗,但異常會失敗。Python 有一個命令行選項,可以將棄用警告轉換為異常。

> python3 -Werror::DeprecationWarning script.pyTraceback (most recent call last): File "script.py", line 14, in Reptile.walk File "script.py", line 8, in walk DeprecationWarning, stacklevel=2)DeprecationWarning: walk is deprecated, use slither

現在,「step step step」 沒有輸出出來,因為腳本以一個錯誤終止。

因此,一旦你發布了庫的一個版本,該版本會警告已啟用的 walk 方法,你就可以在下一個版本中安全地刪除它。對吧?

考慮一下你的庫用戶在他們項目的 requirements 中可能有什麼。

# 用戶的 requirements.txt 顯示 reptile 包的依賴關係reptile

下次他們部署代碼時,他們將安裝最新版本的庫。如果他們尚未處理所有的棄用,那麼他們的代碼將會崩潰,因為代碼仍然依賴 walk。你需要溫柔一點,你必須向用戶做出三個承諾:維護更改日誌,選擇版本化方案和編寫升級指南。

第六個約定:維護變更日誌

你的庫必須有更改日誌,其主要目的是宣布用戶所依賴的功能何時被棄用或刪除。

版本 1.1 中的更改

新特性

新功能 Reptile.slither

棄用

Reptile.walk 已棄用,將在 2.0 版本中刪除,請使用 slither

負責任的創建者會使用版本號來表示庫發生了怎樣的變化,以便用戶能夠對升級做出明智的決定。「版本化方案」是一種用於交流變化速度的語言。

第七個約定:選擇一個版本化方案

有兩種廣泛使用的方案, 語義版本控制 和基於時間的版本控制。我推薦任何庫都進行語義版本控制。Python 的風格在 PEP 440 中定義,像 pip 這樣的工具可以理解語義版本號。

如果你為庫選擇語義版本控制,你可以使用版本號溫柔地刪除腿,例如:

1.0: 第一個「穩定」版,帶有 walk 1.1: 添加 slither,廢棄 walk 2.0: 刪除 walk

你的用戶依賴於你的庫的版本應該有一個範圍,例如:

# 用戶的 requirements.txtreptile>=1,>> os.stat('file.txt').st_ctime1540817862

有一天,核心開發人員決定在 os.stat 中使用浮點數來提供亞秒級精度。但他們擔心現有的用戶代碼還沒有做好準備更改。於是他們在 Python 2.3 中創建了一個設置 stat_float_times,默認情況下是 False 。用戶可以將其設置為 True 來選擇浮點時間戳。

>>> # Python 2.3.>>> os.stat_float_times(True)>>> os.stat('file.txt').st_ctime1540817862.598021

從 Python 2.5 開始,浮點時間成為默認值,因此 2.5 及之後版本編寫的任何新代碼都可以忽略該設置並期望得到浮點數。當然,你可以將其設置為 False 以保持舊行為,或將其設置為 True 以確保所有 Python 版本都得到浮點數,並為刪除 stat_float_times 的那一天準備代碼。

多年過去了,在 Python 3.1 中,該設置已被棄用,以便為人們為遙遠的未來做好準備,最後,經過數十年的旅程, 這個設置被刪除 。浮點時間現在是唯一的選擇。這是一個漫長的過程,但負責任的神靈是有耐心的,因為我們知道這個漸進的過程很有可能於意外的行為變化拯救用戶。

第十個約定:逐漸改變行為

以下是步驟:

添加一個標誌來選擇新行為,默認為 False,如果為 False 則發出警告將默認值更改為 True,表示完全棄用標記刪除該標誌

如果你遵循語義版本控制,版本可能如下:

庫版本庫 API用戶代碼1.0沒有標誌預期的舊行為1.1添加標誌,默認為 False,如果是 False,則警告設置標誌為 True,處理新行為2.0改變默認為 True,完全棄用標誌處理新行為3.0移除標誌處理新行為

你需要兩個主要版本來完成該操作。如果你直接從「添加標誌,默認為 False,如果是 False 則發出警告」變到「刪除標誌」,而沒有中間版本,那麼用戶的代碼將無法升級。為 1.1 正確編寫的用戶代碼必須能夠升級到下一個版本,除了新警告之外,沒有任何不良影響,但如果在下一個版本中刪除了該標誌,那麼該代碼將崩潰。一個負責任的神明從不違反扭曲的政策:「先行者總是自由的」。

負責任的創建者

Demeter

我們的 10 個約定大致可以分為三類:

謹慎發展

避免不良功能最小化特性保持功能單一標記實驗特徵「臨時」溫柔刪除功能

嚴格記錄歷史

維護更改日誌選擇版本方案編寫升級指南

緩慢而明顯地改變

兼容添加參數逐漸改變行為

如果你對你所創造的物種保持這些約定,你將成為一個負責任的造物主。你的生物的身體可以隨著時間的推移而進化,一直在改善和適應環境的變化,而不是在生物沒有準備好就突然改變。如果你維護一個庫,請向用戶保留這些承諾,這樣你就可以在不破壞依賴該庫的代碼的情況下對庫進行更新。

這篇文章最初是在 A. Jesse Jiryu Davis 的博客上' 出現的,經允許轉載。

插圖參考:

《世界進步》, Delphian Society, 1913《走進蛇的歷史》, Charles Owen, 1742關於哥斯大黎加的 batrachia 和爬行動物,關於尼加拉瓜和秘魯的爬行動物和魚類學的記錄, Edward Drinker Cope, 1875《自然史》, Richard Lydekker et. al., 1897Mes Prisons, Silvio Pellico, 1843Tierfotoagentur / m.blue-shadow洛杉磯公共圖書館, 1930

via: https://opensource.com/article/19/5/api-evolution-right-way

作者: A. Jesse 選題: lujun9972 譯者: MjSeven 校對: wxy

本文由 LCTT 原創編譯, Linux中國 榮譽推出

點擊「了解更多」可訪問文內連結,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了