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鸭子类型
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章