Python的TypeHints

本文总阅读量

前记

现在,已经不是那个坚信动态语言+好用的工具就能写工程项目的时代,新出的语言都是走静态语言的路线,动态语言也都开始引入类型增强,解决自己的不足,而Python的也有自己的类型增强方案–TypeHints.

一开始用着Python写起代码非常爽,但是在大项目且多人合作时,会发现重构代码非常艰难或者不敢去修改历史代码.在引入TypeHints后,通过IDE的增强或者mypy等检查,能使我们重构代码方便,且让代码健壮.(也许有朝一日还能做到静态编译,提升性能- -…)

1.PEP的历史进程

Python作为一个非常灵活的动态语言,也不是一下子就拥有TypeHints的,而是通过PEP 3107, PEP 483, PEP 484,PEP 526,PEP 544,PEP 586,PEP 589, PEP 591,慢慢的使TypeHint变得完整

1.1.PEP 3107 TypeHints的主要依赖

PEP 3107,在06年就已经提出来了,是目前TypeHints的主要依赖,然,而此方案一开始跟TypeHints并没有什么关系,官方对于这个提案处于放养阶段,只是让函数拥有了注解功能:

1
2
3
4
5
In [1]: def test(bar: str, foo: int) -> int: 
...: return 1

In [2]: test.__annotations__
Out[2]: {'bar': str, 'foo': int, 'return': int}

1.2.PEP 483

PEP 483并没有去讲怎么实现TypeHints,而是简明扼要的写清楚 Python 的类型系统建设方向、边界.理清Type是语法分析的概念,class是运行时概念,class都是一个type,但type不一定是class.
同时PEP 483还介绍了一些常用的基础类型Any, Callabel,Optional, Tuple等,同时还支持泛型,也支持使用注释标记类型,防止被循环引用

1
2
3
4
5
6
7
8
In [4]: from typing import TypeVar                                       
In [5]: S = TypeVar('S', str, bytes)
In [6]: def test(a: S, b: S) -> S:
...: return a if len(a) > len(b) else b

# 支持注释标记类型和嵌套类型(以下示例要PEP526后才可以使用)
In [7]: from typing import List
In [8]: a = ['1'] # type: List[str]

1.3.PEP484

PEP 484是TypeHints的核心,首先它确定了Python仍将是一种动态类型语言,并不强制Python成为静态语言,同时讲解了TypeHints的几个新特性:

  • 为已经存在的库添加类型描述文件(.pyi),通过在同名的.pyi文件编写类或函数的type hint后,即使原文件没有编写Type Hint相关代码, 也可以被mypy或者其他工具识别.这是一个不改变原有代码就可以获得Type Hint功能的手段.
  • 允许使用 @overload 进行类型重载,但是只是用于代码检查时,实际上只有未被overload装饰的函数才能真正的被使用到
  • 使用typing.TYPE_CHECKING,让一些库只有在运行检查时才引入
    1
    2
    3
    4
    import typing

    if typing.TYPE_CHECKING:
    import expensive_mod

    1.4.PEP 526

    上面一直在说的都是函数,而在PEP526后,变量和类属性都可以支持TypeHints了, 在PEP526后可以如下使用:
    1
    2
    3
    4
    5
    from typing import List
    test_int_list: List[int]

    print(__annotations__)
    {'test_int_list': typing.List[int]}
    需要注意的是,上面的代码实际上并未创建变量,而是把变量和类型存在全局的__annotations__中, 如果是类属性,
    那么变量则会存在类的__annotations__中.

1.5.PEP544

544中主要说的是通过静态鸭子类型,Python自动得知类的Type Hint类型.我们都知道Python的动态类型是动态鸭子类型,当觉得一个类看起来像鸭子,那就认为他是鸭子,PEP544也是这样,只不过把确定的结果当成TypeHint反馈给代码检查工具.如官网给的例子:

1
2
3
4
5
6
7
8
9
from typing import Iterator, Iterable

class Bucket:
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...

def collect(items: Iterable[int]) -> int: ...
result: int = collect(Bucket()) # Passes type check

代码中定义了 Bucket 这种类型,并且提供了两个类成员。这两个类成员刚好是 Interator 的定义。 那么在实际使用中,就可以使用 Bucket 替换 Iterable。

1.6.PEP563

在编写树节点的时候,如果我们使用TypeHints,那我们就会碰到循环依赖的问题,而PEP563就是为了解决这个问题的,在使用PEP563后我们可以如下通过使用'变量'来解决:

1
2
3
4
5
from typing import Optional

class Node:
left: Optional["Node"]
right: Optional["Node"]

1.7.Python3.8 PEP 对TypeHints的增强

在Python3.8中,多了几个PEP,不过比较简单,只是对TypeHint的一个完善

  • PEP586非常简单,只是支持以字面量来作为类型使用, 但一般不推荐这样用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Literal[26]
    Literal[0x1A] # Exactly equivalent to Literal[26]
    Literal[-4]
    Literal["hello world"]
    Literal[b"hello world"]
    Literal[u"hello world"]
    Literal[True]
    Literal[Color.RED] # Assuming Color is some enum
    Literal[None]
  • PEP589则支持对每个dict的key进行类型标注

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from typing import TypedDict

    class Movie(TypedDict):
    name: str
    year: int

    movie: Movie = {'name': 'Blade Runner', 'year': 1982}
    # 没有PEP589之前只能如下编写, 同时不能对每个key进行检查...
    from typing import Dict, Union
    move_dict: Dict[str, Union[str, int]]
  • PEP591增加 final / Final, final是一个装饰器,用于声明一个类不能被更改或者继承,而Final则是声明变量不可被修改

2.一些使用小技巧

常规的TypeHints使用就不多说了,这里只说一些平常少用又好用的…

2.1别名

在项目中,经常会有一些变量类型非常复杂,且在多处地方都会使用到,纳闷利用别名,且把别名放在项目下的types.py是个不错的注意.如下是声明一个DEMO_TYPE的别名, 别名不需要写TypeHInts.

1
2
3
4
5
from typing import Dict, List

DEMO_TYPE = Dict[str, List[int]]

demo_container: DEMO_TYPE = {"bar": [1, 2, 3]}

2.2TypeVar的使用

TypeVarUnion的使用很像,区别是Union返回值的类型与输入的值是可以不一样的,而TypeVar返回值类型与输入的值类型必须一样的.当前Generic也是跟TypeVar一样要求返回值的类型与输入的值类型必须是一样的.
Union例子:

1
2
3
4
5
6
7
8
9
10
11
12
from typing import Union

DEMO_TYPE = Union[int, str]


def demo(a: DEMO_TYPE, b: DEMO_TYPE) -> DEMO_TYPE:
pass


demo(1, '1') # ok
demo(1, 1) # ok
demo('1', '1') # ok

TypeVar例子:

1
2
3
4
5
6
7
8
9
10
11
12
from typing import TypeVar

DEMO_TYPE = TypeVar('DEMO_TYPE', int, str)


def demo(a: DEMO_TYPE, b: DEMO_TYPE) -> DEMO_TYPE:
pass


demo(1, '1') # error
demo(1, 1) # ok
demo('1', '1') # ok

在类中除了使用TypeVar外,也可以使用overload, 如:

1
2
3
4
5
6
7
8
@overload
def demo(value: int) -> int: pass

@overload
def demo(value: str) -> int: pass

def demo(value):
...

2.3防止循环引用

上面提到了在写树节点时会遇到循环引用的情况,实际上Python还有其他解决方案:

1
2
3
4
5
from typing import Optional

class Node:
left=None # tpye: Optional["Node"]
right=None # type: Optional["Node"]

2.4协变与裂变

协变: 让一个比较泛的接口可以接受一个更加具体的接口作为参数或者返回值.如把猫的类赋值给英短蓝白猫的类.
裂变: 让一个比较具体的接口的可以接受一个更加泛的接口作为参数或者返回值.如把英短蓝白猫的类赋值给猫的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from typing import TypeVar, Generic


DEMO_TYPE = TypeVar("DEMO_TYPE", covariant=True)


class MyList(Generic[DEMO_TYPE]): pass


class Cat(MyList): pass


class BritishShorthair(Cat): pass


cat: Cat
british_shorthair: BritishShorthair = BritishShorthair()
cat = british_shorthair

2.5运行时类型检查

通过@runtime_checkable装饰器和Protoclol可以运行时检查

1
2
3
4
5
6
7
8
9
from typing import runtime_checkable, Protocol


@runtime_checkable
class Demo(Protocol):
def close(self):
...

assert isinstance(open('...'), Demo)

3.总结

在Python中,TypeHints能帮我们写的代码更加健全,同时借助IDE,我们也可以非常快速的编写或者更改代码,减少我们一些开发时间.
Typehints在编写代码时,只能被IDE检查进行提示,或者被检查工具用于代码检查,在实际代码中并不会生效,但是由于Python把TypeHints的一些变量存放在相关的__annotations__中,所以我们还是可以在运行中调用__annotations__提取TypeHints并对参数进行类型判断或者转换,这在web中非常有用,目前有个叫Pydantic的库就是专门处理这类应用的,非常不错.

查看评论