学习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/