由於 Python 動態型別(執行階段可以任意改變變數的型別)的特性,所以很多時候開發者們都會透過變數的命名讓他人知道該變數的型別,例如:

dicts = [{"key": "value"}, {"key": "values"}]

複數型的 dicts 命名讓其他人在閱讀時能夠大致猜到它可能是個字典(dict)的列表(list)。

但是現代專案不可能經常是如此簡單的結構,有時光從命名仍難以了解是什麼型別的變數,例如:

def get_value(json)
     x = parse(json)
     return x

當我們看到上述函式中的 json 時,就可能會疑惑它是什麼? str? dict? 也由於這種不確定性,所以在除錯甚至協同開發時都需要實際執行才能夠知道該變數到底是什麼型態,在複雜的大型專案中甚至會成為一種痛苦。

這種情況,我們除了用心命名之外,還可以搭配使用 typing 模組來改善!

本文環境

  • Python 3.7

typing 模組

typing 是 Python 3.5 以上內建的模組。

該模組並非用來規定 Python 程式必須使用什麼型別,而是透過型別註釋(type annotations)讓開發者或協作者可以更加了解某個變數的型別,也讓第三方的工具能夠實作型別檢查器(type checker)。

p.s. Python 本身並不會強迫函式或者變數必須使用規定型別的變數

譬如,本文一開始的範例,就可以改成:

def get_value(json: str) -> str
     x = parse(json)
     return x

上述 json: str 是代表變數 json 的型別註釋(type annotation)是字串str ,代表 應該 傳進去字串型別的變數。

-> str 則代表該函式回傳的型別 應是 字串,

這邊會用 應該/應是 也是呼應 「 Python 本身並不會強迫函式或者變數必須使用規定型別的變數」 ,事實上只要執行過程不會出現任何錯誤,就算其他人不小心傳進錯誤的型別,也不會造成問題,例如以下範例:

def get_first_char(s: str) -> str:
    return s[1]


print(get_first_char('abc'))
print(get_first_char([1, 2, 3]))

其執行結果為:

b
2

前述範例將 list 型態的變數傳進 get_first_char 中也未導致任何錯誤,證明型別註釋(type annotations)僅是類似註解般的存在,沒有任何強迫必須使用特定型別的效果,但即使如此,型別註釋也為 Python 帶來更好的可維護性。

大致了解型別註釋之後,就可以進一步學習 typing 模組囉!

typing.List

Python 內建的型別(例如: int, float, dict, list, …)可以直接使用型別註釋,但如果是較複雜的資料型別就需要 typing 模組的輔助。

例如當我們想表達都是 int 的列表(list),並無法直接這樣表達:

def test(x: list[int]):
    pass

上述會導致以下錯誤:

TypeError: 'type' object is not subscriptable

正確的方式是使用 typing 模組的 List 進行表達:

from typing import List


def test(x: List[int]):
    pass

如此一來就能夠正確執行囉!

typing.Union

得益於 Python 的動態型別,使得我們可以讓一個函式或方法能夠接受多種型別。

例如,以下函式能夠同時接受字串與數字:

def print_something(x):
    if isinstance(x, (int, str, )):
        print(f'Got {x}')
    else:
        raise TypeError('Only int & str are accepted')

此種情況下,如果我們想標示型別註釋(type annotation)的話,可以利用 typing.Union

from typing import Union


def print_something(x: Union[int, str]):
    if isinstance(x, (int, str, )):
        print(f'Got {x}')
    else:
        raise TypeError('Only int & str are accepted')

typing.Dict

通常使用字典(Dictionary)時,我們會存放一致型別的資料在字典中,例如以下的 word_count_mapping , key 是字串, value 是整數:

word_count_mapping = {
    'zoo': 1,
    'zip': 2,
    'google': 10,
    'python': 12,
}

此時可以善用 typing.Dict 為 key 與 value 標上型別,例如 Dict[str, int] 代表一個字典的 key 是字串, value 則是整數:

word_count_mapping: Dict[str, int] = {
    'zoo': 1,
    'zip': 2,
    'google': 10,
    'python': 12,
}


def update_count(mapping: Dict[str, int], key: str, count: int):
    mapping[key] = count

typing.TypedDict

實務上也經常會為字典型的變數規定格式,例如以下的字典,該字典必須有 usernamepassword 2 個 key 存在才行:

login_dict = {
    'username': 'abc',
    'password': 'pass123456',
}

def valid_username_password(login_dict):
    pass

針對這種情況,可以使用 typing.TypedDict 進行註釋:

from typing import TypedDict

class LoginDict(TypedDict):
    username: str
    password: str

login_dict: LoginDict = {
    'username': 'abc',
    'password': 'pass123456',
}

def valid_username_password(login_dict: LoginDict):
    pass

typing.Iterable

Python 中所謂的 Iterable 指的是有實作 __iter__()__next__() 的物件(object),例如 str, dict, list, tuple 都是 Iterable ,所以都能夠透過 for 走訪,因此如果是可以接受 Iterable 的函式或方法,可以使用 typing.Iterable 進行標示:

from typing import Iterable


def print_iterable(x: Iterable):
    for i in x:
        print(i)

typing.Any

如果是不限定型別的情況下,可以使用 typing.Any ,譬如 print 其實就很適合可以使用 typing.Any

from typing import Any


def print_anything(*args: Any):
    print(*args)

當然也可以用 object 代替 typing.Any ,因為在 Python 中所有的物件都繼承自 object

def print_anything(*args: object):
    print(*args)

以上是 typing 模組的大致功用, typing 模組還有許多型別與功能可以利用,有興趣的人可以詳閱 官方文件

mypy

型別註釋能夠提供團隊成員對於變數型別的了解,不過仍不能真正解決型別誤用的情況,畢竟 Python 的型別註釋也只是註釋,並無法偵測誤用/錯用型別的情況。

更積極的做法是透過型別檢查器(type checker)幫助我們偵測誤用/錯用型別的情況,例如 mypy

p.s. 除了 mypy 也可以使用 Facebook 開發的 pyre

安裝 mypy 的指令如下:

$ pip install mypy

p.s. 本文版本 0.780

接著以實際例子偵測誤用/錯用型別的情況,例如以下檔案 test.py :

from typing import Dict


word_count_mapping: Dict[str, int] = {
    'zoo': 1,
    'zip': 2,
    'google': 10,
    'python': 12,
}


def update_count(mapping: Dict[str, int], key: str, count: int):
    mapping[key] = count


update_count(word_count_mapping, 'google', '100')

我們以指令 mypy 進行檢查:

$ mypy test.py

可以發現 mypy 為我們偵測到 1 個型別錯誤:

test.py:16: error: Argument 3 to "update_count" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

update_count 的第 3 個參數必須是 int 才行。

上述範例是針對單一檔案的檢查,如果是想針對整個資料夾的話,只要將檔案名稱改為資料夾名稱即可:

$ mypy direcotry/

mypy 還有很多選項與設定可供使用,在此不多加贅述,詳情請閱讀 mypy 官方文件

如果嫌每次都要輸入指令進行檢查很麻煩,可以將指令放在 git hook 中,例如 pre-commit 就能夠在 commit 之前進行檢查。

以上就是 typing 模組的介紹, Happy Coding!

References

https://docs.python.org/3/library/typing.html