1+1=10

扬长避短 vs 取长补短

Python类型提示与注解

学习FastAPI的入门文档,突然发现对标注很陌生,简单了解记录一下。

捋一捋

  • Python 3.0 引入函数注解(Function Annotations),PEP 3107
  • Python 3.5 引入类型提示(Type Hints),用于函数注解,PEP 484
  • Python 3.6 在3.5基础上,引入了变量注解,PEP 526
  • Python 3.8 引入静态鸭子类型(Protocols),PEP 544
  • Python 3.8 引入TypedDict,PEP 589
  • Python 3.9 引入灵活的函数与变量注解,PEP 539

PEP 3107 引入函数注解(Function Annotations)

  • https://peps.python.org/pep-3107/

背景:Python2.x 中,没有一个标准的方式 对函数的参数和返回值进行标注。故而在Python3.0中,引入一个单一和标准的方式来指定标准信息。

def myadd(a: 'first', b: 'second') -> 'sum of a and b':
    return a + b

print(myadd.__annotations__)

输出结果如下:

{'a': 'first', 'b': 'second', 'return': 'sum of a and b'}

基本格式:

identifier [: expression] [= expression]

注意看里面涉及默认值:

def myadd(a: 'first', b: 'second'=5) -> 'sum of a and b':
    return a + b

print(myadd.__annotations__)
print(myadd(10))

输出结果:

{'a': 'first', 'b': 'second', 'return': 'sum of a and b'}
15

Python加载模块时,会生成__annotations__,但是Python解释器自身不使用这些信息。

注解信息可以用于

  • 类型检查
  • 供IDE显示函数期待的类型
  • 函数重载
  • RPC参数

以及为参数和返回值生成文档。

PEP 484 引入类型提示(Type Hints)

  • https://peps.python.org/pep-0484/
  • https://peps.python.org/pep-0483/

PEP 3107 定义了函数标注的语法,但却未定义语义。 PEP 484 明确Python仍然是是一种动态语言,不会降至成静态语言。PEP 483 引入了 typing 模块,区分type与class概念。

类型提示的用法:

def myadd(a: int, b: int=5) -> int:
    return a + b
print(myadd(10))
print(myadd.__annotations__)

输出

15
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

注意,类型提示,只是提示。Python作为动态类型语言,不会进行运行时检查!!

下面的写法对Python来说没有问题:

def my_mul(a: int, b: int=5) -> int:
    return a * b

print(my_mul(3, "debao"))
print(my_mul.__annotations__)

结果:

debaodebaodebao
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

尽管如此,类型提示对IDE,以及静态代码分析工具比如 mypy 都比较有用。而且PEP484似乎就是受mypy的启发而诞生的。

typing模块

Python3.5 增加typing模块,该模块主要提供对类型提示的支持。这个模块变化很大...

  • typing中定义了内置类型的别名,比如 Dict,List、Set、ForzenSet、Tuple,【已经废弃
  • typing中定义了collections中类型的别名,比如DefaultDict、OrderedDict、ChainMap、Counter、Deque ,【已经废弃】
  • typing中定义的 Text,作为str别名,【已经废弃】
  • typing中定义的 collections.abc中类型的别名,AbsractSet、Collections、Container、itemsView、KeysView、Mapping、MappingView,.... 【已经弃用】
  • ...

感觉:能不用typing实现同样的效果,就不用typing。

可接受的类型提示

  • 内置的类(buit-in classes),含标准库以及第三方扩展库
  • 抽象基类
  • types中的类型
  • 用户定义的类

除此之外

  • None、Any、Union、tuple、Callable
  • typing中的具体类,比如Sequence、Dict
  • 类型变量
  • 类型别名

作为类型提示时,None等同于type(None)。

TypeVar

类型变量这个怎么理解?

如下例子,运行没问题:

from typing import TypeVar

T = TypeVar('T', int, str)

def my_add(x: T, y: T):
    pass

my_add(1, 2)
my_add('d', 'b')
my_add(2, 'd')

使用mypy进行检查,结果(最后一行不过):

> mypy mytest.py
mytest.py:10: error: Value of type variable "T" of "my_add" cannot be "object"  [type-var]

这个和Union很像,但是Union不要求参数都一样。

比如,上面代码中,T改为如下Union,mypy检查将没问题:

T = int | str

overload

同样,overload装饰器,并不是真的重载,只是用于代码检查,比如:

from typing import overload

@overload
def func(a: int) -> None:
    print("int")

@overload
def func(a: str) -> None:
    print("str")

def func(a):
    print("other")


func(1)
func("1")
func(1.0)

运行结果:

other
other
other

如果使用mypy进行检查,结果:

> mypy mytest.py
mytest.py:17: error: No overload variant of "func" matches argument type "float"  [call-overload]

生成器

生成器的类型提示:Generator[yield_type, send_type, return_type]

def echo_round() -> Generator[int, float, str]:
    res = yield
    while res:
        res = yield round(res)
    return 'OK'

a = echo_round()
next(a)
print(a.send(2.4))
print(a.send(3.6))

PEP 526 变量注解

  • https://peps.python.org/pep-0526/
my_var1: int
my_var1 = 1
my_var2: str = 'hello'
my_var3: list[str] = ['d', 'e', 'b', 'a', 'o']

print(__annotations__)

结果:

{'my_var1': <class 'int'>, 'my_var2': <class 'str'>, 'my_var3': list[str]}

注意,在Python3.9之前,list需要写成下面这样(新版本中废弃):

from typing import List
my_var3: List[str] = ['d', 'e', 'b', 'a', 'o']

对于类成员,写法类似:

class Test:
    my_var1: int
    my_var1 = 1
    my_var2: str = 'hello'
    my_var3: list[str] = ['d', 'e', 'b', 'a', 'o']

print(Test.__annotations__)

只是注解!

如下例所示,Test.__annotations__ 会包含my_var1,但是运行时它不存在:

class Test:
    my_var1: int
    my_var2: str = 'hello'
    my_var3: list[str] = ['d', 'e', 'b', 'a', 'o']

print(Test.__annotations__)
#print(Test.my_var1)

运行结果:

{'my_var1': <class 'int'>, 'my_var2': <class 'str'>, 'my_var3': list[str]}

添加一条

print(Test.my_var1)

运行报错,不存在这个属性:

Traceback (most recent call last):
  File "C:\Users\dbzha\PycharmProjects\pythonProject2\a.py", line 7, in <module>
    print(Test.my_var1)
          ^^^^^^^^^^^^
AttributeError: type object 'Test' has no attribute 'my_var1'. Did you mean: 'my_var2'?

ClassVar

用于标识类变量,还是实例变量。比如如下代码,正常运行是没问题的:

from typing import ClassVar
class Test:
    my_var1: ClassVar[int] = 1
    my_var2: int = 2


t = Test()
t.my_var1 = 3
t.my_var2 = 4

使用mypy进行检查,结果:

> mypy mytest.py
mytest.py:8: error: Cannot assign to class variable "my_var1" via instance  [misc]

NamedTuple

这个和TypedDict看起来很像,但是有很大不同。

这是collections.namedtuple()的对应版本,不直接用作注解。

class Employee(NamedTuple):
    name: str
    id: int

相当于:

Employee = collections.namedtuple('Employee', ['name', 'id'])

PEP 544 静态鸭子类型 Protocol

  • https://peps.python.org/pep-0544/

Protocol 这个名字,让人很晕。协议?接口?...

另外,这儿有两个概念,也不知道该怎么翻译:

  • Nominal Subtyping:名义子类型?PEP 484的涉及的范畴,按类型的字面量理解。
  • Structural Subtyping:结构子类型?本PEP的范畴,按照结构(行为)进行理解?

总之,手册看的晕乎乎的,还是从小例子入手。

例子

  • 定义Dog和Cat类
  • 定义函数walk()可以接受有walk成员的对象(鸭子类型)
class Dog:
    def __init__(self):
        pass

    def walk(self):
        print("Dog is walking")

class Cat:
    def __init__(self):
        pass

    def walk(self):
        print("Cat is walking")

def walk(animal):
    animal.walk()

walk(Dog())
walk(Cat())

问题:def walk(animal) 如何对参数添加类型提示??

添加类型提示?

单纯添加,简单。这么就可以了?

def walk(animal: Dog | Cat):
    animal.walk()

只是太傻了,再来几个动物怎么办,不能天天改这个吧?

不过也好办,那就弄个基类吧:

class Animal:
    def __init__(self):
        pass

    def walk(self):
        print("Animal is walking")

class Dog(Animal):
    def __init__(self):
        pass

    def walk(self):
        print("Dog is walking")

class Cat(Animal):
    def __init__(self):
        pass

    def walk(self):
        print("Cat is walking")

def walk(animal: Animal):
    animal.walk()

walk(Dog())
walk(Cat())

问题解决了,但是不符合Python的鸭子哲学

添加类型提示!

Protocol方案。

  • 定义一个接口协议 Animal,但是Dog、Cat不需要从它进行继承
  • Animal 可以用作类型提示。代表 Dog 和 Cat,因为它们有Animal的同样的成员
from typing import Protocol

class Animal(Protocol):
    def walk(self):
        pass

class Dog:
    def __init__(self):
        pass

    def walk(self):
        print("Dog is walking")

class Cat:
    def __init__(self):
        pass

    def walk(self):
        print("Cat is walking")

def walk(animal: Animal):
    animal.walk()

walk(Dog())
walk(Cat())

PEP 589 TypedDict

  • https://peps.python.org/pep-0589/

PEP 589引入TypedDict类型,可以为不同的键设置不同的值类型提示。

from typing import TypedDict

class Movie(TypedDict):
    name: str
    year: int

m1: Movie = {'name': 'The Godfather', 'year': 1972}
m2: Movie = {'name': 'The Godfather', 'year': '1972'}

以上代码执行没问题,但无法通过mypy的检查

> mypy .\anotation.py
anotation.py:8: error: Incompatible types (expression has type "str", TypedDict item "year" has type "int")  [typeddict-item]

NotRequired

  • https://peps.python.org/pep-0655/

PEP 589没有未TypedDict定义可选项,PEP655完善这部分内容:

NotRequired 用于定义某个字段是不是必须的。注意,这个和可选的Optional不同,Optional只是指可以为None。

class Point2D(TypedDict):
    x: int
    y: int
    label: NotRequired[str]

如果全都非必须,可以设置total=False

class Point2D(TypedDict, total=False):
    x: int
    y: int

PEP 539 灵活的函数与变量注解

  • https://peps.python.org/pep-0593

PEP 539 引入一个机制,将 PEP 484的类型标注扩展到任意的元数据(metadata)。

Python 3.9 增加新的类型 Annotated。比如,类型T用一个元数据x进行注解,Annotated[T, x]

例子:

from typing import Annotated
import types

def myadd(a: Annotated[int, "first"], b: Annotated[int, 'second']=5) -> int:
    return a + b

print(myadd(10))
print(myadd.__annotations__)

结果:

15
{'a': typing.Annotated[int, 'first'], 'b': typing.Annotated[int, 'second'], 'return': <class 'int'>}

注解与metadata

from typing import Annotated

T1 = Annotated[int, "first"]
T2 = Annotated[T1, "second"]
print(T1.__metadata__)
print(T2.__metadata__)

结果:

('first',)
('first', 'second')

从T2看出,嵌套的注解会被展平。也就是,下面是成立的:

assert Annotated[Annotated[int, "first"], "second"] == Annotated[int, "first", "second"]

但是,要注意顺序,下面的不成立:

assert Annotated[int, "first", "second"] == Annotated[int, "second", "first"]

get_type_hints()

代码:

from typing import Annotated, get_type_hints
import types

def myadd(a: Annotated[int, "first"], b: Annotated[int, 'second']=5) -> int:
    return a + b

print(myadd.__annotations__)
print(get_type_hints(myadd))

结果:

{'a': typing.Annotated[int, 'first'], 'b': typing.Annotated[int, 'second'], 'return': <class 'int'>}
{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

另外Python3.10引入的get_annotations()可以取代__annotations__

from inspect import get_annotations
print(myadd.__annotations__)
print(get_annotations(myadd))

带有类型参数的类型

有些容器可以包含其他值,比如dict、list、set、tuple。

容器类型

从Python3.9起,它们在typing中对应的Dict,List、Set、Tuple已经不建议使用。

比如,由str构成的list:

items: list[str]

由三个元素构成的tuple:

items_t: tuple[int, int, str]

由bytes构成的set:

items_s: set[bytes]

dict需要指定键和值的类型,比如键为str,值为float:

prices: dict[str, float]

Union

Python3.9 不需要导入typing中的 Union类型,直接使用|即可。

item: int | str

Optional

typing中的 Optional 也没必要用了,直接用Union即可。

Optional 会给人一个值可选的感觉,但实际上它只意味着可以为None。

比如

from typing import Optional
name: Optional[str] = None

等同于

name: str | None = None

参考

  • https://peps.python.org/pep-3107/
  • https://peps.python.org/pep-0483/
  • https://peps.python.org/pep-0484/
  • https://peps.python.org/pep-0526/
  • https://peps.python.org/pep-0544/
  • https://peps.python.org/pep-0589/
  • https://peps.python.org/pep-0593
  • https://peps.python.org/pep-0655/
  • https://docs.python.org/zh-cn/3/howto/annotations.html
  • https://docs.python.org/zh-cn/3/library/typing.html
  • https://medium.com/@life-is-short-so-enjoy-it/research-about-python-typing-annotated-95c9093f97c3
  • https://so1n.me/2020/06/03/Python%E7%9A%84TypeHints/

Comments