在Python中使用Type Hints和Mypy

原文:https://zhuanlan.zhihu.com/p/662747187

[TOC]

Reference:

PEP 484

Python是动态类型的解释型语言,所以有时类型不匹配的问题要拖到运行期才能被发现(比如对一个int对象取下标)

还有就是如果没有类型信息,代码里naming又很烂的话,就很难猜出来鼠标指着的那个对象是啥类型,之前帮老师弄一个Python项目就因为这个问题很打脑阔

0-Basic

为了解决这些问题,我们可以使用Python3.5+标准库引入的类型提示(Type Hints)和辅助用的typing模块

Py3.5+可以为变量instance、函数function(方法method)、类class、模块module提供类型提示

但类型提示只是一种注释,或者说元信息,解释器把类型信息存储到了functionclass type__annotation__属性中

在运行期,解释器也并不会对变量的类型断言

类型进行也没有静态检查的要求,需要一个IDE帮助我们根据Hint进行类型推导,比如PyCharmType Introspection,还会在推导出矛盾时给出一个warning

或者使用mypystatic typing checker,会解析代码找出类型错误

Type Hint的好处总结是

类型推导自动获取调用函数的签名、返回值类型,提高可读性与开发效率

让运行期潜在的类型安全问题提前暴露

Type Hint

最基本的语法是使用标注赋值(Annotated assignment statements),表达式语法为<var> : <type hint> [= <expr>] ,其中type hint大多情况下是一个type或者类型名的字符串

alpha: int = 0
# or
bravo: "str" = 2.3
# PyCharm: Expected type 'str', got 'float' instead

PyCharm中按住Ctrl指向变量名可以看推导的类型)

但这个语法补丁在一些场合不适用,比如元组解构

charlie: int, delta: float = (114, 5.14)       

根据标注赋值表达式语法

annotated_assignment_stmt ::=  augtarget ":" expression
                               ["=" (starred_expression | yield_expression)]

单纯是因为左手侧只能单变量,不支持解构

此时可以改写成下方丑陋的形式

charlie: int
delta: float
charlie: int, delta: float = (114, 5.14)   

但要注意,只给出type hint ,而不赋值变量,则不会把这个变量名加入到globalslocals,也就是说直接使用会抛出Undefined NameError,举栗说

alpha: int 
bravo: "str" = "i hate cowards"
def main():
    charlie: int = 114
    delta: float
    print(locals()) # {'charlie': 114}
    print(globals()['bravo']) # i hate cowards

    # print(delta) 
    # -> UnboundLocalError: cannot access local variable 'delta' where it is not associated with a value
    # print(globals()['alpha']) -> KeyError: 'alpha'
    # print(alpha) -> NameError: name 'alpha' is not defined

main()

这里在main函数作用域中,delta未被加入locals()中不能使用,在全局作用域下,aplha没有加入到globals()中不能使用

或者我们在元组解构时,直接标注整个元组也行

someTup: tuple[int, float] = (114, 5.14)
echo, foxtrot = someTup
# PyCharm: Variable "echo" Inferred type: int
# PyCharm: Variable "foxtrot" Inferred type: float

为函数标注时,也是用: + 类型标注形参,并且在函数体之前使用->尾置类型来标注返回值

def stoi(s: str, base: int = 10) -> int:
    return int(s, base=base)
thatNumber = stoi("107")
# PyCharm: Variable "thatNumber" Inferred type: int

Type Alias

首先要知道在Python中一切皆对象,类型如int, str, list以及自定义类都是一个type类型的对象((list.__class__ is type) is Truetype对象重载了__call__,也就是(),所以能通过type对象调用对应类型的构造函数)

print(object.__class__ is type)
# True
print(type.__class__ is type)
# True
print(type(type(type(type(type)))))
# <class 'type'>

当然type也是一个对象,而且objecttype的类型都是type

import numpy as np
dummy = np.zeros((3, 3), dtype=np.int32)
def up_cast(arr: np.ndarray) -> np.ndarray:
    t: type = get_type_from_config(opt=[np.single, np.half, np.double])
    return arr.astype(t)
print(up_cast(dummy))

所以也把一个对象标注成type也没问题

typing.TypeAliasType

Py3.12中为type hint引入了类型别名的语法type <alias> = <expr>

type String = str
s1: String = 'Let life be beautiful like summer flowers'
Str = str
s2: Str = 'and death like autumn leaves.'
print(String.__class__, String)
# <class 'typing.TypeAliasType'> String
print(Str.__class__, Str)
# <class 'type'> <class 'str'>

# print(String()) -> TypeError: 'typing.TypeAliasType' object is not callable
print(Str())

从样例可以看出,可以从一个类型创建出新类型,但新类型的类型不是type,而是typing.TypeAliasType

所以无法直接从类型别名创建出新对象,但是使用一个对象引用原类型创建别名时,还可以用别名创建对象

为了这个语法,type变成了一个上下文关键字soft keyword,但我觉得为什么要和内置的type冲突呢,把这个关键字改成alias不合适嘛..

typing.NewType

另外一种从现有类型创建新类型的方法是typing.NewType,对于Type Hint而言,NewType被当作原始类型的子类,也就是不同的类型,这在语义上类似Gotype别名,不同类型使用相同的数据结构

然而运行时使用NewType从原始类型构造对象时,它只是返回实参,所以运行时对象还是原始类型

Verse = typing.NewType('Verse', str)


def say(some: Verse) -> None:
    print(some)


say(Verse('The world has kissed my soul with its pain,'
          ' asking for its return in songs.'))
# say('It is the tears of the earth that keep here smiles in bloom.')
# PyCharm: Expected type 'Verse', got 'str' instead

print(Verse.__class__)
# <class 'typing.NewType'>
print(isinstance(Verse, str))
# False
print(Verse('').__class__)
# <class 'str'>

# print(Verse([1, 2]))
# PyCharm: Expected type 'str', got 'list[int]' instead

从样例可以看出,在静态类型推导中,Versestr是不同类型。但在运行时,通过Verse()转化的str其实还是str对象啦

1-mypy

在继续之前,让我们先装备一个Python静态类型检查器,mypy

运行mypy会分析代码,并且出现类型推导矛盾或属性/方法错误时,mypy程序直接报错

但与PyCharm inspection一样,不要求所有代码都带有类型标注,所以不妨在代码重灾区加入Type Hint并使用mypy

首先打开终端输入pip3 install mypy安装mypy

之后把它集成到PyCharm IDE中,点击File -> Settings -> Tools -> External Tools

新建一个ToolProgram: mypy,设定项目目录和源代码目录,这样在IDE中右键-> External Tools -> mypy就可以运行mypy检查了

当然也可以每次Debug前运行一下mypy,打开入口脚本的Run Configuration,添加一个Before launch -> Run External Tools,启用mypy

mypy现在(1.6.1)的问题是,Py3.12更新的Type Hint特性还没支持

2-Utilities

typing提供了一些通用的标注类型用于Type Hint和类型推导

Final

typing.Final (Py3.8+)标注一个对象为只读,语义上类似C#readonlyC/C++const

from typing import Final
MAX_SIZE: Final = 9000
MAX_SIZE += 1  

class Connection:
    TIMEOUT: Final[int] = 10
class FastConnector(Connection):
    TIMEOUT = 1  # Error reported by type checker

# mypy:
# main.py:78: error: Cannot assign to final name "MAX_SIZE"  [misc]
# main.py:86: error: Cannot assign to final name "TIMEOUT"  [misc]

以官方文档为例,被标注为Final的对象不能重新赋值,同时作为类属性的Final对象不能在子类中被重写(overwrite)

Any

在类型推导中,被标注为typing.Any的对象为动态类型

可以被任何类型,同样也可以赋值给任何类型

语义上类似C++/Ruststd::anyGointerface{}

注意Anyobject在类型推到中的区别

def get_x(o: Any) -> int:
    return o.x
# mypy: okay

def get_x(o: object) -> int:
    return o.x
# main.py:79: error: "object" has no attribute "x"  [attr-defined]

Any是开放的,类型推导认为Any类型可以有任意属性/方法,而object是封闭的,使用object类没有的属性会使mypy报错

Any举例:反射

from typing import Any

class Some:
    def __init__(self):
        self.tick = 0

def touch(o: Any) -> Any:
    if hasattr(o, 'tick'):
       	o.tick = o.tick + 1
    return o


y = Some()
print(touch(y).tick)
# 1
print(touch('nothing'))
# nothing

Union

将对象标注为T1 | T2 | ... | TNPy3.10之前没有语法糖要使用typing.Union[T1, T2, ... TN])表示一种 和类型(相对于作为class的积类型,class内部是多个类型的组合),意味着在运行时对象的类型是T1, T2, ... TN其中之一,用处是实现非继承的多态

语义上类似C# SumTypeC++ std::variantRust Enumerated types

以我之前写的代码为例

import numpy as np
def forward(self, images_data: np.ndarray) -> list[np.ndarray] | np.ndarray:
    # ...
    result: list[np.ndarray] = self._forward(dispatched_jobs)

    for i in range(len(result)):
        result[i] = result[i][:real_batch_size, ...]
    if len(result) == 1:
        return result[0]
    return result
# mypy: okay

这里forward返回模型推理结果的类型是运行时确定的(读取的模型结构可能是多流输出或单流输出),因此使用Union标注返回值

Optional

typing.Optional[T]表示类型T的可空版本,当然Python对象可空,所以Optional[T]就是T | None

语义上类似C# Nullable<T>C++ std::optionalRust std::option

一般来说,不使用T的某个特殊值而是可空类型表示空值,因为可读性好而且可能没有适合的特殊值表示空值

from typing import Optional

class Node:
    # ... 
    
def dfs(root: Optional[Node]) -> None:
    if root is None:
        return
    dfs(root.left)
    print(root.val, end=', ')
    dfs(root.right)

比如在深度优先搜索中把形参标注为可空类型

Self

在类中标注方法,如果涉及到了当前类类型,比如这样

class Some:
    def clone(self) -> Some:
        return self._clone_impl()

那么解释器会认为在Some类内部Some还没有完成定义,所以使用了Some时解释器不认可代码,这像是一个缺陷似的

class Some:
    def clone(self) -> 'Some':
        return self._clone_impl()

此时可以改为用字符串标注,类型推导/类型检查器也可以接受

class Tensor:
    def __add__(self, other: Self) -> Self:
        # ...

    def __sub__(self, other: Self) -> Self:
        # ...

    def __mul__(self, other: Self) -> Self:
        # ...

Py3.11,可以使用typing.Self标注当前类类型了,一般常用于运算符重载

要注意,Self其实是一个泛型的语法糖(typing.TypeVar

所以子类overrite父类方法时,Self推导为子类类型,而不是父类类型

3-Introspection

对于类型标注,解释器会将变量名和标注存储在function/class/module对象的__annotations__属性里

但函数作用域内的局部变量的标注不会被存储在function对象的__annotations__

from typing import get_type_hints

class Some:
    a: int
    b: tuple[str, int]

    def __init__(self):
        self.c: int = 0
        self.d: float = 1.

    def f(self, x: int, y: str) -> float:
        tmp: list[int] = [1, 1, 4]
        return .0


glob: str = 'module var'
print(get_type_hints(Some))
# {'a': <class 'int'>, 'b': tuple[str, int]}
print(get_type_hints(Some().f))
# {'x': <class 'int'>, 'y': <class 'str'>, 'return': <class 'float'>}
print(globals()['__annotations__'])
# {'glob': <class 'str'>}

而且最好使用typing.get_type_hints在运行时获取Type Hints,因为它帮我们处理了不同的对象

现在我们获得了内省类型标注的能力,也许可以用它来做运行时类型断言,但我认为更应该提高Py代码内标注覆盖率来做静态断言

所以我认为get_type_hints更大的用处是为Python元编程提供基础设施(如本文第6部分的dataclass

4-Function/Method

Never

Python中,函数正常执行流return或无return语句,返回的是None

但是标注一个永不正常返回的函数,应该使用typing.Never,而不是None

from typing import Never

class CmdExit(Exception): pass

def stop_thread() -> Never:
    raise CmdExit()

(在Py3.11之前,需要换用typing.NoReturn

Callable

我们可以用typing.Callable来标注Python函数、方法或其他函数对象(重载了__call__的对象)

Callable可以是一个泛型(见下文第5部分),其中Callable[[Arg1, Arg2, ... ArgN], RetT]标注一个形参类型为Arg1, Arg2, ... ArgN,返回值类型为RetT的函数对象

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    ...  # Body

一个官方文档的例子

当然,除了回调,我们在实现函数装饰器时,形参也是Callable

def using_huawei_api(func: Callable) -> Callable:
    def wrapper(*args, **kwargs):
        init_huawei_api()
        res = func(*args, **kwargs)
        finalize_huawei_api()
        return res
    return wrapper


@using_huawei_api
def runner(img_paths: list[str], model_cfg: CfgNode) -> Err:
    # ...

what_type = runner
# PyCharm : Variable "what_type" Inferred type:
# (...) -> Any

当然我们实现装饰器时往往需要处理的是任意的函数对象,比如我的代码里@using_huawei_api在任意函数调用前后做了一些事

但是像这样把原函数标注成非泛型的Callable时,装饰器返回的闭包会丢失类型标注

这里被装饰的runnerPyCharm推导为(...) -> Any,也就是Callable[..., Any],一个形参变长,返回任意类型的函数

这肯定不好,会破坏我们的后续的类型推导

from typing import ParamSpec, TypeVar
Param = ParamSpec('Param')
T = TypeVar('T')

所以可以使用typing提供的辅助泛型ParamSpec表示形参类型,同时把返回值类型也标注为泛型

def using_huawei_api(func: Callable[Param, T]) -> Callable[Param, T]:
    def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
        # ...
    return wrapper

@using_huawei_api
def runner(img_paths: list[str], model_cfg: CfgNode) -> Err:
    # ...

what_type = runner
# PyCharm: Variable "what_type" Inferred type:
# (img_paths: list[str], model_cfg: CfgNode) -> Err

使用ParamSpec.args解构出arguments*args),ParamSpec.kwargs解构出keyword arguments**kwargs),然后将标注转发给闭包就可以,此时PyCharm也正确推导出了被装饰函数的签名/返回类型

Decorators

关于函数/方法,typing还提供了几个装饰器

其中和OOP相关的是typing.override/typing.final

class Base:
    def log_status(self) -> None:
        ...

class Sub(Base):
    @override
    def log_status(self) -> None:  # Okay: overrides Base.log_status
        ...

    @override
    def done(self) -> None:  # Error reported by type checker
        ...

以官方文档为例,使用override装饰方法时,是提示类型检查器我们在使用继承多态,父类中必须存在同名的相同签名的方法

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
    ...

相对地,就像其他语言中的OOPtyping.final可以装饰一个类或方法,表示类不能被继承或方法不能被override

另外,如果我们的函数需要重载(当然肯定不是C++中的重载概念,这里指的就是函数多态),可以用typing.overload装饰不同的重载版本

def get_vec(dim1: int, dim2: int | None, dim3: int | None) -> list[int] | list[list[int]] | list[list[list[int]]]:
    if dim3 is not None:
        return [[[0 for i in range(dim3)] for j in range(dim2)] for k in range(dim1)]
    if dim2 is not None:
        return [[0 for j in range(dim2)] for k in range(dim1)]
    return [0 for k in range(dim1)]

比如这个get_vec函数根据形参长度返回不同类型的对象,如果只是像这样把返回值标注成Union,那无论用户如何调用get_vec,得到的推导类型都是list[int] | list[list[int]] | list[list[list[int]]],那就没啥意义了,不同入参方式对应不同返回值,这时我们利用overload表达这种语义

@overload
def get_vec(dim1: int) -> list[int]:
    ...
@overload
def get_vec(dim1: int, dim2: int) -> list[list[int]]:
    ...
@overload
def get_vec(dim1: int, dim2: int, dim3: int) -> list[list[list[int]]]:
    ...
def get_vec(dim1, dim2=None, dim3=None):
    # ... implementation 


v1 = get_vec(1)
v2 = get_vec(1, 4)
v3 = get_vec(5, 1, 4)

# PyCharm:
# Variable "v1" Inferred type: list[int]
# Variable "v2" Inferred type: list[list[int]]
# Variable "v3" Inferred type: list[list[list[int]]]

使用overload需要像这样写,使用@overload装饰每一个重载版本,函数体使用...忽略,而且overload重载声明后要紧接实现的函数,这里@overload重载中已经进行了标注,有了类型推导的信息,所以具体实现函数中不用标注了

根据入参,PyCharm也正确推导出了不同的返回值类型

5-Generic

这里泛型指的是Type Hint里的泛型,Python运行时没有泛型可言

Type Hint中泛型参数包含在方括号([])中

Builtin Container

Type HintPy内置容器不仅有非泛型版本,也有泛型版本

list[T]/set[T]标注存储类型T的列表/集合

tuple[T1, T2, T3, ..., TN]标注存储类型T1, T2, T3, ..., TN的元组

dict[K, V]标注键类型K,值类型V的字典

当然,Py3.9以下要换用typing.List,typing.Set,typing.Tuple,typing.Dict,因为当时还没有为这些类型对象重载[]

Protocol

typing.Protocol定义一种鸭子类型(也就是非继承的接口),具体是定义一个空壳类,继承自Protocol,然后定义鸭子类型需要满足的方法

class CanSub(Protocol):
    def __sub__(self, other: Self) -> Self:
        pass

像这里我们定义一种可减的类型CanSub

def zero(x: CanSub) -> CanSub:
    return x - x
zero(1)
zero([1, 0, 7])

# mypy: error: Argument 1 to "zero" has incompatible type "list[int]"; expected "CanSub"  [arg-type]

然后被标注为CanSub的对象必须重载-才可以

通常一个具体类型可以满足多个鸭子类型,所以现在考虑如何组合不同的Protocol

@runtime_checkable
class CanAdd(Protocol):
    def __add__(self, other: Self) -> Self:
        pass
    
@runtime_checkable
class CanAddSub(CanAdd, CanSub, Protocol):
    pass

print(isinstance([], CanAddSub))
# False
print(isinstance([], CanAdd))
# True
print(isinstance(1, CanAddSub))
# True

现在再写一个可加类型CanAdd,组合CanAddCanSub的方式是再定义一个CanAddSub同时继承自``CanAdd/CanSub/ProtocolProtocol`需要最后被继承)

然后用typing.runtime_checkable装饰一下鸭子类型,就可以在运行时用isinstance判断对象是否满足相应Protocol

TypeVar

通过typing.TypeVar定义一个泛型

T = TypeVar('T')  # Can be anything
S = TypeVar('S', bound=str)  # Can be any subtype of str
A = TypeVar('A', str, bytes)  # Must be exactly str or bytes

默认的TypeVar是开放类型,但可以通过bound*constrains参数施加简单的泛型约束

T = TypeVar('T')
def slow_sort(arr: list[T]) -> list[T]:
    if len(arr) <= 1:
        return arr
    base: T = arr[len(arr) // 2]
    left: list[T] = [x for x in arr if x < base]
    middle: list[T] = [x for x in arr if x == base]
    right: list[T] = [x for x in arr if x > base]
    return slow_sort(left) + middle + slow_sort(right)

what_type = slow_sort(['a', 'c', 'b'])
# PyCharm: Variable "what_type" Inferred type: list[str]

创建一个泛型函数的例子,在Py3.12中,不用再定义TypeVar了,直接用def slow_sort[T](arr: list[T]) -> list[T]就可以定义泛型函数,但mypy好像还没支持这个特性

U = TypeVar('U')
V = TypeVar('V')
class pair(Generic[U, V]):
    def __init__(self, u: U, v: V):
        self.first = u
        self.second = v


p = pair(107, 'zixfy')
a, b = p.first, p.second
# PyCharm: Variable "a" Inferred type: int
#          Variable "v" Inferred type: str

而让类继承typing.Generic可以创建泛型类,同样地,在Py3.12中可以使用class pair[U, V]:定义泛型类

TypeVarTuple

mypy1.6.1TypeVarTuple的支持还是实验特性,需要在mypy程序参数中加入--enable-incomplete-feature=TypeVarTuple --enable-incomplete-feature=Unpack

变长泛型,也就是一个打包了若干类型的列表(TypeList)

通过typing.Unpack,我们可以把这些类型从TypeVarTuple中解构出来

考虑实现如下的类似C++ std::apply的功能,输入函数f和实参*args,在apply内部调用原函数f

这要求f形参类型和*args类型一致,所以样例中将形参和*args都标注为Unpack[Ts](变长的泛型)

from typing import Callable, TypeVarTuple, Unpack
T = TypeVar('T')
Ts = TypeVarTuple('Ts') 

def apply(f: Callable[[Unpack[Ts]], T], *args: Unpack[Ts]) -> T:
    return f(*args)

def f(x: int, y: float, prefix: str) -> str:
    return prefix + str(x + y)

# g = apply(f, 1.333, 1.333, "Result: ")
# mypy: Argument 1 to "apply" has incompatible type 
# "Callable[[int, float, str], str]"; 
# expected "Callable[[float, float, str], str]"


g = apply(f, 1, 1.333, "Result: ")
print(g)

这样,在调用apply,若实参类型不符,mypy会帮我们找出类型错误

如果不都标注为Unpack[Ts],而是标注为...,则无法检查实参,因为...表示任意的变长泛型,而且如果同时把原函数形参和*args都标注为...mypy不认可

同时可以看到,我们可以利用TypeVarTuple标注Py函数的变长参数(*args)了

TypedDict

Py中,函数不仅可以有变长参数*args,还可以有关键字参数**kwargs,kwargs在运行时就是一个dict

相应地,Py3.11引入了typing.TypedDict,限制dict指定键需要对应特定类型

结合Unpack,我们就可以标注kwargs了喔

from typing import Unpack, TypedDict, NotRequired

class Opt(TypedDict):
    lower: int
    upper: int
    as_type: NotRequired[type]

首先定义一个空类Opt标注关键字参数的名字和类型,如果某个参数不是必须的,则将对应类型T标注为typing.NotRequired[T]版本

def clip(x: int, **kwargs: Unpack[Opt]):
    r: int = max(min(x, kwargs['upper']), kwargs['lower'])
    print(kwargs.__class__)
    if 'as_type' in kwargs:
        return kwargs['as_type'](r)
    return r


g = clip(-1, lower=107, upper=109, as_type=str)
print(g, type(g))
# 107 <class 'str'>

# h = clip(1, lower=1 )
# mypy: Missing named argument "upper" for "clip"  [call-arg]

# l = clip(114, lower=514, upper=1919.810)
# mypy: Argument "upper" to "clip" has incompatible type "float"; expected "int"  [arg-type]

在函数签名内,使用Unpack[Opt]解构TypedDict标注**kwargs,可以看到mypy已经可以找出kwargs中的类型错误了

Contravariant/Covariant

协变(covariant)和逆变(contravariant)是泛型(TypeVar)的两个关键字参数

其实这个有些像C#里的inout关键字..

假设某个TypeVar被推导为具体类型T,如果TypeVar是协变的,那么也可以被看作T的某个子类型(更加严格的类型)

反之如果TypeVar是逆变的,那么也可以被看作T的某个父类型(更加宽松的类型)

我们往往把某个接口方法返回值定义成协变的,因为接口的实现类里往往信息更多,可以生成更详细的子类型

同样,也可以把某个接口方法形参定义成逆变的,因为接口的实现类往往信息更多,可以处理更不详细的父类型

还是举个栗子吧

class Item(object): pass
class Bread(Item): pass
class Freedom(Item): pass


Out = TypeVar('Out', covariant=True)
In = TypeVar('In', contravariant=True)


class Factory(Generic[In, Out]):
    @abc.abstractmethod
    def get(self) -> Out:
        pass

    @abc.abstractmethod
    def consume(self, x: In) -> None:
        pass

这里我们定义了协变与逆变泛型分别为OutIn

然后写了数据类Item/Bread/Freedom,其中Item是父类

最后定义了一个泛型接口Factory[In, Out],其get返回值Out是协变的,而consume形参In是逆变的

class Dream(Factory[Item, Item]):
    @override
    def get(self) -> Freedom:
        return Freedom()

    @override
    def consume(self, x: Item) -> None:
        print(f"Dream eats {type(x).__name__}, ")

Factory[Item, Item]的实现类Dream中,我们将get的返回值类型overriteFreedom,这是可以的,因为接口里这里是协变,而Freedom又是Item的子类;但将Freedom替换成Item的父类object就是不可取的

class Work(Factory[Item, Item]):
    @override
    def get(self) -> Bread:
        return Bread()

    @override
    def consume(self, x: object) -> None:
        print(f"Work burns {"anything" if type(x) is object else type(x).__name__}, ")

而在Factory[Item, Item]的另一个实现类Work中,我们将consume的形参类型overriteobject,这是可以的,因为接口里这里是逆变,而object又是Item的父类

但将object替换成Item的子类Bread时,mypy会报错This violates the Liskov substitution principle,里氏原则里有说任何基类可以出现的地方,子类一定可以出现,但这里父子关系是反过来的,可能这就是为什么叫做逆变吧..

Dream().consume(Bread())
Work().consume(Dream().get())
Work().consume(object())
# Dream eats Bread, 
# Work burns Freedom,
# Work burns anything,
# mypy: okay

# Dream().consume(object())
# mypy: no

btw, type,或者说typing.Type的泛型版本type[C]也是协变的

6-dataclass

dataclassPy中的结构体,它也是通过类型标注定义的

from dataclasses import dataclass

@dataclass
class PersonImageInformation(object):
    image_name: str
    person_id: int
    image_quality: float

这里用,@dataclass装饰PersonImageInformation

PersonImageInformation会被添加和定义的类属性相对应的构造方法、哈希、重载<, >, ==, str()

info = PersonImageInformation(
    image_name='/some/path',
    image_quality=0.92,
    person_id=107)

# info2 = PersonImageInformation(
#    image_name='/some/path',
#    person_id=107)
# mypy: Missing positional argument "image_quality" in call to "PersonImageInformation"  [call-arg]

# info3 = PersonImageInformation(
#     image_name=0,
#     image_quality=0.92,
#     person_id=107)
# mypy: Argument "image_name" to "PersonImageInformation" has incompatible type "int"; expected "str"  [arg-type]

可以看到生成的构造函数也是带类型标注的,实际上就是通过内省__annotation__属性拿到的,但dataclass用的不是typing模块,而是inspect模块

cls_annotations = inspect.get_annotations(cls)	

在添加构造方法__init__时,类型标注也被写进方法的签名里

def _init_param(f):
    # Return the __init__ parameter string for this field.  For
    # example, the equivalent of 'x:int=3' (except instead of 'int',
    # reference a variable set to int, and instead of '3', reference a
    # variable set to 3).
    # ...

有些库也是像这样利用Type Hints进行元编程的,比如pydantic

以上