一、基本类型
使用python
的标准库typing
(Python3.5+
)可以为变量、函数、类提供类型注解,但在运行期,解释器并不会对注解的变量进行类型检查。
类型注解也没有静态类型检查的要求,需要mypy/pylint
等静态检查器,或者一个足够现代,集成了静态代码分析的IDE
。
如果这样,我们就可以
1.通过类型推导自动获取调用函数的签名、返回值类型,提高可读性与开发效率
2.通过类型检查,让运行期潜在的类型安全问题提前暴露
单变量注解的语法为:
apple: int = 0 + 0.0
# or
apple: "int" = 2.3
上述代码会在Pycharm2023
提示一个warning: Expected type 'int', got 'float' instead
这个语法补丁在一些场合不适用,比如元组解构
butter: int, charlie: float = (114, 5.14)
因为根据官方文档[link]中的文法,python
中带注解的赋值表达式的左手侧只能单变量,不支持解构
可以改写成下方丑陋的形式
butter: int
charlie: float
butter, charlie = (114, 5.14)
注意butter: int
并非butter
的声明,在赋值前使用会抛出undefined NameError
函数注解中,使用带注解的赋值表达式的语法注解形参,函数体使用->
与尾置类型来注解返回值类型
def stoi(s: str, base: int = 10) -> int:
return int(s, base=base)
b: int = stoi("107")
在运行期,通过__annotations__
或typing.get_type_hints
可以获取注解的函数签名与返回类型,对于全局变量,使用globals()['__annotations__']
print(type(stoi))
# <class 'function'>
print(stoi.__annotations__)
# {'s': <class 'str'>, 'base': <class 'int'>, 'return': <class 'int'>}
print(typing.get_type_hints(stoi))
使用管道符|
可以从不同类型创建可变类型(python3.10+
),如
duff: int | float = 1.2
duff = int(duff)
print(duff)
# 1
二、typing
typing
模块(python3.5+
)提供了复合注解功能
1.类型约束
聚合类型
使用typing.Union[...Types]
创建可变类型,等效于类型间的管道运算符
var = Union[T1, T2, T3]
assert(var == T1 | T2 | T3)
# a ref type may holding type T1 or T2 or T3
NoReturn
注解函数返回值类型,标识函数不能通过正常控制流返回,所以函数体若只有return
或pass
,也是类型错误的,因为会隐式地return None
from typing import *
def handler_bad_request(state: my.HttpConnectState) -> NoReturn:
code: int = my.parse_http_error_code(state)
raise my.BadHttpRequestError(code)
无返回值函数
尾置类型为None
或type(None)
注解一个无返回值函数(因为隐式返回None
),当然None
是一个类型为NoneType
的单例对象,不是真正的无返回值,不能对位于c/c++
中的void
。以下代码可通过mypy
检查
def get_none() -> None:
return None
可空类型
使用typing.Optional[T]
注解一个可空类型(常用于形参、返回值),其类型为None
或T
,即等效于Union[None, T]
,对位于csharp
中的可空值类型(Nullable<T>
),c++17
中的std::optional<T>
。使用Optional[T]
作为函数返回值,可以提醒调用者处理空值None
动态类型
typing.Any
不约束类型,也就是python
本质的动态类型,使用typing.Any
时,优先考虑Union
或TypeVar
泛型注解平替,因为Any
无法体现函数输入输出的类型关系。对位于csharp
中的动态类型(dynamic
),c++17
中的std::any
函数对象
typing.Callable
在注解中相当于collections.abc.Callable
抽象类,其中的纯虚函数为__call__
,使用typing.Callable
注解def
定义的函数或实现__call__
的对象,一个举例是装饰器等高阶函数
from typing import *
import time
def tictoc(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
tic = time.time()
func(*args, **kwargs)
print(f"clock time cost: {time.time() - tic}s")
return wrapper
@tictoc
def f() -> None:
time.sleep(1)
f()
如果关心函数签名,则def f(arg1: T1, ... argn: TN) -> RT
注解为Callable[[T1, T2, ..., TN], RT]
类型类型
typing.Type
在注解中相当于type
,type
也是一个内置类,且type(type) == type
。
之所以要像Callable
使用一个代理类是因为注解中,有时必要使用[]
来表达类型的约束,而python3.9
前的type
没有[]
下标运算符,list, map, set
等内置类型也没有
因为type(type) == type
,所以可以用Type
注解一个运行期决定的类型
import numpy as np
from typing import *
import random
dummy = np.zeros((3, 3), dtype=np.int32)
def up_cast(arr: np.ndarray) -> np.ndarray:
t: Type = random.choice([np.single, np.half, np.double])
return arr.astype(t)
print(up_cast(dummy))
Self
python
注解中,以下代码会让解释器停止思考
class Foo:
def clone(self) -> Foo:
return self
f = Foo().clone()
在第2
行,似乎解释器还未解析完Foo
类型,使用Foo
注解引起了undefined error
,为此要前向声明这个类型,如使用TypeVar
封装Foo
用于注解。另外使用Self
可以达到相同效果,可以将代码改成以下两种相当丑陋的形式
from typing import Self
class Foo:
def clone(self: Self) -> Self:
return self
f = Foo().clone()
or
class Foo:
def clone(self: "Foo") -> "Foo":
return self
f = Foo().clone()
鸭子类型
鸭子类型中,类型匹配不在于继承关系,而是在某种上下文中,一种类型能否在syntax
上替换某种未确定的类型
cpp
模板便是利用静态鸭子类型实现编译期泛型的,如任何实现了operator*=(const T&)
、operator++()
、operator!=(const T&)
的类型,都能作为std::for_each
的迭代器参数。
typing.Protocol
可视为python
中的鸭子类型概念,当类型T
实现了某一Protocol
子类P
中所有属性/方法时,注解才将T, P
视作匹配的类型
class HasRunMethod(Protocol):
def run(self) -> None:
pass
class A:
def run(self) -> None:
print("running A")
class B:
def lazy(self) -> None:
pass
valid_runner: HasRunMethod = A()
invalid_runner: HasRunMethod = B()
# Expected type 'HasRunMethod', got 'B' instead
2. 容器
List[T]/Set[T]
注解存储类型T
的列表/集合
Tuple[T1, T2, T3, ..., TN]
注解存储类型T1, T2, T3, ..., TN
的元组
Dict[K, V]
注解键类型K
,值类型V
的字典
typing
内还有其他非内置容器类型,如NamedTuple, DefaultDict
等(namedtuple
是创建NamedTuple
的工厂函数)
3.泛型
聚合类型Union
的具体类型是始终不确定的,而泛型类型typing.TypeVar
表示一种待确定的类型,静态类型检查器可以通过形参推导出T
的具体类型
TypeVar
创建模板参数,模板约束只能进行类型匹配或继承匹配(bound
),无法进行语义上的类型约束(类似cpp
中的SFINAE
或concepts
)。后者利用了不求值表达式,这在python
语法中没有,所以或许该考虑组合TypeVar
与Protocol
达成相同效果
一个泛型函数举例
from typing import List, TypeVar
T = TypeVar('T', int, float)
# only int or float is acceptable
def quick_sort(arr: List[T]) -> List[T]:
if len(arr) <= 1:
return arr
pivot: T = arr[len(arr) // 2]
left: List[T] = [x for x in arr if x < pivot]
middle: List[T] = [x for x in arr if x == pivot]
right: List[T] = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
res = quick_sort([1, 2, 3])
在Pycharm2023
中,上文代码中res
成功推导为list[int]
另外,让class
继承typing.Generic[T1, T2, ..., Tn]
,可以实现模板参数为T1, T2, ..., Tn
的类模板