typing
是python3.5
中开始新增的专用于类型注解(type hints)的模块,为python
程序提供静态类型检查,如下面的greeting
函数规定了参数name
的类型是str
,返回值的类型也是str
。
def greeting(name: str) -> str:
return 'Hello ' + name
在实践中,该模块常用的类型有 Any, Union, Tuple, Callable, TypeVar,Optional和Generic等,本篇博客主要依据官方文档以及日常使用经验来探讨一下typing
模块的使用方法以及经验。
注意事项:typing模块虽然已经正式加入到了标准库中,但是如果核心开发者认为有必要的话,api也可能会发生改变,即不保证向后兼容性
简单的类型注解及其形式如开篇例子所示,那么除了默认的int、str
等简单类型,就可以通过typing模块来实现注解。首先,我们可以通过给类型赋予别名,简化类型注释,如下例中的Vector
和List[float]
是等价的。
from typing import List
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
上面的例子,似乎不能很好的体现类型注释别名的优势,官网还给了另外一个例子,非常生动形象:
from typing import Dict, Tuple, Sequence
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]
def broadcast_message(message: str, servers: Sequence[Server]) -> None:
pass
def broadcast_message2(
message: str,
servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
pass
毫无疑问,函数broadcast_message2
的注解明显比broadcast_message
更加简洁清晰。
可以使用NewType
来创建一个用户自定义类型,如:
from typing import NewType
UserId = NewType("UserId", int)
def get_user_name(user_id: UserId) -> str:
pass
# 可以通过类型检查
user_a = get_user_name(UserId(42351))
# 不能够通过类型检查
user_b = get_user_name(-1)
NewType
的实现方式很简单,在运行时,NewType(name, tp)
返回一个函数,这个函数返回其原本的值。静态类型检查器会将新类型看作是原始类型的一个子类。
# NewType实现代码
def NewType(name, tp):
def new_type(x):
return x
new_type.__name__ = name
new_type.__supertype__ = tp
return new_type
因为NewType
被看做原始类型的子类,因此在新类型上你可以进行原始类型允许的操作,且结果的类型是原始类型,看起来是不是很神奇,甚至有点绕!其实关键还是要理解其原理,举两个例子:
实例1
>>> user_id_1 = UserId(23)
>>> user_id_2 = UserId(46)
>>> user_id_1 + user_id_2
69
实例2
# 请和NewType第一个例子对比
def get_num(num: int) -> int:
return num
# 可以通过类型检查
get_num(1)
user_id: UserId = UserId(23)
# 可以通过类型检查
get_num(user_id)
请注意,这些检查仅会被静态类型检查程序强制执行。在运行时,Derived = NewType('Derived',Base)
将 Derived
一个函数,该函数立即返回传递给它的任何参数。这意味着表达式 Derived(some_value)
不会创建一个新的类或引入任何超出常规函数调用的开销。更确切地说,表达式 some_value is Derived(some_value)
在运行时总是为真。这也意味着无法创建 Derived
的子类型,因为它是运行时的标识函数,而不是实际的类型。
回调函数可以使用类似Callable[[Arg1Type, Arg2Type],ReturnType]
的类型注释,这个比较简单,例子如下,如果只指定回调函数的返回值类型,则可以使用Callable[..., ReturnType]
的形式:
from typing import Callable
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
pass
由于无法以通用的方式静态推断有关保存在容器(list set tuple
)中对象的类型信息,因此抽象类被用来拓展表示容器中的元素。如下面子里中,使用基类Employee
来扩展其可能得子类如 Sub1_Employee
、Sub2_Employee
等。但是其局限性明显,所以我们需要引入泛型(generics)。
from typing import Mapping, Sequence
def notify_by_email(employees: Sequence[Employee],
overrides: Mapping[str, str]) -> None:
pass
可以通过typing中的TypeVar
将泛型参数化,如:
from typing import Sequence, TypeVar
T = TypeVar('T') # Can be anything
A = TypeVar('A', str, bytes) # Must be str or bytes
def first(l: Sequence[T]) -> T: # Generic function
return l[0]
可以将用户字定义的类定义为泛型类:
from typing import TypeVar, Generic
from logging import Logger
T = TypeVar('T')
class LoggedVar(Generic[T]):
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value
def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new
def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value
def log(self, message: str) -> None:
self.logger.info('%s: %s', self.name, message)
Generic[T]
作为基类定义了类 LoggedVar
采用单个类型参数 T
。这也使得 T
作为类体内的一个类型有效。通过Generic基类使用元类(metaclass)定义__getitem__()
使得LoggedVar[t]
是有效类型:
from typing import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
泛型类型可以有任意数量的类型变量,并且类型变量可能会受到限制:
from typing import TypeVar, Generic
T = TypeVar('T')
S = TypeVar('S', int, str)
class StrangePair(Generic[T, S]):
pass
Any
是一种特殊的类型,静态类型检查器视Any
与任何类型兼容,任何类型与Any
兼容。
def foo(item: Any) -> int:
item.bar()
在实际使用中, Any, Union, Tuple, List, Sequence, Mapping, Callable, TypeVar,Optional, Generic
等的使用频率比较高,其中Union、Optional、Sequence、Mapping
非常有用,注意掌握。
Union
即并集,所以Union[X, Y]
意思是要么X类型、要么Y类型
Optional
Optional[X]
与Union[X, None]
,即它默认允许None类型
Sequence
即序列,需要注意的是,List
一般用来标注返回值;Sequence、Iterable
用来标注参数类型
Mapping
即字典,需要注意的是,Dict
一般用来标注返回值;Mapping
用来标注参数类型
贴一段类型标注的实例代码,是不是让人一目了然,不需要看具体代码逻辑就知道参数类型以及如何调用呢?
def __init__(
self,
X: Optional[Union[np.ndarray, sparse.spmatrix, pd.DataFrame]] = None,
obs: Optional[Union[pd.DataFrame, Mapping[str, Iterable[Any]]]] = None,
var: Optional[Union[pd.DataFrame, Mapping[str, Iterable[Any]]]] = None,
uns: Optional[Mapping[str, Any]] = None,
obsm: Optional[Union[np.ndarray, Mapping[str, Sequence[Any]]]] = None,
varm: Optional[Union[np.ndarray, Mapping[str, Sequence[Any]]]] = None,
layers: Optional[Mapping[str, Union[np.ndarray, sparse.spmatrix]]] = None,
raw: Optional[Raw] = None,
dtype: Union[np.dtype, str] = 'float32',
shape: Optional[Tuple[int, int]] = None,
filename: Optional[PathLike] = None,
filemode: Optional[str] = None,
asview: bool = False,
*, oidx: Index = None, vidx: Index = None):
类型标注可以使程序的维护性、使用性更高,这一点非常重要;另外,许多IDE配合类型标注可以增强智能提示功能,加快编码速度,提高效率,我们何乐而不为呢?