改變代碼又不破壞它,使用裝飾器吧
如何從了解一個功能到使用它來解決問題?
答案是把它用在對個人影響小且不重要的項目中。閱讀這樣的文章可能也會有所幫助。
我最近開始在Nilearn(http://nilearn.github.io/)和Nistats(http://nistats.github.io/)擔任Python開發人員,這兩個庫是神經科學中用於分析功能性MRI數據的開源Python庫。
在合併了Nilearn中最近的一個推送請求(pull-request)之後,我意識到,為像我這樣的新開發人員編寫整個過程的文檔是非常有用的,因為他們在之前的工作中沒有編寫過商業代碼。
我決定追溯和重構我當時的步驟和想法,添加代碼和文本來說明它們。這有一個不幸的副作用,即細節的深度只取決於我的記憶力,而實際的代碼示例只來源於我提交過的代碼。其中也會有一些代碼示例,是我從來沒有提交過或者從來沒有寫過的,只是在我的腦海中把它們看作是可能的。
這並不影響這篇文章的實用性。
注意,我(最終)使用了decorator和**kwargs,但並沒有解釋這些,因為這不是我寫這篇文章的目的,網上有很多教程可以幫助您理解這兩個概念。
此外,這些庫目前是Python2兼容的,所以有時我們的選擇可能會受到一些限制。
也就是說,Geronimo(不要怕,勇往直前)!
序言
這是我們庫中的函數。為了簡潔,我刪除了文檔字元串,這個功能對這篇文章來說並不重要:
我們的任務是使view_connectome()與庫中另一個較老的函數plot_connectome()保持一致。我們希望它們有相同的函數簽名。
為什麼?一致性,如果處理得當和合理,可以使函數易於使用。本質上,一旦用戶理解了我們的一個<>_connectome() 函數,他們就知道如何調用所有函數。
我們想改變函數簽名中某些參數的名稱:
coords>node_coords
cmap>edge_camp
threshold>edge_threshold
marker_size>node_size
不使用迭代:
如果我是這個庫的唯一用戶,那麼改變它就非常簡單:
重命名函數簽名中的參數,重命名函數體中的變數,對文檔字元進行適當改變,等等…
叮咚~!完成了!
還沒有。
注意事項:
我並不是這個庫的唯一使用者。如果我以這種方式進行更改,那麼如果其他用戶使用關鍵字參數時(他們應該會這樣做),就會破壞他們的現有代碼。
請注意,我並沒有到處更改名稱,只更改了參數和相關變數。我沒有在inconnectome_info["marker_size"]=node_size中更改鍵名稱,也沒有在_get_connectome()中更改cmap參數。
這是因為:
- 這並不是PR(pull-request,推送請求)所要做的,而PR應該儘可能緊湊,以便於查看、跟蹤、調試和檢查更改。任何額外的問題或潛在的改進或代碼清理都是一個新問題,一個新PR,可以同時提交,並與當前PR分開。
- 改變其他任何事情都可能引發一些下游需要處理的問題。
- _get_connectome()是Python中的一個私有函數,而不是面向用戶的函數,因此對它進行更改對於用戶界面的影響要簡單得多。
我們需要做的是找到一個方法去:
- 確保位置參數不破壞。
- 讓參數的新名稱和舊名稱同時工作。
- 在此轉換階段向用戶顯示棄用警告。
對於Nilearn,這個過渡期通常意味著2個版本(大約8-11個月),包括point/minor版本。
上述實現只滿足第一個考慮(1.確保位置參數不被破壞。)
第一次迭代
這很容易做到。
我們用新的參數替換原來的參數以保留位置參數,然後在末尾添加替換的參數,這樣舊的關鍵字參數仍然可以工作。
然後,我們在函數體中添加代碼,用於將args提交給新參數並生成適當的棄用警告。
好吧,這行得通,但是,天哪!這看起來是不是不簡潔還是什麼?!
我把代碼變得更長、更難以閱讀、更難看,而且函數體現在要做的不僅僅是一件事:解析和處理參數以及它最初要做的事情。
為了簡潔起見,我故意省略了這裡的warning .filter()部分DeprecationWarnings默認情況下是不向終端用戶顯示的)。
第二次迭代
我決定將詳細警告部分重構為私有函數,並從主體函數中調用它。
這樣好多了。我是這樣看待它的:
- 添加到原始函數的代碼行數更少。
- 更容易編寫單元測試來檢查引發的警告。
- 函數簽名的變化幾乎不超過最小必要性。
和…
- 雖然如此,還是必須在原始函數中添加幾行代碼。
- 沒有單元測試來測試值從舊參數到新參數的正確切換。
- 函數簽名的變化超出了最小必要性。
第三次迭代
因為本質上我想做的是修改現有函數的行為,或者修飾它,所以我決定使用Python的decorator特性來完成這項任務。
所以我寫了一個。
這樣就好多了!
我根本不需要改變函數體,函數簽名的改變對我來說也是可以接受的。除了更改參數名稱外,只在函數簽名中添加了**kwargs。
當完全移除這個警告的時候,原來的函數就完全不需要修改了。
我添加了一個測試(我認為這是一個集成測試,但語義對這篇文章來說並不重要,在我看來。IMO,in my opinion)。
我認為就是這樣,而且這很好。
第四次迭代
結果幾周後,我不得不對Nilearn中的另一個函數做同樣的事情,它的一組參數需要更改,這樣一來,Nistats中的三個不同函數也需要更改相同的參數。
這時,我決定將它變成一個通用的應用函數,通過讓decorator接受參數來實現。
猶豫了一會之後,我決定重用我即將為Nilearn編寫的代碼,以便在Nistats中實現相同的目的,因為duh,而且在不久的將來,我們還要把Nistats庫合併到Nilearn中,所以這樣做很好。
我們沒有去尋找一個可能已經完成這些的外部庫,因為:
- 不到萬不得已,我們不想在代碼中引入過多的依賴關係。
- 我們不想為了一個小功能去安裝一個庫。
- 一般來說,較小的(較少使用/流行的)庫可能更容易停止維護。我沒有尋找任何數據來支持這一點,但對我來說似乎很有意義。
- 一般來說,Nilearn & Nistats庫的許多外部貢獻者都是從事代碼開發的科學家,雖然他們都是非常優秀的程序員,但是在混合時引入新事物和庫會增加貢獻障礙。
出於這個目的,我決定創建一個名為replace_parameters的通用裝飾器,並將其添加到nilearn/_utils/helper.py模塊中。
下面是最終的代碼:
現在我是否有必要為這裡的私有函數編寫額外的文檔字元串呢?有些人可能會說沒必要,但我覺得有必要。
- 內部/私有函數的詳細文檔字元串有助於下一個加入團隊的開發人員理解代碼基礎,特別是當原作者可能不在時。
- 它使我更容易編寫單元測試,因為我知道預期結果是什麼。文檔字元串在我的腦海中列出了這一切。
主要函數是這樣的:
Woohoo(哇偶)!
- 除了我們要重命名的東西外,函數體無需更改。
- 函數簽名不需要更改,只需要重新命名我們打算重命名的參數。事實證明,使用decorator之後,我不需要顯式聲明**kwargs。
- 該功能易於重用、清理和測試。
我決定保留我已經寫好的集成測試,並為這段新代碼添加單元和集成測試。
這就是我解決這個問題的過程,其實也是一步一步的思考讓我找到了解決方案。
我在這裡添加了最終的代碼和測試作為一個gist (Generalized Search Trees,通用搜索樹):
https://gist.github.com/kchawla-pi/40a08a18dc04f39cd338a4cdc15eb6
英文原文:https://dev.to/kchawla_pi/using-a-decorators-to-solve-my-task-the-thinking--the-process-49f0 譯者:天天向上
TAG:Python部落 |