文章目录
- 优点
- 1. 易于理解代码
- 2. 易于重构
- 3. 易于调用库
- 4. Type Linters
- 5. 验证运行数据
- 不能用来做什么
- 如何添加
- 1. 类型标注 Type annotations
- 2. 类型注释 Type comments
- 3. 接口文件 Interface stub
- 4. 文档字符串 Docstrings
- 添加什么
- 1. 标准类型
- 2. 鸭子类型 - protocols
- 陷阱
- 工具
- 类型检查器
- 类型标注(Type annotation)生成器
- 运行时代码评估
- 增强文档——合并文档字符串和类型提示
- 总结
- 类名的Type Hint
- 参考文献
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
生成文档