快速了解Python Type Hint

2020-11-25

< view all posts

Python 作为一个动态类型语言,虽然给我们编写代码带来了很多方便,但是动态类型也会容易导致一些隐蔽的bug,一旦出现问题,通常需要较多的精力进行排查。 Python type hint 将类似于静态语言的类型提示引入代码中,可以帮助我们在编码的早期发现潜在的问题。

这篇文章的内容主要参考了mypy的Type hints cheat sheet,以及Python Typing的官方文档。对其中的内容做了翻译和整理,以及一些自己的补充。

首先需要明确的一点是,typing是提供给IDE或者各种插件检查的,实际type与声明不一致并不会导致python的解释器报错。

下面例子中使用的语法需要python 3.6以上的版本。

内置变量类型

简单的内置类型,直接用冒号加类型名称作注解。

x: int = 1
x: float = 1.0
x: bool = True
x: str = "test"
x: bytes = b"test"

可以只声明类型而不给变量赋值。

a: int

集合、字典、列表等类型,将集合内变量的类型放在中括号中。

from typing import List, Set, Dict, Tuple, Optional, Union, Any

x: List[int] = []  # OK
x: List[int] = [1]
x: Set[int] = {6, 7}
x: Dict[str, float] = {'field': 2.0}

# 固定长度的Tuple,指定所有类型
x: Tuple[int, str, float] = (3, "yes", 7.5)

# 变量长度的Tuple,使用省略号
x: Tuple[int, ...] = (1, 2, 3)

可以为None的值,使用Optional注解:

x: Optional[str] = some_function()

同时可以存在多种类型的时候,使用Union进行注解:

x: List[Union[int, str]] = [3, 5, "test", "fun"]

使用Any表示可以为任意类型

x: Any = mystery_function()

函数

对于没有返回值的函数,可以指定返回类型为None

def play(player_name: str) -> None:
    print(f"{player_name} plays")

对于不存在return的函数,也可以用 NoReturn

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

定义函数时注解变量和返回值类型:

from typing import Callable, Iterator, Union, Optional, List

# 定义函数
def stringify(num: int) -> str:
    return str(num)

# 多个输入变量
def plus(num1: int, num2: int) -> int:
    return num1 + num2

# 指定默认值
def f(num1: int, my_float: float = 3.5) -> float:
    return num1 + my_float

将函数赋值给变量,例如 Callable[[int], str] 表示函数(int) -> str.

x: Callable[[int, float], float] = f

对 Generator 函数可使用 Iterator 作注解

def g(n: int) -> Iterator[int]:
    i = 0
    while i < n:
        yield i
        i += 1

对涉及系统输入输出的可以使用 IO 注解

# Use IO[] for functions that should accept or return any
# object that comes from an open() call (IO[] does not
# distinguish between reading, writing or other modes)
def get_sys_IO(mode: str = 'w') -> IO[str]:
    if mode == 'w':
        return sys.stdout
    elif mode == 'r':
        return sys.stdin
    else:
        return sys.stdout

Duck Types

对于 Iterators (实现了__iter__()和__next__()方法的对象),可以使用 Iterable 注解。

from typing import Mapping, MutableMapping, Sequence, Iterable, List, Set

def f(ints: Iterable[int]) -> List[str]:
    return [str(x) for x in ints]

f(range(1, 3))

对于序列对象(实现了__len__()和__getitem()__方法的对象),可以使用 Sequence 注解。

def str_len(seq: Sequence) -> str:
    return str(len(seq))

可以使用 Mapping 注解类似于字典(键值对)形式,且不需要改变其内容的类型。使用 MutableMapping 注解可以改变内容的类型。

def f(my_mapping: Mapping[int, str]) -> List[int]:
    my_mapping[5] = 'maybe'  # 这是与Mapping注解相悖的
    return list(my_mapping.keys())

f({3: 'yes', 4: 'no'})

def f(my_mapping: MutableMapping[int, str]) -> Set[str]:
    my_mapping[5] = 'maybe'  # 这是符合MutableMapping注解的
    return set(my_mapping.values())

f({3: 'yes', 4: 'no'})

用户自定义的类可以作为类型被注解。

x: MyClass = MyClass()

需要注意的是,如果我们在一个类被定义前使用类名作为注解,是不通过的。如果要让注解和类的位置无关,可以使用类名的字符串。

def f(foo: A) -> int:  # This will fail
    ...

class A:
    ...

# If you use the string literal 'A', it will pass as long as there is a
# class of that name later on in the file
def f(foo: 'A') -> int:  # Ok
    ...

可以用 ClassVar 注解禁止在实例里设置的类的属性。

from typing import ClassVar, Final

class Starship:
    stats: ClassVar[dict[str, int]] = {} # class variable
    damage: int = 10                     # instance variable

enterprise_d = Starship(3000)
enterprise_d.stats = {} # 与注解相悖
Starship.stats = {}     # 这样是可以的

可以用 Final 来注解不能在子类中修改的属性。也可以直接将变量注解为 Final。(New in v3.8)

MAX_SIZE: Final = 9000

class Connection:
    TIMEOUT: Final[int] = 10

其它

可以用 Literal 来枚举一个变量所有有效的值。

from typing import Literal

def validate_simple(data: Any) -> Literal[True]:  # always returns True
    ...

MODE = Literal['r', 'rb', 'w', 'wb']
def open_helper(file: str, mode: MODE) -> str:
    ...

定义类型别名可以有两种方式,直接赋值:

from typing import List, Tuple

Card = Tuple[str, str]
Deck = List[Card]

或者用NewType:

from typing import NewType

UserId = NewType('UserId', int)
ProUserId = NewType('ProUserId', UserId)

这两者的区别在于,直接赋值,相当于声明这两个类型之间完全等价。因此类型别名和类型本身是可以互换的。而使用NewType,是从原类型中衍生出了一个子类型,因此在注解了需要子类型变量的地方,使用原类型的变量,是无法通过的。如下面这个例子:

t1 = int
def f1(x: t1):
    pass
f1(99)    # OK

t2 = NewType('t2', int)
def f2(x: t2):
    pass
f2(99)    # Can not pass

a = t2(99)
f2(a)    # OK

在 for 和 with 语句中不能直接使用类型注解,有两种替代的方案。可以选择在语句前进行注解:

i: int
for i in range(5):
    pass

或者使用旧的注释方式标注类型

for i in range(5):  # type: int
  pass