python Typing模块-类型注解

写在篇前

  typingpython3.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也可能会发生改变,即不保证向后兼容性

Type aliases

  简单的类型注解及其形式如开篇例子所示,那么除了默认的int、str等简单类型,就可以通过typing模块来实现注解。首先,我们可以通过给类型赋予别名,简化类型注释,如下例中的VectorList[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

  可以使用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

  回调函数可以使用类似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

Generics

  由于无法以通用的方式静态推断有关保存在容器(list set tuple)中对象的类型信息,因此抽象类被用来拓展表示容器中的元素。如下面子里中,使用基类Employee来扩展其可能得子类如 Sub1_EmployeeSub2_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]

User-defined generic types

  可以将用户字定义的类定义为泛型类:

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 Type

  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配合类型标注可以增强智能提示功能,加快编码速度,提高效率,我们何乐而不为呢?

你可能感兴趣的:(Python)