Python類型提示Type Hints




2014年Python之父Guido van Rossum提交PEP484添加類型提示(Type Hints),2015年Python 3.5支持類型提示。

爲什麼Python需要類型提示?優點如下:

優點

1. 易於理解代碼

假設有一個函數,雖然剛創建時知道參數類型,但過了幾個月很可能就忘了

聲明參數和返回的類型十分便於理解

代碼讀得永遠比寫得多,因此,應該優化閱讀的便利性

類型提示告訴我們在調用時應該傳遞什麼參數

def send_request(request_data: Any,
                 headers: Optional[Dict[str, str]],
                 user_id: Optional[UserId] = None,
                 as_json: bool = True):

例如,該函數定義就可以很清晰看到各個參數的數據類型

  • request_data:任意值
  • headers:字符串構成的字典
  • user_id:可選,默認爲空,或爲UserId實例
  • as_json:布爾型,默認爲True

在出現類型提示之前,大家更多在文檔中提到類型信息,類型提示讓類型信息接近函數入口。構建linters(代碼分析工具如Pylint)可以確保它們不會過時

linters:檢查代碼風格或錯誤的小工具




2. 易於重構

類型提示讓查找給定類的使用位置變得非常簡單




3. 易於調用庫

類型提示讓IDE擁有更精確智能的自動完成

在這裏插入圖片描述




4. Type Linters

雖然IDE會警告使用了不正確的參數類型,但最好使用linter,確保程序合理並且可以在早期發現bug

在這裏插入圖片描述

例如,輸入參數必須是str,傳入None會拋出異常:

def parse(value):
    return value.upper()


print(parse(None))

主流類型檢查工具有:

  • 官方mypy
  • 微軟pyright
  • Googlepytype
  • Facebookpyre-check

推薦閱讀:PyCharm集成類型檢查mypy

在這裏插入圖片描述




5. 驗證運行數據

類型提示可用於在運行時進行驗證,確保調用者正常調用

例如使用類型提示的數據解析和驗證庫pydantic

from typing import List
from datetime import datetime
from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None
    friends: List[int] = []


# 正確調用
user = User(id=1, name='XerCis', signup_ts='2020-05-20 13:14', friends=[1, 2, 3])
print(user.id)
print(user.signup_ts)
print(user.friends)

# 錯誤調用
try:
    User(signup_ts='not datetime', friends=[1, 2, 'not int'])
except ValidationError as e:
    print(e.json())

正確調用可以將對象信息輸出
錯誤調用的具體原因很明確:沒提供id、提供的signup_ts和friends類型出錯

1
2020-05-20 13:14:00
[1, 2, 3]
[
  {
    "loc": [
      "id"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  },
  {
    "loc": [
      "signup_ts"
    ],
    "msg": "invalid datetime format",
    "type": "value_error.datetime"
  },
  {
    "loc": [
      "friends",
      2
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }
]




不能用來做什麼

從一開始,Python之父就指出類型提示不是用來幹以下事情的(當然,有這些庫——開源的力量!):

  1. 運行時類型推斷
  2. 性能調優




如何添加

有多種方式添加類型提示到代碼中:

1. 類型標註 Type annotations

from typing import List


class A:
    def __init__(self) -> None:
        self.elements: List[int] = []

    def add(self, element: int) -> None:
        self.elements.append(element)

: 添加變量信息

-> 添加返回值信息

優點:

  1. 標準、乾淨
  2. 直接用

缺點:

  1. 不先後兼容,需要Python 3.6+
  2. 強制導入所有類型依賴項,即使在運行時根本不使用
  3. 在類型提示中有複合類型,如List[int]。爲了構造複合類型,解釋器在第一次加載時需要做些操作



2. 類型註釋 Type comments

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

from typing import List


class A:
    def __init__(self):
        # type: () -> None
        self.elements = []  # type: List[int]

    def add(self, element):
        # type: (List[int]) -> None
        self.elements.append(element)

函數類型註釋:必須定義在下一行,並且用type:開頭

變量類型註釋:必須定義在同一行,並且用type:開頭

優點:

  1. 適配所有版本

缺點:

  1. 代碼看起來混亂,若有長度限制可能會出問題
  2. 與其他類型檢查工具競爭

爲避免長行代碼作爲類型提示,可以一個個輸入:

from typing import List


class A:
    def __init__(self):
        # type: () -> None
        self.elements = []  # type: List[int]

    def add(self,
            element  # type: List[int]
            ):
        # type: (...) -> None
        self.elements.append(element)



3. 接口文件 Interface stub

接口文件C/C++已經用了幾十年了,因爲Python是一種解釋型語言,所以通常用不上

a.py

class A:
    def __init__(self):
        self.elements = []

    def add(self, element):
        self.elements.append(element)

a.pyi

from typing import List


class A:
    elements = ...  # type: List[int]

    def __init__(self) -> None: ...

    def add(self, element: int) -> None: ...

優點:

  1. 不需要改源代碼
  2. 適配所有版本
  3. 這是一種通過測試的良好設計

缺點:

  1. 每個函數都有兩個定義
  2. 需要打包額外的文件
  3. 沒有檢查實現和接口是否匹配



4. 文檔字符串 Docstrings

主流IDE都支持,並且是傳統的做法

class A:
    def __init__(self):
        self.elements = []

    def add(self, element):
        '''add a element
        
        :param element: a element
        :type element: int
        :return: None
        '''
        self.elements.append(element)

優點:

  1. 適配所有版本
  2. 不與其他linter工具衝突

缺點:

  1. 沒有標準指定複雜類型提示(例如int或bool)。PyCharm有自己的方法,但是Sphinx用了不同的方法
  2. 難以保證文檔和代碼一致和最新,因爲沒有工具檢查




添加什麼

添加類型提示的詳細內容應該查閱typing — Python文檔

1. 標準類型

內置標準類型,如int,float,bollean

from typing import *


class A:
    def __init__(self):
        self.t: Tuple[int, float] = (0, 1.2)
        self.d: Dict[str, int] = {"a": 1, "b": 2}
        self.d: MutableMapping[str, int] = {"a": 1, "b": 2}
        self.l: List[int] = [1, 2, 3]
        self.i: Iterable[Text] = [u'1', u'2', u'3']
        self.OptFList: Optional[List[float]] = [0.1, 0.2]

Union 其中之一

from typing import *


class A:
    def __init__(self):
        self.id: Union[None, int, str] = 1  # None,int,float其中之一

Optional 可選

from typing import *


class A:
    def __init__(self):
        self.percentage: Optional[int, float] = 0.85  # int或float,沒的話None

甚至回調函數也有類型提示

from typing import Callable


def feeder(get_next_item: Callable[[], str]) -> None:
    pass

可以使用TypeVar定義自己的泛型容器

from typing import *

T = TypeVar('T')


class Magic(Generic[T]):
    def __init__(self, value: T) -> None:
        self.value: T = value


def square_values(v: Iterable[Magic[int]]) -> None:
    v.value = v.value * v.value

使用Any,在不需要的地方禁用類型檢查

from typing import *


def foo(item: Any) -> None:
    item.bar()



2. 鴨子類型 - protocols

當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱爲鴨子。

不顯式聲明類型

根據PEP 544,需要Python 3.8

from typing import *

KEY = TypeVar('KEY', contravariant=True)


# 這是一個以泛型類型作爲參數的協議,它有一個var類型的類變量和一個具有相同鍵類型的getter
class MagicGetter(Protocol[KEY], Sized):
    var: KEY

    def __getitem__(self, item: KEY) -> int: ...


def func_int(param: MagicGetter[int]) -> int:
    return param['a'] * 2


def func_str(param: MagicGetter[str]) -> str:
    return '{}'.format(param['a'])




陷阱

類型提示有時會有些奇怪的情況:

  1. Python 2/3之間的str差異
  2. 多個返回類型
  3. 類型查找
  4. 逆變的參數
  5. 兼容性




工具

類型檢查器

  1. mypy - Python開發
  2. pyre - Facebook開發,只支持Python 3比mypy快
  3. pytype - Google開發

類型標註(Type annotation)生成器

自動添加類型標註到現有的代碼庫可以利用以下庫:

  1. mypy stubgen命令行
  2. pyannotate - Dropbox開發,使用測試生成類型信息
  3. monkeytype - Instagram,在他們的生產系統中每一百萬次調用運行一次

運行時代碼評估

使用這些工具在運行時檢查輸入參數的類型是否正確:

  1. pydantic
  2. enforce
  3. pytypes




增強文檔——合併文檔字符串和類型提示

Sphinx和HTML,並使用插件agronholm/sphinx-autodoc-typehints來添加

如代碼:

def combine_reducers(*top_reducers: Union[Reducer, Module], **reducers: Union[Reducer, Module]) -> Reducer:
    """Create a reducer combining the reducers passed as parameters.
    It is possible to use this function to combine top-level reducers or to
    assign to reducers a specific subpath of the state. The result is a reducer,
    so it is possible to combine the resulted function with other reducers
    creating at-will complex reducer trees.
    :param top_reducers: An optional list of top-level reducers.
    :param reducers: An optional list of reducers that will handle a subpath.
    :returns: The combined reducer function.
    """
    def reduce(prev: Any, action: Action) -> Any:
        next = prev
        for r in top_reducers:
            next = r(next, action)
        for key, r in reducers.items():
            next[key] = r(next.get(key), action)
        return next

    return reduce

生成文檔

在這裏插入圖片描述

推薦閱讀:Sphinx入門——快速生成Python文檔




總結

疑問:有必要用類型提示嗎?應該在什麼時候使用?

類型提示說到底和單元測試本質上是一樣的,只是在代碼中表達不同而已。

因此,只要編寫單元測試,就應該使用類型提示。




參考文獻

  1. the state of type hints in Python
  2. PEP 484 – Type Hints
  3. Python Types Intro
  4. linters 這7大神器, 讓你的Python 代碼更易於維護
  5. 介紹幾款 Python 類型檢查工具
  6. Python基於類型提示的數據解析和驗證庫pydantic
  7. typing — Python 3.8.3 文檔
  8. Sphinx入門——快速生成Python文檔
  9. duck type鴨子類型
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章