概况
Python 是动态类型语言
- 类型只在运行时检查
- 跑到哪查到哪,没跑到的一定不会有类型错误
- 变量在生命周期内可以变类型
1
2
3
4
5
6
7
if True:
1 + '1' # TypeError: unsupported operand type(s) for +: 'int' and 'str'
else:
1 / '1' # 永远跑不到,永远不会报错
v = 'a'
v = 1 # 变量生命周期内类型可变
Duck Typing: “If it walks like a duck and it quacks like a duck, then it must be a duck”
- 只针对动态类型
- 认为对象支持什么方法比对象本身到底是什么类型更重要
类型注解
- 优点
- 更清晰的文档
- 更好的 IDE 和 linter 支持
- 缺点
- 需要花时间写
- 额外的导入,稍花点时间
- python -X importtime 可以看 import 耗时
- 类型注解是正常的表达式
- 不内置的东西需要 import
- 可以用自己定义的类型
- 实例的类型名就是类名
- 注解里不能调用函数,用的都是下标
- 类型注解类似注释
- 不在运行时检查,类型写错了代码照常执行
- 目前不用于提升运行效率
“type hints should be used whenever unit tests are worth writing.”
个人理解注解主要应该标在变量生命周期的开头附近,之后的类型都交给引擎去推。比如 PyLance 如果根据函数返回值认为变量 v 是 int | List[int],就算你下一行明确写 v: int,PyLance 依旧会坚持他推出来的类型。控制流上的改变才会改变 PyLance 的推理,比如 assert 或者 if 其中一个情况 return。感觉这很合理,这样才有类似复查的意义,过多的人为介入就成自嗨了。 |
变量
实例的类型就是实例类的名字
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 简单类型
name: str = "hello"
pi: float = 3.142
centered: bool = False
# 实例的类型就是类名,类本身的类型是 Type[类名]
class MyClass:
pass
mc: MyClass = MyClass()
# 特殊情况:只注解不赋值,这个变量还是不存在的
nothing: str
nothing # NameError: name 'nothing' is not defined
# 或
age: None | int
age = None
age = 18
# 不写容器内容细节
names: list = ["Guido", "Jukka", "Ivan"]
version: tuple = (3, 7, 1)
options: dict = {"centered": False, "capitalize": True}
# 容器内容细节
# 3.9+开始内置类型不用从 typing 导入大写的类型名
# list内容类型只写一次,所有元素一般是一个类型
ls: list[str] = ["a", "b", "c"]
l: list[int, str] = [1, 'a'] # error: "list" expects 1 type argument, but 2 given [type-arg]
# tuple需要指定每个元素的类型,有几个元素写几个类型
t: tuple[int, str, bool] = (3, 'a', True)
t: tuple[int, str] = (1, 'a', 2) # error: Incompatible types in assignment (expression has type "Tuple[int, str, int]", variable has type "Tuple[int, str]") [assignment]
t: tuple[int] = (1, 2, 3) # error: Incompatible types in assignment (expression has type "Tuple[int, int, int]", variable has type "Tuple[int]") [assignment]
# dict
options: dict[str, bool] = {"centered": False, "capitalize": True}
# 嵌套
pairs: list[tuple[int, int]] = [(1, 2), (3, 4)]
# 序列,duck typing,只要支持len()和.__getitem__()就是一个sequence
# 可以用于不关心变量是list还是tuple时
seql: Sequence[int] = [1, 2, 3]
seqt: Sequence[int] = (1, 2, 3)
- None 值的类型就是 None
参数
支持变量的所有语法,此外
1
2
3
# args, kwargs
def func(*args: str, **kwargs: dict[str, int]): # 注解其中一个元素,不是注解整体
print(args)
类型别名
1
2
3
Card = tuple[str, str]
Deck = list[Card]
Deck # list[tuple[str, str]]
Any 和渐进注解
- Any 是任何类型的子类型,Any 也是任何类型的父类型
- 类型 C 和 P 一致,如果 C 是 P 的子类型,或者 C 和 P 中至少有一个 Any
TypeVar
动态去找一个最精确的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Sequence, TypeVar
Choosable = TypeVar("Choosable")
def choose(items: Sequence[Choosable]) -> Choosable:
return random.choice(items)
reveal_type(choose(["Guido", "Jukka", "Ivan"])) # test.py:9: note: Revealed type is "builtins.str"
reveal_type(choose([1, 2, 3])) # test.py:10: note: Revealed type is "builtins.int"
reveal_type(choose([True, 42, 3.14])) # test.py:11: note: Revealed type is "builtins.float"
reveal_type(choose(["Python", 3, 7])) # test.py:12: note: Revealed type is "builtins.object"
限制 TypeVar 的搜索范围
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import Sequence, TypeVar
Choosable = TypeVar("Choosable", str, float)
def choose(items: Sequence[Choosable]) -> Choosable:
return random.choice(items)
reveal_type(choose(["Guido", "Jukka", "Ivan"])) # test.py:9: note: Revealed type is "builtins.str"
# 不再是int了,因为int不在范围内
reveal_type(choose([1, 2, 3])) # test.py:11: note: Revealed type is "builtins.float"
reveal_type(choose([True, 42, 3.14])) # test.py:11: note: Revealed type is "builtins.float"
reveal_type(choose(["Python", 3, 7])) # error: Value of type variable "Choosable" of "choose" cannot be "object" [type-var]
限制 TypeVar 的搜索上限
- bound=上限
- 可以找到上限的所有子类型
函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 一般写法
def func(a: int, b: int) -> int:
return a + b
# 这个情况不写类型也能推出来
def func(a: int, b: int):
return a + b
# 会推出一个 int | str 类型
def func(a: int, b: int):
c = a + b
if c >= 0:
return c
else:
return "Negative"
# 函数写 return,但是还是会返回一个 None
def no_return_value() -> None:
pass
from typing import NoReturn
def wont_run_to_return() -> NoReturn:
raise RuntimeError()
返回值
- 函数没有 return,还是会返回一个 None,返回类型写 None
- 函数跑不到 return,比如内部 raise Exception,写 NoReturn
- 根据参数的个数/类型不同可以区分的重载,可以每个重载单独给返回值类型
前向引用
需要的类型还没定义或者没完全定义,有两个方案
- 字符串
- from future import annotations,注解里可以用后面定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from __future__ import annotations
from typing import Type
class C:
def __new__(cls) -> Type[C]: # 这个时候C还没有完全定义
return cls
# 或者不用future直接用字符串
from typing import Type
class C:
def __new__(cls) -> Type["C"]: # 这个时候C还没有完全定义
return cls
一般 return self 或者 cls 都能推理出来不需要手动写,但是有继承要注意是返回父类还是子类。比如下面这个,因为父类注解了方法返回值类型,子类继承之后虽然返回的是子类的对象,但是类型推理认为应该返回的是父类的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from __future__ import annotations
from typing import Type
class P:
def __init__(self, name: str) -> None:
self.name = name
@classmethod
def create(cls: Type[P], name: str) -> P:
return cls(name)
def clone(self) -> P:
return self.__class__(self.name)
class C(P):
pass
c1: C = C.create("c1") # Incompatible types in assignment (expression has type "P", variable has type "C") [assignment]
c2: C = c1.clone() # error: Incompatible types in assignment (expression has type "P", variable has type "C") [assignment]
用 TypeVar 解决
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 __future__ import annotations
from typing import Type, TypeVar
TP = TypeVar("TP", bound="P") # TP写在这的话 P 必须字符串,future只管注解里的
class P:
def __init__(self, name: str) -> None:
self.name = name
@classmethod
def create(cls: Type[TP], name: str) -> TP:
return cls(name)
def clone(self: TP) -> TP:
return self.__class__(self.name)
# TP = TypeVar("TP", bound=P) # TP也可以写在这
class C(P):
pass
c1: C = C.create("c1")
c2: C = c1.clone()
子类型
- 子类型相比父类型,不多取值,不少方法
- 子类型完全可以当父类型用
C 是 P 的子类型
- C 的所有变量 P 都有
- P 的所有方法 C 都有
- bool 是 int 的子类型
- True 就是 1,False 就是 0
- int 是 float 的子类型
1
2
3
4
5
6
7
8
True + True # 2
True - True # 0
True * True # 1
True / True # 1.0
True / False # ZeroDivisionError: division by zero
issubclass(bool, int) # True
- subclass 基本一定是 subtype
- 基类的方法都定义在基类里,想删除必须从基类里删掉
- subtype 不一定要用 subclass 实现
- int 是 float 的 subtype 但不是 subclass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class P:
def method(self):
print("in method")
p = P()
p.method() # in method
class C(P):
pass
c = C()
c.method() # in method
del C.method # AttributeError: type object 'C' has no attribute 'method'
del P.method
c = C()
c.method() # AttributeError: 'C' object has no attribute 'method'
1
2
3
i: int = True
t: tuple[int] = (True, )
l: list[int] = [True]
MYPY
用 reveal_type(),reveal_locals()查看 mypy 推理类型结果。这两个就是 mypy debug 用的,不需要 import,运行代码前删掉
1
2
3
4
5
6
7
8
9
10
v = 1
reveal_type(v)
reveal_locals()
'''
输出
test.py:2: note: Revealed type is "builtins.int"
test.py:3: note: Revealed local types are:
test.py:3: note: v: builtins.int
Success: no issues found in 1 source file
'''
常用参数
- –ignore-missing-imports
- –no-implicit-optional
https://bernat.tech/posts/
https://realpython.com/python-type-checking/