原文:https://zhuanlan.zhihu.com/p/662747187
[TOC]
Reference:
Python
是动态类型的解释型语言,所以有时类型不匹配的问题要拖到运行期才能被发现(比如对一个int
对象取下标)
还有就是如果没有类型信息,代码里naming
又很烂的话,就很难猜出来鼠标指着的那个对象是啥类型,之前帮老师弄一个Python
项目就因为这个问题很打脑阔
0-Basic
为了解决这些问题,我们可以使用Python3.5+
标准库引入的类型提示(Type Hints
)和辅助用的typing
模块
Py3.5+
可以为变量instance
、函数function
(方法method
)、类class
、模块module
提供类型提示
但类型提示只是一种注释,或者说元信息,解释器把类型信息存储到了function
或class type
的__annotation__
属性中
在运行期,解释器也并不会对变量的类型断言
类型进行也没有静态检查的要求,需要一个IDE
帮助我们根据Hint
进行类型推导,比如PyCharm
的Type Introspection
,还会在推导出矛盾时给出一个warning
或者使用mypy
等static 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
,而不赋值变量,则不会把这个变量名加入到globals
或locals
,也就是说直接使用会抛出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 True
,type
对象重载了__call__
,也就是()
,所以能通过type
对象调用对应类型的构造函数)
print(object.__class__ is type)
# True
print(type.__class__ is type)
# True
print(type(type(type(type(type)))))
# <class 'type'>
当然type
也是一个对象,而且object
和type
的类型都是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
被当作原始类型的子类,也就是不同的类型,这在语义上类似Go
的type
别名,不同类型使用相同的数据结构
然而运行时使用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
从样例可以看出,在静态类型推导中,Verse
和str
是不同类型。但在运行时,通过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
新建一个Tool
,Program: 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#
的readonly
,C/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++/Rust
的std::any
、Go
的interface{}
注意Any
和object
在类型推到中的区别
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 | ... | TN
(Py3.10
之前没有语法糖要使用typing.Union[T1, T2, ... TN]
)表示一种 和类型(相对于作为class
的积类型,class
内部是多个类型的组合),意味着在运行时对象的类型是T1, T2, ... TN
其中之一,用处是实现非继承的多态
语义上类似C# SumType
, C++ std::variant
,Rust 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::optional
,Rust 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
时,装饰器返回的闭包会丢失类型标注
这里被装饰的runner
被PyCharm
推导为(...) -> 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
...
相对地,就像其他语言中的OOP
,typing.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 Hint
中Py
内置容器不仅有非泛型版本,也有泛型版本
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
,组合CanAdd
和CanSub
的方式是再定义一个CanAddSub
同时继承自``CanAdd/CanSub/Protocol(
Protocol`需要最后被继承)
然后用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.1
对TypeVarTuple
的支持还是实验特性,需要在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#
里的in
和out
关键字..
假设某个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
这里我们定义了协变与逆变泛型分别为Out
与In
然后写了数据类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
的返回值类型overrite
成Freedom
,这是可以的,因为接口里这里是协变,而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
的形参类型overrite
成object
,这是可以的,因为接口里这里是逆变,而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
dataclass
是Py
中的结构体,它也是通过类型标注定义的
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
以上