Tutorials

Subtype 概念(子类型)

  • type tree

    • 类型树

      • 类似 class tree (类树)
  • 注意

    • 严格使用问题

      • 虽然 type checker 支持 type tree
      • 但是 Python 语言本身,不按 type tree 来编写,也能正常运行
    • a = 1; –> a = 'abc'

      • 特殊使用情况

        • 只支持 子类型, 而不需要父类型
        • 比如:number –> int, 实际我需要的合法内容是 int

List

  • List[int]

    • List 只包含 int 类型
    • eg: List[float]

Gradual Typing

  • 用于只标注一部分代码

    • 可以很好的平衡 动态类型和静态类型

is consistent with 类型(区别于相对 Subtye)

  • 与 …… 一致类型
  • 三个规范

    1. A type t1 is consistent with a type t2 if t1 is a subtype of t2. (But not the other way around.)

      • 子类型 对于 父类型 是 一致类型
    2. Any is consistent with every type. (But Any is not a subtype of every type.)

      • Any 类型 对于 所有类型 是一致类型
    3. Every type is consistent with Any. (But every type is not a subtype of Any.)

      • 所有类型 对于 Any 类型 都是一致类型

Any 类型

  • 一种新类型,类型通用表示符号
  • 表示的目标对象

    • 所有的值
    • 所有的方法

Types vs. Classes

  • Class 是 动态概念

    • 使用 class 定义, 被 type(obj) 内置函数返回
  • types 是 静态概念,用于 static type checker

    • 出现位置

      • 变量,方法,类型声明
  • 辨析

    • Class 一定是 type
    • type 不一定是 Class

      • eg: Union[str, int]
    • 但是,type 接口是通过 Class 实现的
    • type 的特殊性

      • 虽然是 Class,但是

        • 不能被实例化
        • 不能被继承
        • 不能被 isinstance 使用
        • 如果被 isinstance 或 issubclass 使用

          • 报 TypeError 错误

Block 类型(组合类型)

Union[t1, t2, …]

  • 真正类型,可能是 t1, t2, … 它们的 Subtype 中的一个

Subtype 类型问题

  • 根据类型枚举 集合判断

    • 类型多的是 父类型 Union[int, float, str]
    • 类型少的是 子类型 Union[int, float]

枚举类型排序

  • 排序无关紧要

嵌套 Union 问题

  • Union[int, Union[float, str]] 横等于 Union[int, float, str]
  • 枚举类型完全被展开了

枚举类型之间是 Subtype 关系

  • 只保留 父类型

枚举类型数量不能为空

  • Union[] –> 非法
  • Union[()] –> 非法

单个枚举类型

  • Union[int] 等价于 t1

出现 object 类型

  • Union[…, object, …] returns object
  • 结果就是 object

Optional[t1]

  • 即,Union[t1, None]

Tuple[t1, t2, …]

  • tuple 类型
  • t1: 第一个元素的类型
  • t2: 第二个元素的类型
  • eg: Tuple[int, float] –> (3, 5.27)

Tuple[()]

  • 空 tuple,即:(,)

Tuple[t1, …]

  • 类型都是 t1 的,可变长度 tuple

Subtype 类型

  • Tuple[u1, u2, …] 是 Tuple[t1, t2, … ] 的 父类型

    • 前提条件

      • u1, u2, … 分别是 t1, t2, … 的对应 父类型

Callable 类型

1
Callable[[t1, t2, ..., tn], tr]
  • [t1, t2, …, tn]

    • 对应输入参数的类型
  • tr

    • 对应返回值的类型

无参函数 与 不检查输入参数

1
Callable[..., tr]

缺点

  • 不能检查可变参数

    • fun(*args)
    • fun(**kwargs)
    • 解决方法

      • Callable[…, tr]

Generic Types 泛型

https://www.python.org/dev/peps/pep-0483/#generic-types

  • Classes, that behave as generic type constructors are called generic types.
  • 类似 Java, C++ 泛型

    1
    2
    3
    4
    5
    6
    7
    
    def take_first(seq: Sequence[T]) -> T: # a generic function
        return seq[0]
    
    accumulator = 0 # type: int
    
    accumulator += take_first([1, 2, 3])   # Safe, T deduced to be int
    accumulator += take_first((2.7, 3.5))  # Unsafe

Generic Type Construct

  • 接收一个类型,返回一个类型
  • eg: typing.Iterable

Generic Function

  • 即,使用 Generic Types 定义的函数

    1
    2
    3
    4
    5
    
    def take_first(seq: Sequence[T]) -> T: # a generic function
        return seq[0]
    
    accumulator = 0 # type: int
    accumulator += take_first([1, 2, 3])   # Safe, T deduced to be int
  • 这里 type variale: T, 不需要有具体值

    • 类似 C++, 在使用时才有意思

Type Variables 类型变量

  • 非约束性 type variable

    • 即:不说明限定类型
    1
    
    X = TypeVar('X')
    • 特性

      • 声明一个独特的 type variable
      • 这里 变量名 和 TypeVar 中的参数,必须相同
    • X –> 'X'
    • Y –> 'Y'
  • 约束性 type variable

    • 即:带限定类型
    1
    
    Y = TypeVar('Y', t1, t2, ...)
    • 作用类似 Union(t1, t2, …)

在函数参数中出现多次, 返回值类型 中出现 def fun(a: T, b: T) –> T: …

  1. 约束性 类型变量 type variable

    • 各个参数的具体类型要完全一致

      1
      2
      3
      4
      5
      6
      7
      8
      
      S = TypeVar('S', str, bytes)
      
      def longest(first: S, second: S) -> S:
          return first if len(first) >= len(second) else second
      
      result = longest('a', 'abc')  # The inferred type for result is str
      
      result = longest('a', b'abc')  # Fails static type check
      • 这里表示 形参 first, second 的类型 和 return 的类型 完全一致

        • 并不是 只要 是 S 中 的 (str, bytes) 任意一个就可以
        • eg:

          • (str, str) –> str: 合法
          • (bytes, bytes) –> bytes: 合法
          • (str, bytes) –> ?: 非法
            • 不能判定返回值类型
  2. 非约束性 类型变量 type variable

    • 对于输入参数 类型

      • 没有约束性
    • 对于返回值 类型

      • 约束的是,合法类型为:公共子类

        1
        2
        3
        4
        5
        6
        7
        8
        
        S = TypeVar('S')
        
        def longest(first: S, second: S) -> S:
            return first if len(first) >= len(second) else second
        
        class MyStr(str): ...
        
            result = longest(MyStr('a'), MyStr('abc'))
  3. Union 类型

    • 对于输入参数,返回值 类型

      • 都是约束宽泛
      • 对于任意一个参数

        • 类型只要是 Union 中的一个,就合法
    • 注意

      • 这里会检查,运算的合法性

        1
        2
        
          def concat(first: U, second: U) -> U:
          return x + y  # Error: can't concatenate str and bytes
    • 即:参数类型不同时,可能导致的代码,变成非法操作问题

声明和使用 Generic Types

https://docs.python.org/3/library/typing.html#user-defined-generic-types

  • 声明

    1. 使用 class 定义 和 继承 Generic[X, Y, …] 类型 实现

      • 注意

        • 继承的 Generic Type
  • 作用

    • 用来包裹 定义类,需要的多个 TypeVar

      • 即,一个 TypeVar 的 包裹器

        • 类似 C++, std::map<char, int>

          • 这里的<char, int>

typing.NewType

https://stackoverflow.com/a/58775376

  • 只是用来创建一个类型标记

    • 在 runtime 运行时,返回的还是原来类型
    • 确切的说

      • 它返回的是一个,类型标记函数
      • 而不是一个类
      • 因此,不能被继承

使检验生效问题

1
2
3
4
from typing import NewType

UserId = NewType('UserId', int)
some_id = UserId(524313)
  • 只有使用了 你定义的 NewType–>UserId 时,才能触发 typecheck

    • 也就是,只有确切使用 UserID(your_input)

      • 这样才能被进行检验

        1
        2
        
        # typechecks
        user_a = get_user_name(UserId(42351))
  • 不适用 UserId 包裹的参数

    • 不会被检验

      1
      2
      
      # does not typecheck; an int is not a UserId
      user_b = get_user_name(-1)

当作原类型使用

1
2
# 'output' is of type 'int', not 'UserId'
output = UserId(23413) + UserId(54341)
  • 注意

    • 上面的返回值 output,

      • 是 int 类型,不是 UserId 类型

在制作 新的 NewType 时,当作参数使用

1
Derived = NewType("Derived", UserId)

typing.Iterable

typing 规定的类型

  • Generic
  • Any
  • Tuple
  • List

    • 一种 list 的 Generic 类型
    • 注意

      • 推荐使用在 返回值类型
      • 而,在参数中,使用 Sequence 或者 Iterable

        1
        2
        3
        4
        5
        6
        7
        
        T = TypeVar('T', int, float)
        
        def vec2(x: T, y: T) -> List[T]:
            return [x, y]
        
        def keep_positives(vector: Sequence[T]) -> List[T]:
            return [item for item in vector if item > 0]
  • Callable
  • Iterable
  • Sequence
  • Mapping
  • Sized
  • NewType

    • 类型辅助函数

鸭子类 Vs. 明确的子类型

https://docs.python.org/3/library/typing.html#nominal-vs-structural-subtyping

明确的子类型 explicit base classes

  • PEP 484 中
  • 在 类定义的开头

    • 声明父类

      1
      2
      3
      4
      5
      6
      
      from typing import Sized, Iterable, Iterator
      
      class Bucket(Sized, Iterable[int]):
          ...
          def __len__(self) -> int: ...
          def __iter__(self) -> Iterator[int]: ...

鸭子类型

  • PEP 544 中

    • 即,遵循 class protocol
  • 类的方法

    • 接口与需要的类型相似,即可

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      from typing import Iterator, Iterable
      
      class Bucket:  # Note: no base classes
          ...
          def __len__(self) -> int: ...
          def __iter__(self) -> Iterator[int]: ...
      
      def collect(items: Iterable[int]) -> int: ...
      result = collect(Bucket())  # Passes type check

鸭子类,方法接口检验 typing.Protocol

python 3.8 特性 参考:

作用:

  • 说明接收的类型,有给定的方法等接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Proto(Protocol):
    def meth(self) -> int:
        ...

class C:
    def meth(self) -> int:
        return 0

def func(x: Proto) -> int:
    return x.meth()

func(C())  # Passes static type check
  • 这里 func(x: Proto)

    • 用来检验 实参 x

      • 是否使用了 Proto 规定的方法接口

当成 Generic 泛型使用

1
2
3
class GenProto(Protocol[T]):
    def meth(self) -> T:
        ...

运行时使用 @typing.runtime_checkable

作用:

  • 把 Protocol 子类标注为,在 runtime 时也可以使用

例子:

1
2
3
4
5
@runtime_checkable
class Closable(Protocol):
    def close(self): ...

assert isinstance(open('/some/file'), Closable)

变量的检查

  • 格式

    • 在注释中说明类型

      1
      2
      3
      4
      
      d = dict()  # type: Dict[str, int]
      
      d['id'] = 3
      d['id'] = 1.2    # --> 这里会报错,不合法
    • 冒号形式

      1
      2
      3
      4
      
      from typing import List
      
      
      members: List[str] = ['Lily', 'Lucy']

泛型之 协变、逆变、不变

Python 中

不变

  • 大部份 可变类型 都属于不变类型

    • List, Dict 类型 就属于不变类型
    • eg: List[Animal] 不允许 List[Cat]

      • 这里 List[Animal] 只能接收 Animal 本身
      • 因为 your_list[index] 会 按预先设定的 类型 进行检查

         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        
          class Shape:
          pass
        
          class Circle(Shape):
          def rotate(self):
              ...
        
          def add_one(things: List[Shape]) -> None:
          things.append(Shape())
        
          my_things: List[Circle] = []
          add_one(my_things)     # This may appear safe, but...
          my_things[0].rotate()  # ...this will fail
  • 内置不变类型 –> 协变变类型

    • 与 typing 模块中 的 泛型类型 版本关系
    • list –> List –> Sequence
    • dict –> Dict –> Mapping
builtins不变版本可变版本
listListSequence
dictDictMapping

协变

  • 大部份用户自定义的 泛型类型属于协变类型

    • Union 属于协变类型
  • eg: Union[Animal] 允许 Union[Cat]

手动设定,协变还是逆变

  • 使用 TypeVar('your_name', covariant=True)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    from typing import Generic, TypeVar
    
    T_co = TypeVar('T_co', covariant=True)
    
    class Box(Generic[T_co]):  # this type is declared covariant
        def __init__(self, content: T_co) -> None:
        self._content = content
    
        def get_content(self) -> T_co:
        return self._content
    
    def look_into(box: Box[Animal]): ...
    
    my_box = Box(Cat())
    look_into(my_box)  # OK, but mypy would complain here for an invariant type

Java 和 概念解释

协变

  • 意思

    • 允许子类
  • java

    1
    
    <? extends T>

逆变

  • 意思

    • 允许父类

      1
      
        <? super T>
  • 用法 与 作用

    • 用来限定 容器能放入的类型

      1
      2
      3
      4
      
        List<? super Number> list001 = new ArrayList<Number>();
        List<? super Number> list002 = new ArrayList<Object>();
        list001.add(new Integer(3));
        list002.add(new Integer(3));
      • 解释

        • 上面 List<? super Number> 限定了容器类型

          • 至少是 Number 类,或者 Number 类的父类
        • 这样我们就可以

          • 安全的在 容器中(list001),放入 Number 类的 子类类型的元素了

不变

  • 意思

    • 只允许当前类型
  • java

    1
    
    T

TypeGuard

python 3.10 特性

概念:

  • typeguard : 它是指 python 的一些类型判断语句或函数

    • 例子:简单例子

      1
      2
      3
      4
      5
      6
      7
      8
      
      def func(val: Optional[str]):
          # "is None" type guard
          if val is not None:
              # Type of val is narrowed to str
              ...
          else:
              # Type of val is narrowed to None
              ...
      • 解说:

        • 这里 val is not Noneisinstance(val, str) 进行类型缩窄(type narrowing)
        • 这就是一种 typeguard
        • 注:这种语句能够被 mypy 识别
    • 例子:自定义函数类型检验

      1
      2
      3
      4
      5
      6
      7
      
      def is_str_list(val: List[object]) -> bool:
          """Determines whether all objects in the list are strings"""
          return all(isinstance(x, str) for x in val)
      
      def func1(val: List[object]):
          if is_str_list(val):
              print(" ".join(val)) # Error: invalid type
      • 解说:

        • 这里函数 is_str_list() 检验一个列表是否是 List[str]
        • 这个函数也是一种 typeguard
        • 注:这种自定义函数作为 typeguard 不能被被 mypy 识别

解说:

  • 因此,typing.TypeGuard 引入了一种功能,让 mypy 也能识别自定义函数类型的 类型检验工具 (typeguard)

使用例子:

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

# 老写法
def is_str_list(val: List[object]) -> bool:
    """Determines whether all objects in the list are strings"""
    return all(isinstance(x, str) for x in val)

# 新写法
def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
    """Determines whether all objects in the list are strings"""
    return all(isinstance(x, str) for x in val)
  • 注释:

    • 这里把 -> bool 写成了 TypeGuard[List[str]]

      • 意为:检验数据类型是否是 List[str] 类型

例子:检验一个 class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Person(TypedDict):
    name: str
    age: int

def is_person(val: dict) -> "TypeGuard[Person]":
    try:
        return isinstance(val["name"], str) and isinstance(val["age"], int)
    except KeyError:
        return False

def print_age(val: dict):
    if is_person(val):
        print(f"Age: {val['age']}")
    else:
        print("Not a person!")
  • 注释: 这里使用了 "TypeGuard[Person]"

特点

  1. 一个 TypeGuard 自定义函数,总是返回 bool 类型
  2. 检验值 value,总是第一个位置参数

    1
    2
    
    def my_guard(value: InputType, ...) -> TypeGuard[CheckType]:
        ...
  3. 检验的类型 CheckType 总是比输入类型 InputType 严谨

注意

  1. 自定义 TypeGuard 使用时,不可使用 is True

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    from __future__ import annotations
    
    from typing import Any, TypeGuard
    
    class MyPositive:
        def __init__(self, value):
            self.value = value
    
    
    def is_my_positive(num: Any) -> TypeGuard[MyPositive]:
        return isinstance(num, MyPositive)
    
    
    
    def foo(val: None | MyPositive):
        # 正确用法
        if is_my_positive(val):
            return val.value
    
        # 错误用法,mypy 报错
        if is_my_positive(val) is True:
            return val.value
    • 验证: lsp-bridge + pyright

TypeAlias

python 3.10 特性

参考:

作用:

  • 用来区别赋值类别名类型
  • 告诉 mypy, 它是一个类型别名,不是一个 class 类对象
  • explicit syntax, 显式说明类型别名

    1
    2
    
    mytype = str                    # 赋值,类对象,mytype 是一个普通变量,implict
    alias_mytype: TypeAlias = str   # 类型别名, explicit

例子:

1
2
3
from typing import TypeAlias

Factors: TypeAlias = list[int]

ParamSpec

python 3.10 特性

参考:

作用:

  • 用来注解回调函数 callback 和装饰器 decorator 等高级函数用法

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from collections.abc import Callable
from typing import TypeVar, ParamSpec
import logging

T = TypeVar('T')
P = ParamSpec('P')

def add_logging(f: Callable[P, T]) -> Callable[P, T]: # *注意用法*
    '''A type-safe decorator to add logging to a function.'''
    def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        logging.info(f'{f.__name__} was called')
        return f(*args, **kwargs)
    return inner

@add_logging
def add_two(x: float, y: float) -> float:
    '''Add two numbers together.'''
    return x + y

解释:

  • 这里 P 用来注解整个函数参数表 Callable[P, T]

    • P 是参数表
    • T 是返回值
  • 获取位置参数方法: P.args
  • 获取关键字参数方法: P.kwargs

    • 传递给内部函数参数,就不能直接用 P,只能用 P.args 和 P.kwargs 了

Concatenate

python 3.10 特性

作用:

  • 和 Callable、ParamSpec 一起使用用来注解高级函数用法
  • 和 ParamSpec 一起使用,拼接函数参数表

特点:

  • 只能 用在函数参数表的开头

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
P = ParamSpec('P')
R = TypeVar('R')

my_lock = Lock()

def with_lock(f: Callable[Concatenate[Lock, P], R]) -> Callable[P, R]:
    '''A type-safe decorator which provides a lock.'''
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        # Provide the lock as the first argument.
        return f(my_lock, *args, **kwargs)
    return inner
  • 解说:

    • Callable[Concatenate[Lock, P], R]

      • 这里 Concatenate[Lock, P]: 拼接了函数参数表
      • 表示 Lock 和 P 中包含的参数一起构成了这个函数的参数表

Type

参考:

注:

  • 已经从 python 3.9 开始 deprecated, 推荐使用 builtin.type 代替

作用:

  • 类似 type(val), 获取一个变量的类型

例子:

1
a = int
  • 解说:

    • a 具有 int 类的类型,即 type(int) 类型, 这里规定为 Type[int] 类型

Literal

python 3.8 特性

参考:

作用:

  • 把子面量本身作为一个类型

例子:

1
2
3
4
5
6
7
8
9
def validate_simple(data: Any) -> Literal[True]:  # always returns True
    ...

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

open_helper('/some/path', 'r')  # Passes type check
open_helper('/other/path', 'typo')  # Error in type checker
  • 解说:

    • Literal[True]: 只接受 True 作为合法值
    • 即,它把 True 字面量作为了一个类型注解

ClassVar

参考:

作用:

  • 用来说明一个变量是类变量

    • 有些类似 @classmethod 功能
  • 禁止实例变量访问它

例子:

1
2
3
4
5
6
7
class Starship:
    stats: ClassVar[dict[str, int]] = {} # class variable 标注为类变量
    damage: int = 10                     # instance variable 没有标注,是实例标量

enterprise_d = Starship(3000)
enterprise_d.stats = {}    # 禁止访问Error, setting class variable on instance
Starship.stats = {}        # This is OK

Final

python 3.8 特性

作用:

  • 声明,变量是常量
1
2
3
4
5
6
7
8
MAX_SIZE: Final = 9000 # 全局常量
MAX_SIZE += 1  # Error reported by type checker

class Connection:
    TIMEOUT: Final[int] = 10    # 类常量

class FastConnector(Connection):
    TIMEOUT = 1  # Error reported by type checker

@final 装饰器

python 3.8 特性

作用:

  • 禁止方法重写
  • 禁止类继承
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Base:
    @final                      # 禁止重写
    def done(self) -> None:
        ...
class Sub(Base):
    def done(self) -> None:     # Error reported by type checker
        ...

@final                          # 禁止被继承
class Leaf:
    ...
class Other(Leaf):              # Error reported by type checker
    ...

Annotated

python 3.9 特性 参考:

作用:

  • 除类型之外,提供额外注解信息

特点:

  • 这个额外信息处理方式:

    • 在 static analysis 和 runtime (静态分析和运行时)都可以使用
    • 但是,怎么处理,由处理给定变量的工具 tool 或 library 决定

      • 没有处理功能,就会被忽略
  • 额外信息,可以有多个
  • 额外信息像 list 一样

    • 有顺序
    • 可重复
    • 可嵌套

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]

Annotated[int, ValueRange(3, 10), ctype("char")] # 多个额外信息


# 嵌套模式
Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
    int, ValueRange(3, 10), ctype("char")
]

TypedDict

python 3.8 特性

参考:

作用:

  • 声明的 key对应value的类型 做限定

特点:

  • 对没有声明的 key 对应的 value 类型无影响

用法:

  • 两种模式

    • 定义一个类

      • 支持继承
    • 通过函数创建
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Point2D(TypedDict):       # 类方式声明
    x: int
    y: int
    label: str

a: Point2D = {'x': 1, 'y': 2, 'label': 'good'}  # OK
b: Point2D = {'z': 3, 'label': 'bad'}           # Fails type check

assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')

Point2D = TypedDict('Point2D', {'in': int, 'x-y': int}) # 函数方式声明

忽略类型检查 ignore

参考:

1
2
name: str = 3                   # type: ignore
name: str = 3                   # pyright: ignore