Python中的類型提示(中)

Python中的类型提示(中)

3、接口存根文件

這個選項允許你如下圖一般保存你的代碼:

Python中的类型提示(中)

並在原文件的旁邊添加一個擴展名為pyi的文件:

Python中的类型提示(中)

接口文件並不是一個新事物,C/C++已經用了十幾年了。因為Python是一種解釋語言,它通常不需要接口文件。但是由於計算機科學中的每一個問題都可以通過增加一個新的中間層來解決,那麼我們就可以添加一層來存儲類型信息。

這樣做的優點是:

√ 你不需要修改源代碼,且在任何python版本下都可以工作,因為解釋器不會處理這些文件。

√ 在存根文件中,你可以使用最新的語法(比如,類型標註)。因為在應用的執行過程中這些根本不會被查看。

√ 因為不需要修改你的源代碼,這就意味著添加類型提示的過程中不會導致新的錯誤,你添加的內容也不會和其他linter工具產生衝突。

√ 這是一種經過測試的設計,typeshed項目使用接口存根文件對整個標準庫添加了類型提示,並加入了一些流行的庫,比如:requests, yaml, dateutil等等。

√ 可以用來對不是你的代碼或者你不能輕易修改的代碼添加類型提示。

現在要對這種方法列出罰單了:

· 你需要複製你的代碼庫,也就是說每個方程現在都有兩種定義(注意你不需要重複代碼的主體或默認參數,使用…-省略號-做佔位符即可)。

· 現在你有一些額外的文件需要打包並要和源代碼一起裝載。

· 你不太可能註釋掉函數中的內容了(在上面的例子中,這會影響到pyi文件中方法內部的兩個方法和局部變量)。

· 沒法確保實現的文件和存根文件相匹配(而且IDE總是會優先使用存根文件中的定義)。

· 然而,最嚴重的一張罰單是,你不能對使用存根文件添加的類型提示進行類型檢查。通過存根文件來添加類型提示這種方式是被設計用來對使用庫函數的代碼進行類型檢查的,而不是對你的代碼庫進行類型檢查的。

最後兩個缺點使得檢測使用存根文件添加類型提示的代碼是否同步變得非常困難。在當下的形式中,類型存根文件是一種給你的用戶提供類型提示功能的方法,但並不能給你帶來便利,並且很難維護。為了解決這些問題,我承擔起了將存根文件和源文件在mypy中合併在一起的任務;理論上,這將同時解決這兩個問題。你可以在python/mypy ~ issue 5208下追蹤項目的進展。

4、文檔字符串

將類型信息添加到文檔字符串中也是可行的。儘管這不是為Python設計的類型提示功能框架的一部分,它依舊得到了大部分主流IDE的支持。這大概是實現這一目標的傳統方式吧。

從好的一面出發:

√ 適用於任何Python版本,它的定義在PEP-257中。

√ 不和其他linter工具衝突,因為大多數工具不檢查文檔字符串,通常只檢查代碼部分。

然而,這種方法存在以下嚴重缺陷:

· 沒有一種標準的方法來聲明覆雜數據類型提示(比如,int或bool)。Pycharm有它特有的方法,但是像Sphinx就使用了一種完全不同的方法。

· 需要修改文檔,並且因為沒有工具來檢查它的有效性所以很難保證數據的及時性和準確性。

· 文檔字符串和添加有類型提示的代碼的兼容性不好。試想當類型標註和文檔字符串同時生效時,哪個優先呢?

添加什麼?

Python中的类型提示(中)

現在讓我們深入細節。如果需要查看可添加的類型信息的具體列表,請參考官方文檔。在這裡,我僅做一個3分鐘的簡述幫助你理解它的思想。將數據類型分為兩類:標準類型和鴨子類型(協議)。

1、標準類型

標準類型是指在python解釋器中有名稱的類型。比如所有的內置類型(int, bolean, float, type, object等等)。然後這些一般的數據類型主要以容器的形式體現:

Python中的类型提示(中)

對於複合類型,一遍一遍的重複定義顯得很笨重。所以系統允許你通過下面的方式對複合類型進行命名:

Python中的类型提示(中)

我們甚至可以對內置的數據類型重新命名,這在避免例如給一個函數以錯誤的順序傳入兩個類型一樣的參數這類的錯誤時很有用:

Python中的类型提示(中)

對於命名元組,你可以直接附上自己的類型信息(注意,它和python3.7及great attrs庫中的數據類非常相似。)

Python中的类型提示(中)

你還可以指定類型為多種指定類型中的一個:

Python中的类型提示(中)

我們還可以用TypeVar函數來定義自己的一般變量:

Python中的类型提示(中)

最後,在不需要檢查的地方可以使用Any這種類型提示來禁止類型檢查:

Python中的類型提示(中)2、鴨子類型 – 協議

在這種情況下,與其說是使用類型去定義更像是用python語言來定義,並遵守這樣的定律:如果一個生物像鴨子一樣嘎嘎叫又表現的很像鴨子,那麼無論是出於什麼樣的目的都可以認為這個生物就是鴨子。在本例中,你需要在對象上定義想要的操作和屬性,而不是直接聲明它們的類型。這裡的依據請查看PEP-544~Protocols。

Python中的类型提示(中)

哈,抓到你了

一旦你開始在代碼庫中添加類型提示,有時可能會遇到一些奇怪的情況。那時你可能像下面這隻海豹一樣露出“這是什麼鬼”的表情。

Python中的类型提示(中)

1、Python2/3間的差異

這裡來快速地在一個類上實現__repr__方法:

Python中的类型提示(中)

這段代碼是有bug的。在python3下運行是正確的,但在python2下不行,這是因為在python2環境中,解釋器期望__repr__方法返回bytes類型的數據,然而Unicode_literals的引入使得返回值實際上為unicode類型的。本例中,將下一個版本的新特性引入意味著不可能編寫一個同時滿足python2和python3的類型需求的__repr__方法。你需要加入運行邏輯來使代碼做正確的事情:

Python中的类型提示(中)

現在,要讓IDE接收這種形式,你需要添加一些linter註釋,這使得代碼讀起來很複雜。更重要的是,類型檢查器的存在迫使你必須在運行中額外進行一次檢查。

2、多個返回類型

假設你想編寫一個函數,作用是將一個字符串或者整數乘以2。對此的一種嘗試可能是:

Python中的类型提示(中)

你輸入的類型是str或者int,你的返回值相應的也是str或int。然而,如果如上圖中那樣做,你實際上是在告訴類型提示輸入的類型可以是這兩種類型中的任意一種。因此,作為調用方,你需要聲明調用的類型:

Python中的类型提示(中)

這種不便可能會讓一些人通過定義返回值為any類型來避免麻煩。但是,這兒有一種更好的解決方案。類型提示系統允許你定義重載。重載的意思是對於一個給定類型的輸入只會返回一個指定類型的輸出。對於本例而言:

Python中的类型提示(中)

不過這也有不利的一面。此時你的靜態linter工具正在抱怨你利用相同的名稱重新定義函數。這是一個錯誤的告警,可以添加一個靜態linter禁用註釋標記(#pylint:disable=function-redefined)。

3、類型查找

試想你有一個類,它允許將其包含的數據表示成不同的類型,或者具有一個包含不同類型的字段。你希望用戶有一個快捷簡單的方式來引用它們,所以你添加了一個內置了類型名稱的函數:

Python中的类型提示(中)

一旦運行你就會發現:

Python中的类型提示(中)

有人可能會問這到底是怎麼回事。我給返回值的定義是float類型而不是test.A.float類型。出現這種混淆錯誤的原因時類型提示通過定義的位置出發評估每一個遇到的範圍來解析類型。一旦找到匹配的名稱,它就停止了。本例中類型提示所查找的第一層次是在類A中,在這裡它找到了一個float(float函數)並進行了替換。

避免這類問題的解決辦法是明確地定義我們不是想要任何float,而是隻要內置的float(builtin.float):

Python中的类型提示(中)

值得注意的是,要做到這一點你還需要引入builtins同時為了避免在運行時出現這種問題,你可以使用typing.TYPE_CHECKING標誌來指導類型提示,這個標誌只有在linter工具執行期間為真,其餘時刻都為假。

4、 逆變參數

檢查下面的例子。你定義了一個抽象的基本類,其包含了一些常規操作。然後你有一些具體的類,它們都只處理一種類型。控制類的創建來確保傳遞正確的類型,並且基本類是抽象的,這似乎是一個讓人滿意的設計:

Python中的类型提示(中)

然而當你運行類型linter工具進行檢查時會發現:

Python中的类型提示(中)

這裡的問題在於類中的參數是逆變的。這意味著在你的派生類中,必須處理父類中所有的類型。然而,你還可以在派生類中添加額外的類型。也就是說你只能擴展派生類中的函數參數,但不能以任何形式加以限制:

Python中的类型提示(中)

5、 兼容性

看看你能否從下面的代碼片段中發現錯誤:

Python中的类型提示(中)

如果你還沒有想好,請試想如果你運行下面的腳本會發生什麼:

Python中的类型提示(中)

如果你嘗試運行它,這會運行失敗並給出以下告警:

Python中的类型提示(中)

這是因為B是A的子類。進而B可以被裝入一個A類的容器中(又因為B擴展了A,所以B能做的比A更多)。然而B的類方法不能被裝入A類的容器,因為它不能近用一個參數來調用magic方法。此外,類型linter工具也不能指出這一點:

Python中的类型提示(中)

一個快速而簡單的解決方法是通過一些手段確保B.magic方法可以在只有一個參數的情況下工作,比如將第二個參數設置為可選項。現在用我們所學到的來看下面的代碼:

Python中的类型提示(中)

你覺得會發生什麼?注意,我們將類方法移動到構造函數中,並沒有做其他的修改。所以我們的腳本也需要一點小小的修改:

Python中的类型提示(中)

下面是運行時的告警,和之前的基本一致,只是現在是關於__init__而不是magic的:

Python中的类型提示(中)

那麼你覺得mypy會說什麼?如果去運行你會發現mypy選擇保持沉默。沒錯,它會將這些標記為正確的,儘管在運行時失敗了。mypy的創作者說這是因為類型失配太普遍了,以致於不能禁止__init__和__new__的不匹配問題。

英文原文:https://www.bernat.tech/the-state-of-type-hints-in-python/
譯者:舞象加冠


分享到:


相關文章: