我認為靜態類型似乎被吹捧過高了。
盡管如此,mypy極低的侵入性能帶來許多好處。關于如何在現有的Python項目中添加類型,以下是我的一些想法,大致按重要性排序。
首先確保mypy成功運行?
Mypy上手時兩個很常見的問題有:
1.Mypy沒有作為構建的一部分運行
2.mypy 雖然正在運行,但它沒有找到任何源文件或只找到了一部分源文件
Mypy的“默認允許”特性使兩者都極易出現。無論出現哪種情況,都會非常痛苦,因為最后人們應用的類型,其實并未被檢查,因而問題會慢慢暴露出來,讓人非常困惑。
要手動添加類型的地方
Mypy 可進行類型推斷,即通過檢查有關值的代碼,基于上下文區分值的類型。但事實上,由于mypy“漸進式推斷”的特點,(如果不確定,它將推斷成Any類型),比起像Haskell等其他推斷性的語言,在python中更需要手動提供類型。
我目前的想法是:你應該力求為所有函數實參和返回值1 (以及其他任何mypy需要幫助的地方)提供類型。
一般來說,變量不需要應用類型,盡管這可能有助于使你的代碼更清晰,或者幫助處理你不理解的類型錯誤。
Opitional會被頻繁使用
在實踐中最重要的一種類型就是 Optional. Optional 被用于可為空的值. 舉個例子:
大量的代碼會使用Optional配合其他類型使用。有了Optional,mypy就能夠檢查空值,無論該值被用在哪里。
Optional是一種簡單的類型但它卻能夠查出大量的缺陷,它可能是整個類型檢查體系中最好的部分。
考慮是否要包含你的測試
關于是否在類型檢查中包含測試(即也對測試進行類型檢查),我不知道有沒有統一答案。有些項目確實能在 tests/? 目錄上運行mypy,有些則不能。
在類型檢查2中包含測試的主要優點是:你能迅速發現應用類型和預期用法不一致,或mypy推斷的類型與預期用法不符。
這在新的或沒有太多類型應用的代碼庫中尤其有用。另外,測試也能用來改善IDE的 tab-completion
然而不利的一面就是,通常出于Mock(模擬)和Fake(偽造)的目的,測試有時會對你的代碼進行一些古怪操作,而且,某些測試模式通過類型檢查器可能有點困難 這雖然不是什么大問題,但似乎有點浪費時間,并且會削弱類型的優勢。
有選擇性地使用第三方stub(存根)
一些庫包含大量運行時的元編程技巧. 因而這些庫通常不會提供太多類型信息。
在程序中心使用了這些庫,卻不能獲取類型信息,這會十分惱人。一些庫有第三方Stub文件,例如sqlalchemy有sqlalchemy-stubs ,它會提供一些有用但不完全的類型。
這些庫并非都有有用的第三方Stub。在撰寫本文時,我對boto的任何第三方Stub都不信服。最好的方法似乎是在調用AWS / Openstack API時學會與Any一起使用(但要用moto進行徹底測試)。
偶爾需要應急措施
你偶爾會遇到這樣的情況:代碼正確但mypy無法分辨。這里有幾種解決方法:
第一種(可能是最好的方法)就是用typing.cast,它會告訴? mypy 你知道的比它知道的更多。這將在整個代碼中保持類型檢查,除非通知 mypy做一個特定的更正。
第二種選擇就是值顯式設置為Any。這將禁用檢查該特定值。如果所討論的值是沒有簡單類型的復雜對象,則可以使用此方法。
第三種就是使用?# type: ignore 語法. 如果問題不在于確定特定類型,而在于mypy誤認為被破壞了的某些不變類型,用這個會很方便。
考慮加一個注釋去解釋原因
優先選擇一些嚴格性選項
Mypy的 mypy.ini 文件允許進行廣泛的配置。我還沒見過哪個實操項目避免使用mypy配置的。以下是我的首選:
?check_untyped_defs 使mypy嘗試檢查沒有類型注釋的函數內部。否則,對于類型注釋級別較低的項目mypy很少檢查。
no_implicit_optional 當你設定參數不是空值,但實際上它們可為空時,它將提示類型錯誤。
ignore_missing_imports ?這個應謹慎使用,不要將其應用于整個mypy 中,否則會極大地增加由于錯誤導致重要代碼檢查失敗的風險。用此配置選項來標記特定模塊(或名稱空間)即可。
一個可行的例子:
一旦你的項目深度耦合了mypy,我建議最好去查看mypy提供的所有其他嚴格選項。從低嚴格度級別開始,朝著高嚴格度級別推進是一個好的策略。
如何調試類型問題
對于難懂的類型問題,這里給出兩個策略來調試。
第一種:你可以將類型應用于出錯類型周圍,如變量、函數參數、循環迭代變量等。這樣有助于將錯誤從難懂的那行代碼中移動到更容易看出問題的地方。
第二:使用魔法般的mypy內置函數。reveal_type(expr)能使 mypy 打印出給定表達式類型的意見;reveal_locals()?則會讓mypy打印出范圍內所有變量的類型. 這兩者中,我用 reveal_locals()??更多一些。
抽象,具體,可變和不變
Mypy 有具體的類型,如 List 和 Dict;也有抽象的類型,如 Sequence 和Mapping
一些抽象的類型還分可變和不可變的版本,例如Set 和 MutableSet ,Mapping 和 MutableMapping
因此我們需要去選擇應用哪種類型及何時應用。
我的朋友Oli Russell提出以下策略:
讓參數類型盡可能抽象
這樣能使調用者盡可能自由地傳遞他們想要的
? 2.讓返回類型(更)具體
同樣也是為了讓調用者盡可能自由地使用返回值
以上與我的經驗相符。如果你過于苛刻, 比如,你返回 Sequence 而不是 List, 那么別人之后還要去編輯你的返回類型才能達到他們的目的。
同樣的, 太具體的參數類型也很麻煩。你會發現無法將自定義的類似dict的對象傳遞給函數。有的對象只定義了__getitem__,但它確實可以當成一個Dict來使用。
你可以查閱 collections.abc 的標準庫文檔,去找每種抽象類型中包含的方法。以下是最常用的:
Iterable
Sequence 和 MutableSequence
Mapping 和 MutableMapping
Set 和 MutableSet
另外還有其他考慮。也許你想要阻止調用者修改你要返回的東西 (可能是因為你正在內部使用它3)。在這種情況下,最好返回 Mapping 而不是Dict。
Typed dataclasses(類型化的數據類)
Python 3.7 引入了 dataclasses. 下面是一個可能的類定義,如果你的類主要包含數據 (而不是行為)。它能為你提供一個更簡潔的語法。
類型系統也支持這樣的類,來作為其值的類型。
但缺點就是,大量更改其數據表示形式的代碼往往不是快速代碼。如果一個不可變的 Mapping作用于程序中的大部分,那它會比有一系列中間數據類要更快。
TypedDict
在 mypy 的擴展庫中還有一個typed dictionary ?。自3.8版本起它在標準庫里。
TypedDict 能讓你通過類型系統來控制存在哪些鍵以及它們的鍵值是什么,這一點勝過平常用的Mapping[str, str]。
它能成為Typed dataclasses的一個有效替代方案,且通常速度更快。
泛型和類型變量
mypy包括對類型變量和泛型類型的支持. 似乎大多數人發現如List等使用起來簡單自然,但當有新的類型被創建出來之后,代碼往往傾向于將類型限制定義為基類。
但是不是所有的泛型都不符合規則 , 在各式各樣的Python 代碼中,泛型偶爾也有使用價值。
泛型允許變量的類型更靈活 ,但它會保持檢查 ,舉個例子:
另一個例子:
泛型能讓你定義專門作用于某種類型變量的類。下面這個例子中,我們將D綁定到一個特定的抽象基類。
我相信接下來幾年這個將被廣泛使用
ABCs vs Protocols
有時,需要將抽象類型應用于具有各種具體選項的事物(此處在實踐中和使用泛型類型有交叉)。有兩種方法可以做到這一點。
第一種:通過下面的方法命名父基類,子類將從該基類繼承。
這里的 feed_animal 標記為采用Animal (一種抽象基類)。
第一種方法稱為名義子類型化,作為在面向對象的語言中使用抽象類型的傳統方式,大多數人應該熟悉。
還有第二種(相對于Python)較新的方法,在該方法中,您無需命名父類,而是命名一個有你所需方法的“協議”。
這里的feed_animal 被標記使用 Carnivore (一種協議)。注意,我不必將任何animal類標記為成員:如果它們具有相同類型的eat_meat方法,則它們將自動作為Carnivore的一部分
協議是“開放的”,因此任何具有匹配eat_meat方法的類都將被算作成員。這種方法叫做結構子類型化。有許多 built in protocols(內置協議),例如Sized,SupportsBytes,Container等。
如果你能控制足夠多的類層次結構,則可以選擇名義子類型,但是如果不能,則必須使用結構子類型。
與泛型類型一樣,這個最好謹慎使用。希望絕大多數情況下,具體類型就夠用了,這樣就不必在代碼庫中填充大量協議和抽象基類了。
最后的提示
一些人太過于執迷類型了,我目睹了大量Haskell社區的人一頭扎進自己的創作中無法自拔,變得癡癡傻傻,實在有點看不下去。
要小心變成類型狂!不要忽略這樣一個事實:類型檢查是修正錯誤的一種輔助,而勿滿足于類型檢查本身。
另請參閱
Dropbox是最早將mypy應用于其代碼庫的公司之一。對此他們也寫了經驗,讀來十分有趣。"The Tangle"在許多Python項目中出現過。
注釋:
強制執行此操作的相關配置選項是disallow_untyped_defs,但對現有項目立即啟用它通常只會貪多嚼不爛。
請注意,你必須在與主代碼庫相同的mypy中運行測試。將測試分開各自運行是得不到任何好處的。
正如我在本文提到過的,避免在任何地方創建新集合的一個很好的理由是速度。用不可變的抽象類型標記返回類型有助于對此進行靜態分析。
英文原文:http://calpaterson.com/mypy-hints.html
譯者:?