Home Python类型注解
Post
Cancel

Python类型注解

概况

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 是 intList[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/

All Rights Reserved.

Python装饰器

DietPi部署邮件服务