前置学习

为什么会有类型提示

  • Python 是一种动态类型语言,这意味着我们在编写代码的时候更为自由,运行时不需要指定变量类型
  • 但是与此同时 IDE 无法像静态类型语言那样分析代码,及时给我们相应的提示,比如字符串的 split 方法
1
2
def split_str(s):
strs = s.split(",")

由于不知道参数 s 是什么类型,所以当你敲 s. 的时候不会出现 split 的语法提示

解决上述问题,类型提示

Python 3.5、3.6 新增了两个特性 PEP 484 和 PEP 526

帮助 IDE 为我们提供更智能的提示

这些新特性不会影响语言本身,只是增加一点提示

类型提示分类

主要分两个

  • 变量提示:PEP 526 特性加的
  • 函数参数提示:PEP 484 特性加的

变量类型提示

没有使用类型提示

想说明变量的数据类型只能通过注释

1
2
3
4
5
6
7
8
9
10
# 'primes' is a list of integers
primes = [] # type: List[int]

# 'captain' is a string (Note: initial value is a problem)
captain = ... # type: str


class Starship:
# 'stats' is a class variable
stats = {} # type: Dict[str, int]

使用了类型提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import List, ClassVar, Dict

# int 变量,默认值为 0
num: int = 0

# bool 变量,默认值为 True
bool_var: bool = True

# 字典变量,默认为空
dict_var: Dict = {}

# 列表变量,且列表元素为 int
primes: List[int] = []


class Starship:
# 类变量,字典类型,键-字符串,值-整型
stats: ClassVar[Dict[str, int]] = {}

# 实例变量,标注了是一个整型
num: int

这里会用到 typing 模块,后面会再展开详解

假设变量标注了类型,传错了会报错吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from typing import List

# int 变量,默认值为 0
num: int = 0

# bool 变量,默认值为 True
bool_var: bool = True

# 字典变量,默认为空
dict_var: Dict = {}

# 列表变量,且列表元素为 int
primes: List[int] = []


num = "123"
bool_var = 123
dict_var = []
primes = ["1", "2"]

print(num, bool_var, dict_var, primes)


# 输出结果
123 123 [] ['1', '2']

它并不会报错,但是会有 warning,是 IDE 的智能语法提示

所以,这个类型提示更像是一个规范约束,并不是一个语法限制

变量类型提示-元组打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 正常的元组打包
a = 1, 2, 3

# 加上类型提示的元组打包
t: Tuple[int, ...] = (1, 2, 3)
print(t)

t = 1, 2, 3
print(t)

# py3.8+ 才有的写法
t: Tuple[int, ...] = 1, 2, 3
print(t)

t = 1, 2, 3
print(t)


# 输出结果
(1, 2, 3)
(1, 2, 3)
(1, 2, 3)
(1, 2, 3)
为什么要加 …

不加的话,元组打包的时候,会有一个 warning 提示

变量类型提示-元组解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 正常元组解包
message = (1, 2, 3)
a, b, c = message
print(a, b, c) # 输出 1 2 3

# 加上类型提示的元组解包
header: str
kind: int
body: Optional[List[str]]

# 不会 warning 的栗子
header, kind, body = ("str", 123, ["1", "2", "3"])

# 会提示 warning 的栗子
header, kind, body = (123, 123, ["1", "2", "3"])

Optional 会在后面讲 typing 的时候详解

在类里面使用

1
2
3
4
class BasicStarship:
captain: str = 'Picard' # 实例变量,有默认值
damage: int # 实例变量,没有默认值
stats: ClassVar[Dict[str, int]] = {} # 类变量,有默认值
ClassVar
  • 是 typing 模块的一个特殊类
  • 它向静态类型检查器指示不应在类实例上设置此变量

函数参数类型提示

不仅提供了函数参数列表的类型提示,也提供了函数返回的类型提示

栗子一

1
2
3
# 参数 name 类型提示 str,而函数返回值类型提示也是 str
def greeting(name: str) -> str:
return 'Hello ' + name

栗子二

1
2
def greeting(name: str, obj: Dict[str, List[int]]) -> None:
print(name, obj)

常用类型提示

  • int,long,float: 整型,长整形,浮点型;
  • bool,str: 布尔型,字符串类型;
  • List, Tuple, Dict, Set:列表,元组,字典, 集合;
  • Iterable,Iterator:可迭代类型,迭代器类型;
  • Generator:生成器类型;

前两行小写的不需要 import,后面三行都需要通过 typing 模块 import 哦

常用类型提示栗子

指定函数参数类型

单个参数
1
2
3
# name 参数类型为 str
def greeting(name: str) :
return "hello"
多个参数
1
2
3
# 多个参数,参数类型均不同
def add(a: int, string: str, f: float, b: bool or str):
print(a, string, f, b)

bool or str:代表参数 b 可以是布尔类型,也可以是字符串

指定函数返回的参数类型

简单栗子
1
2
3
# 函数返回值指定为字符串
def greeting(name: str) -> str:
return "hello"
复杂一点的栗子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from typing import Tuple, List, Dict


# 返回一个 Tuple 类型的数据,第一个元素是 List,第二个元素是 Tuple,第三个元素是 Dict,第四个元素可以是字符串或布尔
def add(a: int, string: str, f: float, b: bool or str) -> Tuple[List, Tuple, Dict, str or bool]:
list1 = list(range(a))
tup = (string, string, string)
d = {"a": f}
bl = b
return list1, tup, d, bl


# 不 warn 的调用写法
print(add(1, "2", 123, True))


# 输出结果
([0], ('2', '2', '2'), {'a': 123}, True)

指定类型的时候用 list、set、dict、tuple 可不可以呢?
可以是可以,但是不能指定里面元素数据类型

1
2
def test(a: list, b: dict, c: set, d: tuple):
print(a, b, c, d)

List[T]、Set[T] 只能传一个类型,传多个会报错

1
2
a: List[int, str] = [1, "2"]
b: Set[int, str] = {1, 2, 3}

IDE 不会报错,但运行时会报错

1
2
3
4
5
6
7
8
9
10
Traceback (most recent call last):
File "/Users/polo/Documents/test.py", line 36, in <module>
a: List[int, str] = [1, "2"]
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/typing.py", line 261, in inner
return func(*args, **kwds)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/typing.py", line 683, in __getitem__
_check_generic(self, params)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.8/lib/python3.8/typing.py", line 215, in _check_generic
raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
TypeError: Too many parameters for typing.List; actual 2, expected 1

大致意思就是:List 传了太多参数,期望 1 个,实际 2 个

那 Tuple[T] 传多个会报错吗?

1
2
3
4
5
6
d: Tuple[int, str] = (1, "2")
print(d)


# 输出结果
(1, '2')

是不会报错的

再来看看 Tuple[T] 的多种写法


只写一个 int,赋值两个 int 元素会报 warning
如果 Tuple[T] 指定类型数量和赋值的元素数量不一致呢?

1
d: Tuple[int, str] = (1, "2", "2")


不会报错,但是也会有 warning

综上两个栗子,得出结论

Tuple[T] 指定一个类型的时候,仅针对同一个索引下的元素类型

如果想像 List[T] 一样,指定一个类型,可以对所有元素生效呢
1
2
d: Tuple[int, ...] = (1, 2, 3)
d: Tuple[Dict[str, str], ...] = ({"name": "poloyy"}, {"age": "33"})

指定一个类型后,在后面加个 … 就行

类型别名

可以将复杂一点类型给个别名,这样好用一些

变量栗子

1
2
3
4
5
6
# 别名
vector = List[float]

var: vector = [1.1, 2.2]
# 等价写法
var: List[float] = [1.1, 2.2]

函数栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# float 组成的列表别名
vector_list_es = List[float]
# 字典别名
vector_dict = Dict[str, vector_list_es]
# 字典组成列表别名
vector_list = List[vector_dict]

# vector_list 等价写法,不用别名的话,有点像套娃
vector = List[Dict[str, List[float]]]

# 函数
def scale(scalar: float, vector: vector_list) -> vector_list:
for item in vector:
for key, value in item.items():
item[key] = [scalar * num for num in value]
print(vector)
return vector


scale(2.2, [{"a": [1, 2, 3]}, {"b": [4, 5, 6]}])


# 输出结果
[{'a': [2.2, 4.4, 6.6000000000000005]}, {'b': [8.8, 11.0, 13.200000000000001]}]

更接近实际应用的栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 更接近实际应用的栗子
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]


def broadcast_message(message: str, servers: Server) -> None:
print(message, servers)


message = "发送服务器消息"
servers = (("127.0.0.1", 127), {"name": "服务器1"})
broadcast_message(message, servers)


# 输出结果
发送服务器消息 (('127.0.0.1', 127), {'name': '服务器1'})

NewType

可以自定义创一个新类型

  • 主要用于类型检查
  • NewType(name, tp) 返回一个函数,这个函数返回其原本的值
  • 静态类型检查器会将新类型看作是原始类型的一个子类
  • tp 就是原始类型

实际栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# NewType
from typing import NewType

UserId = NewType('UserId', int)


def name_by_id(user_id: UserId) -> str:
print(user_id)


UserId('user') # Fails type check
num = UserId(5) # type: int

name_by_id(42) # Fails type check
name_by_id(UserId(42)) # OK

print(type(UserId(5)))


# 输出结果
42
42
<class 'int'>

可以看到 UserId 其实也是 int 类型

类型检查

1
2
3
4
5
6
7
8
9
10
11
UserId = NewType('UserIds', int)


def get_user_name(user_id: UserId) -> str:
...

# typechecks
user_a = get_user_name(UserId(42351))

# 类型检测不通过,需要 UserId,不是 int
user_b = get_user_name(-1)

使用 UserId 类型做算术运算,得到的是 int 类型数据

1
2
3
4
5
6
7
8
9
# 'output' is of type 'int', not 'UserId'
output = UserId(23413) + UserId(54341)
print(output)
print(type(output))


# 输出结果
77754
<class 'int'>

Callable

是一个可调用对象类型

查看对象是否可调用

语法

1
2
# 返回True或False
isinstance(对象, Callable)

栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 最简单的函数
def print_name(name: str):
print(name)


# 判断函数是否可调用
print(isinstance(print_name, Callable))

x = 1
print(isinstance(x, Callable))



# 输出结果
True
False

函数是可调用的,所以是 True,而变量不是可调用对象,所以是 False

Callable 作为函数参数

看看 Callable 的源码

1
Callable type; Callable[[int], str] is a function of (int) -> str.
  • 第一个类型(int)代表参数类型
  • 第二个类型(str)代表返回值类型

栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def print_name(name: str):
print(name)


# Callable 作为函数参数使用,其实只是做一个类型检查的作用,检查传入的参数值 get_func 是否为可调用对象
def get_name(get_func: Callable[[str], None]):
return get_func


vars = get_name(print_name)
vars("test")


# 等价写法,其实就是将函数作为参数传入
def get_name_test(func):
return func


vars2 = get_name_test(print_name)
vars2("森七")


# 输出结果
test
森七

Callable 作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Callable  作为函数返回值使用,其实只是做一个类型检查的作用,看看返回值是否为可调用对象
def get_name_return() -> Callable[[str], None]:
return print_name


vars = get_name_return()
vars("test")


# 等价写法,相当于直接返回一个函数对象
def get_name_test():
return print_name


vars2 = get_name_test()
vars2("森七")


# 输出结果
test
森七

TypeVar 泛型

源码解析使用方式

1
2
3
4
5
6
7
class TypeVar(_TypingBase, _root=True):
"""Type variable.

Usage::

T = TypeVar('T') # Can be anything
A = TypeVar('A', str, bytes) # Must be str or bytes

任意类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 可以是任意类型
T = TypeVar('T')


def test(name: T) -> T:
print(name)
return name


test(11)
test("aa")


# 输出结果
11
aa

指定类型

1
2
3
4
5
6
7
8
9
10
11
12
# 可以是 int,也可以是 str 类型
AA = TypeVar('AA', int, str)

num1: AA = 1
num2: AA = "123"
print(num1, num2)

num3: AA = [] #


# 输出结果
1 123

Any Type

  • 一种特殊的类型是 Any
  • 静态类型检查器会将每种类型都视为与 Any 兼容,将 Any 视为与每种类型兼容

小栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Any
from typing import Any

a = None # type: Any
a1 = [] # OK
a2 = 2 # OK

s = '' # type: str
s1 = a # OK


def foo(item: Any) -> int:
# Typechecks; 'item' 可以是任意类型
print(item)
return 1


foo(a)
foo(a1)
foo(a2)
foo(s)
foo(s1)

隐式使用 Any

1
2
3
4
5
6
7
8
9
10
11
12
def legacy_parser(text):
...
return data


# 上述写法等价于下述写法
# 所有没有返回类型或参数类型的函数将隐式默认使用 Any


def legacy_parser(text: Any) -> Any:
...
return data

Union

联合类型
Union[int, str] 表示既可以是 int,也可以是 str

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

# vars 变量可以是 int 也可以是 str 类型
vars: Union[int, str]
vars = 1
print(vars)

vars = '123'
print(vars)

# 赋值列表会有 warning
vars = []
print(vars)

等价写法

1
2
3
4
5
6
7
8
vars: Union[int, str]
# 等价于
vars: [int or str]


vars: Union[int]
# 等价于
vars: int

union 等价写法

1
Union[int] == int

最终 Union[int] 返回的也是 int 类型

1
Union[int, str, int] == Union[int, str]

重复的类型参数会自动忽略掉

1
Union[int, str] == Union[str, int]

自动忽略类型参数顺序

1
Union[Union[int, str], float] == Union[int, str, float]

union 嵌套 union 会自动解包

Optional

可选类型

和默认参数有什么不一样

  • 官方原话:可选参数具有默认值,具有默认值的可选参数不需要在其类型批注上使用 Optional,因为它是可选的
  • 不过 Optional 和默认参数其实没啥实质上的区别,只是写法不同
  • 使用 Optional 是为了让 IDE 识别到该参数有一个类型提示,可以传指定的类型和 None,且参数是可选非必传的
1
2
3
4
5
6
7
# 可选参数
def foo(arg: int = 0) -> None:
...


# 不传 arg 默认取 0
foo()

重点

  • Optional[int] 等价于 Union[int, None]
  • 意味着:既可以传指定的类型 int,也可以传 None

实际栗子

1
2
3
4
5
6
7
8
9
10
11
def foo_func(arg: Optional[int] = None):
print(arg)


foo_func()
foo_func(1)


# 输出结果
None
1
使用默认参数的写法
1
2
3
4
5
6
7
8
9
10
11
def foo_func(arg: int = None):
print(arg)


foo_func()
foo_func(1)


# 输出结果
None
1

这种写法,Pycharm 并不会 warning

重点

Optional[] 里面只能写一个数据类型

1
2
3
4
5
6
7
8
# 正确
Optional[str]
Optional[List[str]]
Optional[Dict[str, Any]]

# 错误
Optional[str, int]
Optional[Union[str, int, float]]