众所周知,Python 是动态类型语言,运行时不需要指定变量类型。这一点是不会改变的,但是2015年9月创始人 Guido van Rossum 在 Python 3.5 引入了一个类型系统,允许开发者指定变量类型。它的主要作用是方便开发,供IDE 和各种开发工具使用,对代码运行不产生影响,运行时会过滤类型信息。
Python的主要卖点之一就是它是动态类型的,这一点也不会改变。而在2014年9月,Guido van Rossum (Python BDFL) 创建了一个Python增强提议(PEP-484),为Python添加类型提示(Type Hints)。并在一年后,于2015年9月作为Python3.5.0的一部分发布了。于是对于存在了二十五年的Python,有了一种标准方法向代码中添加类型信息。在这篇博文中,我将探讨如何使用它。
# def 函数名(变量: 类型) -> 返回值类型
def foo(a: int, b: int) -> int:
return a + b
print(f(1, 2))
class A:
name = "A"
def get_name(o: A) -> str:
return o.name
get_name(A)
# ~^~ 错误提示:应为类型 'A',但实际为 'Type[A]'
get_name(A()) # 正确写法
特殊情况: 出现循环依赖的情况可加引号解决此类问题,例如下面举了一个双向链表的例子
class Node:
def __init__(self, prev: "Node"):
self.prev = prev
self.next = None
# Python < 3.9
from typing import List
def my_sum(lst: List[int]) -> int:
total = 0
for i in lst:
total += i
return total
my_sum([0, 1, 2])
my_sum([0, 1, "2"])
# ~~~~~~~~^~ 这里的"2"将会提示报错
# Python >= 3.9
def my_sum(lst: list[int]) -> int:
total = 0
for i in lst:
total += i
return total
my_sum([0, 1, 2])
my_sum([0, 1, "2"])
# ~~~~~~~~^~ 这里的"2"将会提示报错
但如果my_sum中传入tuple也将会报错,可将类型List修改成Sequence,而Sequence也更常在实际中使用。
from typing import Sequence
def my_sum(lst: Sequence[int]) -> int:
total = 0
for i in lst:
total += i
return total
# 以下写法均合法
my_sum([0, 1, 2])
my_sum((0, 1, 2))
my_sum(b'0123')
my_sum(range(3))
def my_sum(d: dict[str, int]) -> int:
total = 0
for i in d.values():
total += i
return total
my_sum({"a": 1, "b": 2, "c": 3})
my_sum({"a": 1, "b": 2, "c": "3"})
# ~~~~~^^^~ 当传入值类型不符时就会报错
from typing import Union
def foo(x: Union[int, None]) -> int:
if x is None:
return 0
return x
f(None)
f(0)
Python 3.10及以上版本时,x: Union[int, None]可简写成x: int | None。当传入类型有可能含有None或者其他类型时,还可使用Optional。例如
from typing import Optional
def foo(x: Optional[int]) -> int:
if x is None:
return 0
return x
显然Optional的写法比Union的写法也更加清晰
在实际开发中,可能会想创建一个字符串列表,可以使用如下方法
user: list[str] = []
user.append(1)
# ~^~ 将会报错:应为类型 'str' (匹配的泛型类型 '_T'),但实际为 'int'
Any在我们没有进行类型标注时,那么类型默认即为Any,只是大部分时候我们认为显式是要好于隐式的。
from typing import Any
def foo(a: Any) -> Any:
return a
但是当函数没有返回值时,不能将函数返回值类型标注为Any,因为当函数没有返回值时,函数实际上是返回了None,下面就是一个不理想案例
from typing import Any
def foo(a: list) -> Any:
a.append(1)
lst: list = []
i: int = foo(lst)
当函数foo类型标注为Any时(实际上函数foo返回None),变量i永远不可能从函数foo中得到一个int,但是函数foo类型标注为Any,这个时候不会报错,因为IDE认为函数foo返回任何东西都有可能。如果我们把函数foo的返回类型改为None时
from typing import Any
def foo(a: list) -> None:
a.append(1)
lst: list = []
i: int = foo(lst)
# ~~~~^^^~ 将会报错:应为类型 'int',但实际为 'None'
上文提到没有进行类型标注时,那么类型默认即为Any,这里也亦如此,当函数foo的返回类型留空不填时还会出现如上问题,所以当函数不返回值的时候也不应该留空,还是有必要加上返回类型None的。
NoReturn当然也有函数真的不会返回东西,即他不是正常的运行完这个函数之后没有显示返回而返回None,他可能是raise了一个exception或直接退出了程序,这个时候可用NoReturn进行类型标注。
from typing import NoReturn
def error() -> NoReturn:
raise ValueError
Callable当我们要求传入的参数是可调用的时,我们可以使用Callable进行类型标注,下面以函数装饰器举例
from typing import Callable
def my_dec(func: Callable):
def wrapper(*args, **kwargs):
print("start")
ret = func(*args, **kwargs)
print("end")
return ret
return wrapper
my_dec(1)
# ~^~ 此处报错:Argument 1 to "my_dec" has incompatible type "int";
# expected "Callable[..., Any]"
接下来修改一下这个函数
from typing import Callable
def my_dec(func: Callable):
def wrapper(a: int, b: int):
print(f"args = {a}, {b}")
ret = func(a, b)
print(f"result = {ret}")
return ret
return wrapper
@my_dec
def add(a: int, b: int) -> int:
return a + b
add(1, 2)
这个函数装饰器的作用就是将两个参数a和b打印下来并且将最后的结果打印下来,结果如下
args = 1, 2
result = 3
但是如果被装饰的函数只接受一个参数时(没有或不止两个时)就会带来一些问题,如
@my_dec
def absolute(a: int) -> int:
return abs(a)
absolute(1, 2)
现在运行这个代码就会出错
args = 1, 2
Traceback (most recent call last):
File "example.py", line 19, in <module>
absolute(1, 2)
File "example.py", line 7, in wrapper
ret = func(a, b)
TypeError: absolute() takes 1 positional argument but 2 were given
那么现在想让这个函数func接受什么参数就返回什么值怎么办呢,就像对待列表和字典一样,做进一步定义
from typing import Callable
def my_dec(func: Callable[[int, int], int]):
def wrapper(a: int, b: int):
print(f"args = {a}, {b}")
ret = func(a, b)
print(f"result = {ret}")
return ret
return wrapper
# vvvvv~ 报错:Argument 1 to "my_dec" has incompatible type "Callable[[str, str], str]";
@my_dec # expected "Callable[[int, int], int]"
def add(a: str, b: str) -> str:
return a + b
# vvvvv~ 报错:Argument 1 to "my_dec" has incompatible type "Callable[[int], int]";
@my_dec # expected "Callable[[int, int], int]"
def absolute(a: int) -> int:
return abs(a)
absolute(1, 2)
Literal当我们想规定某个参数传进的值只能是规定的值时也可以使用Type Hint中的Literal
from typing import Literal
class Person:
def __init__(self, name: str, gender: Literal["male", "female"]):
self.name = name
self.gender = gender
a = Person("Li", "woman")
# ~~~~~~~^^^^^^^~ 将会报错:应为类型 'Literal["male", "female"]',但实际为 'Literal["woman"]'
b = Person("Bob", "male")
但是有时候我们想让一个变量如g保存gender但是这样导致了一个问题,没错会报错
g = "female"
a = Person("Li", g)
# ~~~~~~^~ Argument 2 to "Person" has incompatible type "str";
# expected "Literal['male', 'female']"
这个时候我们只要把g进行类型标注即可解决问题
g: Literal["male", "female"] = "female"
a = Person("Li", g)
但是这样g就太过麻烦,有没有办法简写呢?答案是有的,因为Type Hint在Python中也是一个object,也就是说能给他一个变量名
from typing import Literal
GenderType = Literal["male", "female"]
class Person:
def __init__(self, name: str, gender: GenderType):
self.name = name
self.gender = gender
g: GenderType = "female"
a = Person("Li", g)
NewType为避免直接给类型起别名的一下小弊端,Python为我们提供了NewType,让我们新建返回类型
from typing import NewType
UserId = NewType("UserId", int)
AttackPoint = NewType("AttackPoint", int)
class Player:
def __init__(self, uid: UserId, attack: AttackPoint):
self.uid = uid
self.attack = attack
def update_attack(self, atk: AttackPoint):
self.attack = atk
但是这也会带来一个问题,不能直接用int给值了,需要显式的将1变成UserId,100变成AttackPoint
p = Player(1, 100)
# ~~~~~~~^~~^^^~ 竟然报错了:
# | Argument 1 to "Player" has incompatible type "int"; expected "UserId"
# | Argument 2 to "Player" has incompatible type "int"; expected "AttackPoint"
# 正确写法
p = Player(UserId(1), AttackPoint(100))
当然Type Hint在Run Time的时候不会有任何影响,也让我们的程序不会容易出错了,但是这样也让Python变得复杂了