众所周知,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变得复杂了