1.4 Python进阶特性(类型标注)
1.4.1 类型标注介绍
Python属于动态类型语言,只有在运行代码的时候才能够知道变量类型,那么这样容易产生传入参数类型不一致的问题,导致出现意想不到的bug。这也是动态类型语言天生的一个问题。
所以在Python3.5的时候开始引入了类型标注
(Type Hint
),让我们能够显式地标注类型。经过后续版本更新,现在Python中的类型标注功能已经慢慢完善起来。
注意:在Python中添加类型标注静静是在语法层面,对代码的运行没有影响,Python解释器在执行代码的时候会忽略类型提示。
1.4.2 类型标注的优点
提升代码的可读性:可以很方便的得知变量的类型
提升代码的可用性:方便调用者传入、传出正确的类型参数,便于代码重构。
易于检测代码的逻辑问题:此处可以使用mypy工具进行静态检测代码逻辑问题。
提升开发效率:在PyCharm等IDE中,会根据变量的类型标注提示该变量的方法、代码补全等,方便开发。
1.4.3 类型标注语法
1.4.3.1 标注为单类型
最简答的类型标注其实就是为一个变量标注为某个数据类型,如下:
>>> a: str = ‘nanchang’
上述代码就是通过类型标注将变量a标注成了str
类型,今后在使用a时,Python会默认a的类型就是str
类型,如果遇到类型不符的操作时,就会给出提示。
类型标注在函数中更加常用,通常是用来为函数的参数添加类型标注,这样,当别人传入参数时,如果传入的实参类型与函数形参的类型不一致,则会进行提示,让开发者尽快发现问题。如下:
from icecream import ic
a: str = 'nanchang'
def myPow(n1: int, n2=2):
ic(n1 ** n2)
myPow(a)
此时,不用运行代码,我的PyCharm就提示类型 'str' 没有预期的特性 '__pow__'
,并且在最后的代码myPow(a)
的a的下方添加了波浪线提示。
为变量添加类型参数还有一个,尤其是函数中的形参,那就是之前提到过的,在IDE中,会根据变量的类型标注提示该变量的方法、代码补全等,方便开发。
在没有添加类型标注之前,对形参使用.
符号调用方法时,由于IDE不知道该形参是什么类型,会将Python自带的数据类型的所有方法一股脑全给列出来,不利于找到想要的方法。如下:
但是,在添加类型标注后,就只会列出指定数据类型的方法,十分方便。如下:
不关函数的形参可以添加类型标注,函数的返回也可以添加。语法如下:
def myPow(n1, n2=2) -> int:
ic(n1 ** n2)
return n1 ** n2
为函数返回值添加类型标注后,函数的返回值如果不是指定的类型,那么Python也会进行提示。
为变量指定其它Python自带单类型的标注如下:
a: bool = True
b: int = 2
c: float = 1.4
d: list = ["A", "B"]
e: dict = {"x": "y"}
f: tuple = ("age", "job")
g: set = {"a", "b"}
1.4.3.2 List
上面介绍了将变量标注为单类型,下面介绍一些其他类型。
如果想将某个变量的类型标注为list,但是内部的元素必须是特定的类型,则可以使用List。
from typing import List
a: List[str] = ['age']
def myfun(var: List[int]):
return sum(var)
1.4.3.3 Dict
同理,指定字典键值的数据类型,可以使用Dict。
from typing import Dict
my_dict: Dict[str, str] = {"name": "zhangsan", "gender": "man"}
1.4.3.4 Union
如果,要为某个变量指定多个类型,那么可以使用Union。
from icecream import ic
from typing import Union
def myfun(a: Union[str, int]):
ic(a * 2)
myfun(123)
myfun('123')
15:31:41|> a * 2: 246
15:31:41|> a * 2: ‘123123’
如上,对于myfun函数,如果我们想让它接收int或者str类型,如果是int类型,则返回参数的2倍,如果是str类型,则重复2遍再返回。那么用Union就可以实现了。
PS:从 Python 3.10 开始,Union 被替换为 | 这意味着 Union[X, Y, …] 现在等价于(X | Y | …)。当然,(X, Y, …)也是可以的。
1.4.3.5 Any
当变量的类型可以是任何类型时,可以使用Any。
from icecream import ic
from typing import Any
def myfun(arg: Any):
ic(arg)
myfun(2)
15:46:41|> arg: 2
1.4.3.6 Sequence
Sequence 类型的对象是可以被索引的任何东西:列表、元组、字符串等。
from icecream import ic
from typing import Sequence
def myfun(arg: Sequence):
for _ in arg:
ic(_)
myfun('wasd')
15:51:48|> _: ‘w’
15:51:48|> _: ‘a’
15:51:48|> _: ‘s’
15:51:48|> _: ‘d’
1.4.3.7 Tuple
Tuple 类型的工作方式与 List 类型略有不同,Tuple 需要指定每一个位置的类型,并且数量要一致。
from typing import Tuple
a: Tuple[int, int, int] = (1, 2, 3) # √
b: Tuple[int, int, str] = (1, 2, 3) # ×:第3个元素类型不对
c: Tuple[int, int] = (1, 2, 3) # ×:多了一个元素
1.4.3.8 Optional
Optional意思是说这个参数可以为空或已经声明的类型,即 Optional[类型1] 等价于 Union[类型1, None]。
需要注意的是这个并不等价于可选参数,当它作为参数类型注解的时候,不代表这个参数可以不传递了,而是说这个参数可以传为 None。
from icecream import ic
from typing import Optional
def myfun(arg: Optional[int] = 23):
ic(arg)
myfun()
myfun(None)
15:50:08|> arg: 23
16:50:35|> arg: None
1.4.3.9 Callable
当遇到类型标注为函数时,则可以使用Callable。如下:
from icecream import ic
from typing import Callable
def myfun(fun: Callable, *arg):
ic(fun(*arg))
myfun(max, 1, 2)
15:42:20|> fun(*arg): 2
我们甚至还可以给传入的函数参数指定参数列表及类型,语法如下:
>>> Callable[[input_type_1, …], return_type]
1.4.3.10 NewType
可以使用 NewType() 辅助函数创建不同的类型,有时候我们需要创建一种特定的数据类型,比如:用户id的数据类型。实际上,数据id数据类型就是int类型,但是,在使用时,如果有专门的用户id数据类型的话,会比较方便,并且也易于理解。
from icecream import ic
from typing import NewType
UserId = NewType('UserId', int)
some_id = UserId(524313)
ic(type(some_id), some_id)
15:20:35|> t1.py:6 in
type(some_id): <class ‘int’>
some_id: 524313
上面的代码中,创建了一种新的数据类型UserId
,其实就是int类型。静态类型检查器会将新类型视为它是原始类型的子类。这对于帮助捕捉逻辑错误非常有用:
from icecream import ic
from typing import NewType
UserId = NewType('UserId', int)
def get_user_name(user_id: UserId):
ic(user_id)
get_user_name(UserId(42351))
get_user_name(-1) # 这里会提示,类型不对
11:30:04|> user_id: 42351
11:30:04|> user_id: -1
您仍然可以对 UserId 类型的变量执行所有的 int 支持的操作,但结果将始终为 int 类型。这可以让你在需要 int 的地方传入 UserId,但会阻止你以无效的方式无意中创建 UserId:
from icecream import ic
from typing import NewType
UserId = NewType('UserId', int)
a = UserId(1) + UserId(2)
ic(type(a), a)
15:29:49|> t1.py:8 in - type(a): <class ‘int’>, a: 3
注意,这些检查只由静态类型检查器强制执行。 在运行时,语句 Derived = NewType(‘Derived’, Base) 将产生一个 Derived 函数,该函数立即返回你传递给它的任何参数。 这意味着表达式 Derived(some_value) 不会创建一个新的类,也不会引入超出常规函数调用的很多开销。
更确切地说,表达式 some_value is Derived(some_value) 在运行时总是为真。
这也意味着无法创建 Derived 的子类型,因为它是运行时的标识函数,而不是实际的类型:
from typing import NewType
UserId = NewType('UserId', int)
class AdminUserId(UserId):
pass
Traceback (most recent call last):
File “E:/t1.py”, line 7, in
class AdminUserId(UserId):
TypeError: function() argument ‘code’ must be code, not str
然而,我们可以在 “派生的” NewType 的基础上创建一个 NewType。
from typing import NewType
UserId = NewType('UserId', int)
ProUserId = NewType('ProUserId', UserId)
并且 ProUserId 的类型检查将按预期工作。
1.4.4 类型别名
类型别名通过将类型分配给别名来定义。在这个例子中, Vector 和 List[float] 将被视为可互换的同义词:
from typing import List
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
new_vector = scale(2.0, [1.0, -4.2, 5.4])
类型别名可用于简化复杂类型签名。例如:
from typing import Dict, Tuple, Sequence
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]
def broadcast_message(message: str, servers: Sequence[Server]) -> None:
pass
# 而非以下方式:
def broadcast_message(
message: str,
servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
pass
请注意,None 作为类型提示是一种特殊情况,并且由 type(None) 取代。
1.4.5 泛型(TypeVar)
有时候,我们在定义函数时,某些参数或者参数和返回值需要相同的类型,但是,参数或返回值的类型又不固定,可能是int或者str等,这个时候就可以用到泛型了。
简单理解就是泛型其实就是不确定类型标注是标注的类型是什么。类似值不确定,用变量来表示。
from typing import List, TypeVar
T = TypeVar('T')
def myfun(var: T) -> T:
return str(var)
myfun(1)
>>> mypy t1.py
t1.py:7: error: Incompatible return value type (got “str”, expected “T”)
Found 1 error in 1 file (checked 1 source file)
如上,我们创建了一个泛型:T
,代表任何类型的数据。在后续定义的myfun
中,参数var
的类型为T
(也就是任何类型都可以),而后函数的返回类型也是T
,但此时,返回的类型T
的实际类型必须和var一致。
在调用myfun
函数时,我们传入的参数的类型是int
,但是返回的类型是str
,这两者不一致,随后我们在使用第三方模块mypy
进行代码检查时就报错了:在第7行的类型和函数传入的参数的类型不一致。
注意:
1、在创建泛型时,如果可以是任何类型,则写法固定:T = TypeVar(‘T’)
2、如果,创建泛型时,是指定的类型,则写法为:A = Typevar(‘T’, int, str…)
from typing import TypeVar
A = TypeVar('A', str, bytes)
def longest(x: A, y: A) -> A:
return x if len(x) >= len(y) else y
ic(longest('abc', 'asdfsdfa'))
18:26:26|> longest(‘abc’, ‘asdfsdfa’): ‘asdfsdfa’
>>> mypy t1.py
Success: no issues found in 1 source file
1.4.6 对可变参数进行标注
在python中,函数的可变类型参数是指可以接受任意数量的值的参数,例如*args和**kwargs。要对这些参数进行类型标注,可以使用typing模块中的特殊类型,例如Any、Tuple、Dict等。也可以使用Python中默认的单类型。
1.4.6.1 标注*args
*args
接收后的参数会全部丢到元组中,如果确定*args
接收的参数都是同一种类型的,可以按照如下方式标注:
def add(*args: int) ->int:
sum_value = sum(args)
return sum_value
def foo(*args: Any) -> None:
# do something with args
pass
如果接收到的参数不止一种类型,那么就可以使用Union
from typing import Optional, Union
def add(*args: Union[str, int, float]) -> float:
sum_value = sum([float(item) for item in args])
return sum_value
print(add(2, '1', 4.8))
也可以使用Tuple
def baz(*args: Tuple[int, str]) -> int:
# do something with args
return 0
传入的可变参数可以是str,int,float中的任意一个,args虽然是元组,但是我们不是按照元组来进行标注,标注的是对这些参数的期望值。
1.4.6.2 标注**kwargs
对于**kwargs,因为会被作为字典接收,所以可以使用Union或Dict。
from typing import Any, Union
def add(**kwargs: Union[int, str, float]) -> None:
print(kwargs)
def baz(**kwargs: Dict[str, Any]) -> int:
# do something with kwargs
return 0