Python中的類型提示(上)

Python中的类型提示(上)

Python最大的賣點之一就在於它動態的數據類型。沒有計劃想改變python的這一特點。不過在2014年的9月,Guido van Rossum (python開源社區的領導者)創建了一項python改進提議 (PEP-484) ,將類型提示加入到python語言中。一年後,也就是2015年的9月,這項功能作為Python3.5.0版本中的一種特性得以面世。也就是說,在Python誕生了25年以後,終於有一種標準的方法可以在Python代碼中添加類型信息了。在這篇博客中,我將前去探索類型提示這個系統是如何走向成熟的,我們應該如何去使用它,以及類型提示功能的下一步發展會是怎樣的。

聲明:在這篇博客中,你將會看到許多海豹和企鵝的圖片,這主要是因為我個人對這些動物的喜歡,再說了,沒有什麼比可愛的動物們更能幫助我們理解消化這些複雜的問題了,不是嗎?

為什麼我們需要類型提示?

Python中的类型提示(上)

類型提示是用來做什麼的?

首先,讓我們來了解下為什麼在python中需要類型提示。這項功能有很多優點,我會試著將這些優點按重要性排序一一列舉出來。

1、使代碼更容易看懂

清楚每一個參數的數據類型會讓理解和維護代碼庫容易很多。比如,假設你有一個函數。在剛設計好這個函數的時候,我們都知道函數中參數的類型,但幾個月過後情況就完全不同了。在代碼旁邊表明所有參數和返回值的類型將顯著的提高理解代碼片段的速度。要知道,你用來讀一段自己以前寫的代碼上的時間要遠長於用來寫它的時間。因此,你應該優化代碼的可讀性。

類型提示功能可以在你調用一個函數時提示你需要傳入什麼類型的參數,或者在你需要擴展/修改函數時告訴你函數輸入輸出兩端的數據類型。舉個例子,想象下圖中用於發送請求的函數

Python中的类型提示(上)

只要看看標記我就可以知道request_data可以是任何類型,headers的內容是一個字符串字典。用戶信息是可選項(默認值為None)或它還需要UserId的值。同時as_json的約定是它必須是一個布爾值,其本質是是一個標誌,但單從名稱上可能不能立刻看出這一點。

事實上,很多人都已經明白了類型信息是必要的,但是直到現在,因為缺少更好的選擇,類型信息經常被記錄在文檔中。類型提示系統將這些信息移動到了更接近函數接口的位置,並提供一種優秀的定義方法來滿足聲明覆雜數據類型的需求。你可以建造一些linter工具,並且在每次修改過代碼後都運行這些工具來檢查這些類型提示約束以來確保它們永遠不會過時。

2、更容易重構

當你在嘗試重構你的代碼庫時,類型提示功能使得尋找一個特定的類在何處使用過變的非常容易。雖然很多IDE已經有了類似的功能來實現這一點,類型提示可以讓它們的檢測範圍和準確率都變成100%。通常情況下,會對數據類型是如何在你的代碼中演變的提供更平滑更準確的檢測。要牢記動態類型意味著任何變量的數量類型都有可能發生改變,且所有的變量在某一時刻有且只有一種類型。數據類型這個系統依舊屬於編程的核心組成部分。時刻謹記,你應該用isinstance來駕馭應用的邏輯。

3、更容易使用庫

使用類型提示功能意味著IDE擁有了一個更加準確更加聰明的建議引擎。現在當你調用自動補全功能時,IDE完全清楚哪些方法和屬性在一個對象上是可用的。此外,如果用戶試圖調用某些不存在的東西或者傳入類型錯誤的參數,IDE都可以立即發出警告。

Python中的类型提示(上)

4、類型linter工具

Python中的类型提示(上)

IDE對數據類型不正確的參數的建議很不錯,對此做進一步提升的話可以使用linter工具來確保你應用中的數據類型是合乎邏輯的。運行這些工具可以幫你更早發現bug。(就像下面這個例子中,輸入應該是字符串類型的,傳入None類型會引發異常)

Python中的类型提示(上)

當然這只是一個簡單的小例子,一些讀者可能會提出這種不匹配的錯誤很容易發現。但是要知道linter工具在更加複雜的情況中,也即這類不匹配的錯誤越來越難被發現的情況中一樣有效。比如嵌套函數調用:

Python中的类型提示(上)

儘管現在有越來越多的linter工具,python類型檢測功能的實現參考的是mypy。mypy是一款python命令行應用,這也使得它很容易被集成進連續地一體化的流水線中去。

5、運行時的數據校驗

類型提示可以被用於在運行時進行校驗以確保調用方不破壞方法的規則。在也不用和一個包含數據類型提示信息的長列表一起啟動你的函數了。取而代之,啟用一個可以重複使用類型提示功能並在你的業務邏輯運行之前自動檢查他們是否匹配的框架。(一個pydantic的例子):

Python中的类型提示(上)

類型提示不是設計用於做什麼的?

從一開始,Guido就明確聲明瞭類型提示功能並不是被設計出來以便在下面這些例子中使用的。(當然這並不意味著人們沒有庫或工具可用,開源力量的勝利!)

1、在運行時不能進行數據類型推斷

運行解釋器(Cpython)在運行時不會試圖去推斷類型信息,也不會對傳遞的參數進行任何的類型驗證。

2、不能提高性能

運行解釋器(Cpython)不會使用類型信息來優化其產生的字節碼的安全性或執行效率。在執行一個python腳本時,類型提示會被解釋器當作註釋丟棄掉。

上述內容的重點在於類型提示功能是為了提高開發者的體驗而生的,不影響你的程序的計算方式。它帶來的是快樂的程序員而不是效率更高的代碼!

Python中的类型提示(上)

數據類型系統是什麼樣的?

Python中的類型提示是漸變的,也就是說對於給定的函數或者變量在沒有指定類型提示時,我們就假設它可以擁有任何類型(依然是一個動態的數據類型)。一次為一個函數或一個變量添加類型提示,逐漸地讓你的代碼具有數據類型的意識。可能需要添加類型提示的對象有:

· 函數參量

· 函數返回值

· 變量

記住只有添加過類型提示的代碼才可以進行類型檢查。在這些代碼上使用linter工具(比如mypy)時,如果有類型不匹配的錯誤你會得到錯誤提示:

Python中的类型提示(上)

這段代碼會得到如下的結果:

Python中的类型提示(上)

注意,我們可以檢測傳入的參數的類型的不匹配問題以及訪問對象上不存在的屬性的問題。甚至在後來提出了有效性檢查作為可選項,讓檢查和修改拼寫錯誤變得更容易。

如何添加類型提示

一旦你決定添加類型提示,你會意識到有不止一種方法可以將其添加到代碼庫中。一起來看下你都有哪些選擇。

Python中的类型提示(上)

1、類型標註

Python中的类型提示(上)

類型標註是一種直接的方法,同時也是在typing文檔中最常被提到的方法。類型標註使用函數標註(詳見PEP-3107,Python3.0+)和變量標註(詳見PEP-526,Python3.6+)。這些允許你使用:給變量和函數參數附加信息,使用->給函數或方法的返回值附加信息。

這種方法的優點是:

√ 這是一種規範的做法,這意味著這是所有方法中最簡潔的。

√ 因為這些類型信息是直接附加在代碼旁邊的,這意味著你已經將這些數據打包了出來。

這種方法的缺點是:

· 它不能向後兼容,你需要Python3.6及以上版本才能使用這項功能

· 它會強迫你導入所有數據類型的依賴,儘管這些信息在運行時完全不會用到。

· 在類型提示中,你可以使用組合類型,比如List[int]。為了創建這些複雜類型,在第一次加載這些文件是,解釋器確實需要進行一些額外的操作。

最後兩點和我們前面介紹的類型系統的初衷相違背,也就是在運行時將類型提示作為註釋來處理。為了在一定程度上解決這個問題,Python3.7版本引入了PEP563-標註延遲處理。一旦你加入這行代碼:

Python中的类型提示(上)

解釋器將不會構建這些組合結構。一旦解析完腳本的語法樹,解釋器會識別類型標註並跳過對它的運算,並不加修改的保存下來。這個機制實現了僅在需要的時候對類型標註進行解釋:比如在運行類型檢查時由linter工具來處理。在神話般的Python4問世後,這種機制應該會成為默認的行為。

2、類型註釋

當標註語法不可用時,可以使用類型註釋:

Python中的类型提示(上)

這樣做,我們可以獲得以下好處:

√ 類型註釋可以在任何Python版本下工作。雖然類型庫在Python3.5以上的版本中已經加入到了標準庫,在Python2.7以上的版本中依然可以通過PyPi包來使用。此外,因為Python註釋在任何版本的Python代碼中都是有效的語言特性。這使得你可以在任何Python2.7及以上版本的代碼中使用類型提示。使用的要求是:類型提示註釋必須在函數或變量定義處的同一行或者下一行中;必須以type:開頭。

√ 這個方案同樣解決了打包問題,因為在打包後,代碼中的註釋很少會被刪除。將類型提示信息和源程序一起打包可以讓別人在使用你的庫時獲得更好的開發體驗。

但是,這也產生了以下新問題:

· 一個缺點是,儘管類型信息的位置和參數很近,但並不是正好在參數旁邊。這使得代碼變得有些混亂。類型註釋還必須在一行內完成。如果你有一個很長的類型提示信息,而且你的代碼庫有行的長度限制,那麼就會導致問題。

· 另一個問題是,使用類型註釋添加的類型提示信息會和其他使用註釋標記的工具產生衝突。(例如,抑制其他linter工具產生的告警)。

· 除了強迫你導入所有類型信息之外,還會使你處於一個更危險的境地。現在,這些引入的類型都僅在代碼中使用,這會讓大部分linter工具認為這些導入都是沒用的。如果你允許linter工具刪除掉這些數據就會使得你的類型linter不能工作。值得注意的是,pylint將它的AST解析器升級成了類型AST解析器,從而修復了這個問題。並且將會在Python3.7發佈後推出。

為了避免使用一行很長的代碼作為類型提示,可以通過類型註釋的方式一個一個的給參數添加類型提示,然後再放入行中。在返回值處使用類型標註:

Python中的类型提示(上)

讓我們來快速使用一下,並看看類型註釋是如何讓你的代碼變得更加混亂的。下面是一個相當簡單的代碼片段,其作用是在一個類中交換兩個屬性的值:

Python中的类型提示(上)

首先必須添加類型提示。因為類型提示會很長,你可以一個參數一個參數地添加:

Python中的类型提示(上)

(譯者注:# type: (...)->Generator[Tuple[HasGetSetMutable,Optional[HasGetSetMutable]], None, None]不能分行,此處及下文中是為了圖片規格的統一不得已而為之)

然而,等你需要引入你的類型時:

Python中的类型提示(上)

現在,這種樣式的代碼會在的靜態的linter工具中產生錯誤的告警(例如在這裡使用pylint),所以你需要為此添加一些用於抑制告警的註釋:

Python中的类型提示(上)

完工了,儘管你把原本6行代碼變成了16行。沒錯,更多需要維護的代碼。只有在你的薪水是按所寫代碼的行數來支付的以及你的經理抱怨你表現的不夠好時,擴大你的代碼庫才聽上去像個好主意。

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


分享到:


相關文章: