ref:
https://xinetzone.github.io/zh-CN/55e01cf0.html
https://docs.python.org/zh-cn/3/library/typing.html

[typing](https://docs.python.org/zh-cn/3/library/typing.html#module-typing) —- 类型标注支持

3.5 新版功能.
源码: Lib/typing.py
注解
The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc.


This module provides runtime support for type hints as specified by PEP 484, PEP 526, PEP 544, PEP 586, PEP 589, and PEP 591. The most fundamental support consists of the types Any, Union, Tuple, Callable, TypeVar, and Generic. For full specification please see PEP 484. For a simplified introduction to type hints see PEP 483.
函数接受并返回一个字符串,注释像下面这样:

  1. def greeting(name: str) -> str:
  2. return 'Hello ' + name

在函数 greeting 中,参数 name 预期是 str 类型,并且返回 str 类型。子类型允许作为参数。

类型注解

类型注解通过将类型分配给别名来定义。在这个例子中, VectorList[float] 将被视为可互换的同义词:

  1. from typing import List
  2. Vector = List[float]
  3. def scale(scalar: float, vector: Vector) -> Vector:
  4. return [scalar * num for num in vector]
  5. # typechecks; a list of floats qualifies as a Vector.
  6. new_vector = scale(2.0, [1.0, -4.2, 5.4])

类型注解可用于简化复杂类型签名。例如:

  1. from typing import Dict, Tuple, Sequence
  2. ConnectionOptions = Dict[str, str]
  3. Address = Tuple[str, int]
  4. Server = Tuple[Address, ConnectionOptions]
  5. def broadcast_message(message: str, servers: Sequence[Server]) -> None:
  6. ...
  7. # The static type checker will treat the previous type signature as
  8. # being exactly equivalent to this one.
  9. def broadcast_message(
  10. message: str,
  11. servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
  12. ...

请注意,None 作为类型提示是一种特殊情况,并且由 type(None) 取代。


0 引言:定义一个线段

假设我们使用 Python 实现数学中的整点线段的概念,即 $L = [a,b]$, 且 $a,b \in Z$。使用 Python 可以这样写:

  1. class LineSegment:
  2. def __init__(self, start, end):
  3. '''
  4. 整点线段
  5. =======
  6. start 和 end 均是整数
  7. '''
  8. assert start <= end, "线段的方向错误"
  9. self.start = start # 线段的起点
  10. self.end = end # 线段的终点
  11. def __len__(self):
  12. '''
  13. 获取线段的长度
  14. '''
  15. return self.end - self.start + 1

Python
类 LineSegment 模拟了线段的形态与长度。但是,这样的写法是不是有点繁琐?为了更加人性化,Python 提供了 typing 模块,让我们可以对代码进行注解。

1 typing 注解 Python

如果想要声明函数参数和返回值的类型,您可以这样做:

  1. def add(x:int, y:int=2) -> int:
  2. return x + y

这就是被称为 function annotation 的写法。使用冒号 : 加类型名来代表参数的类型,使用箭头 -> 加类型表示返回值的类型。注解部分不会被 Python 解析器所解析。只是一种注解的方式,类似于:

  1. def add(x, y):
  2. '''
  3. x: int
  4. y: int
  5. return int
  6. '''
  7. return x + y

注意:由于 Python 是动态语言,所以注解是对函数参数和返回值的“注释”,没有强制定义的作用。
比如,您像这样 add(1.2, 3.0) 传入参数,Python 解释器并不会报错。

  1. print("print 'add(1.2, 3.0)', result is", add(1.2, 3.0))

output:

  1. print 'add(1.2, 3.0)', result is 4.2

1.1 使用 inspect 检查 python 对象的类型

如果您想要让 Python 对类型进行检查,可以借助模块 inspect。比如:

  1. # 1、类型检查
  2. import inspect
  3. inspect.isfunction(add) # 判断add是否是函数
  4. inspect.ismethod(add) # 判断add是否是类的方法
  5. inspect.isgenerator(add) # 判断add是否是生成器对象
  6. inspect.isclass(add) # 判断add是否是类

如果对函数的参数进行检查呢?这个需要借助 sig=inspect.signature

  1. from inspect import signature
  2. # 获得函数的签名
  3. sig = signature(add)
  4. sig

output

  1. <Signature (x: int, y: int = 2) -> int>

接着,可以直接获取函数的信息:

  1. params = sig.parameters # 获取函数的参数信息
  2. params

output:

  1. mappingproxy({'x': <Parameter "x: int">, 'y': <Parameter "y: int = 2">})

Parameter 是 inspect 下的一个类,可以把它看做是一个有序字典,里面存放了函数的参数和参数类型,遍历 params.values() 的 annotation 属性会得到参数的注解类型:

  1. for v in params.values():
  2. print(v, 'annotation is', v.annotation)

output:

  1. x: int annotation is <class 'int'>
  2. y: int = 2 annotation is <class 'int'>

可以这样定义类型检查函数:

  1. from inspect import signature
  2. def checkParameterType(fn):
  3. def wrapper(*args, **kwargs):
  4. sig = signature(fn)
  5. # 获取函数的参数信息
  6. params = sig.parameters
  7. values = params.values()
  8. for p, v in zip(args, values):
  9. print(p, v)
  10. # 判断传入参数的类型和参数注解类型是否相符
  11. if not isinstance(p, v.annotation):
  12. print('TypeWrong')
  13. for k, v in kwargs.items():
  14. if not isinstance(v, params[k].annotation):
  15. print('TypeWrong')
  16. return fn(*args, **kwargs)
  17. return wrapper
  18. @checkParameterType
  19. def add(x: int, y: int = 2) -> int:
  20. return x + y

1.2 利用 python 对象的 annotations 属性检查类型

下面以定义一个商品对象:

  1. from typing import Any
  2. def goods(name: str, price: float) -> Any:
  3. return goods
  4. fish = goods('fish', 10.0)
  5. fish.__annotations__

输出结果是(Any 代表 Python 很难表达的形式或者类型):

  1. {'name': str, 'price': float, 'return': typing.Any}

可以看出,annotations 属性与 1.1 中的 params.values()[k].annotations 很相似。
这样,可以改写其类型检查函数为:

  1. def checkParameterType(fn):
  2. def wrapper(*args, **kwargs):
  3. annotations = fn.__annotations__
  4. for k, v in zip(annotations.keys(), args):
  5. if not isinstance(v, annotations[k]):
  6. print('TypeWrong')
  7. for k, v in kwargs.items():
  8. if not isinstance(v, annotations[k]):
  9. print('TypeWrong')
  10. return fn(*args, **kwargs)
  11. return wrapper

2 dataclass 提供强大的类型注解机制

dataclass 的定义位于 PEP-557,一个 dataclass 是指“一个带有默认值的可变的 namedtuple”,广义的定义就是有一个类,它的属性均可公开访问,可以带有默认值并能被修改,而且类中含有与这些属性相关的类方法,那么这个类就可以称为 dataclass,再通俗点讲,dataclass 就是一个含有数据及操作数据方法的容器。
我们先看看 dataclass 的参数:

key 含义
init 指定是否自动生成__init__,如果已经有定义同名方法则忽略这个值,也就是指定为True也不会自动生成
repr init,指定是否自动生成__repr__;自动生成的打印格式为class_name(arrt1:value1, attr2:value2, ...)
eq init,指定是否生成__eq__;自动生成的方法将按属性在类内定义时的顺序逐个比较,全部的值相同才会返回True
order 自动生成__lt____le____gt____ge__,比较方式与eq相同;如果order指定为Trueeq指定为False,将引发ValueError;如果已经定义同名函数,将引发TypeError
frozen 设为True时对field赋值将会引发错误,对象将是不可变的,如果已经定义了__setattr____delattr__将会引发TypeError

参数 unsafehash 的使用比较复杂,当设置为unsafehash=True时将会根据类属性自动生成__hash__,然而这是不安全的,因为这些属性是默认可变的,这会导致hash的不一致,所以除非能保证对象属性不可随意改变,否则应该谨慎地设置该参数为True。对于 unsafehash=False 的情况将根据eqfrozen参数来生成__hash__,下面单独列出:

参数设置 含义
eq=True,frozen=True __hash__将会生成
eq=True,frozen=False __hash__将被设为None
eq=False,frozen=True __hash__将使用超类(object)的同名属性(通常就是基于对象idhash

注意:最好去掉了init方法,以确保 dataclass 装饰器可以添加它生成的对应方法。
如果要覆盖 init,我们将失去 dataclass 的优势,因此,如果要处理任何附加功能可以使用新的 dunder 方法:post_init,让我们看看post_init方法对于我们的包装类来说是什么样子的。
由于 Python 支持中文作为变量名、类名、函数名等,所以,我可以这样定义一个 Python 类:

  1. from dataclasses import dataclass
  2. @dataclass
  3. class BookList:
  4. name: str
  5. price: float
  6. num: int=0
  7. def __post_init__(self) -> float:
  8. self.sum = self.price * self.num
  9. book1 = BookList('书籍', 10.0, 7)
  10. print(f'{book1},花费的成本{book1.sum}元')
  1. BookList(name='书籍', price=10.0, num=7),花费的成本70.0

上面的代码模拟了出售商品的清单,整个代码看起来都很清爽。