文章目錄
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
- Google
pytype
- Facebook
pyre-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. 類型標註 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)
:
添加變量信息
->
添加返回值信息
優點:
- 標準、乾淨
- 直接用
缺點:
- 不先後兼容,需要Python 3.6+
- 強制導入所有類型依賴項,即使在運行時根本不使用
- 在類型提示中有複合類型,如
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:
開頭
優點:
- 適配所有版本
缺點:
- 代碼看起來混亂,若有長度限制可能會出問題
- 與其他類型檢查工具競爭
爲避免長行代碼作爲類型提示,可以一個個輸入:
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: ...
優點:
- 不需要改源代碼
- 適配所有版本
- 這是一種通過測試的良好設計
缺點:
- 每個函數都有兩個定義
- 需要打包額外的文件
- 沒有檢查實現和接口是否匹配
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)
優點:
- 適配所有版本
- 不與其他linter工具衝突
缺點:
- 沒有標準指定複雜類型提示(例如int或bool)。PyCharm有自己的方法,但是Sphinx用了不同的方法
- 難以保證文檔和代碼一致和最新,因爲沒有工具檢查
添加什麼
添加類型提示的詳細內容應該查閱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'])
陷阱
類型提示有時會有些奇怪的情況:
- Python 2/3之間的str差異
- 多個返回類型
- 類型查找
- 逆變的參數
- 兼容性
工具
類型檢查器
類型標註(Type annotation)生成器
自動添加類型標註到現有的代碼庫可以利用以下庫:
mypy stubgen
命令行- pyannotate - Dropbox開發,使用測試生成類型信息
- monkeytype - Instagram,在他們的生產系統中每一百萬次調用運行一次
運行時代碼評估
使用這些工具在運行時檢查輸入參數的類型是否正確:
增強文檔——合併文檔字符串和類型提示
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
生成文檔
總結
疑問:有必要用類型提示嗎?應該在什麼時候使用?
類型提示說到底和單元測試本質上是一樣的,只是在代碼中表達不同而已。
因此,只要編寫單元測試,就應該使用類型提示。