Python进阶特性(类型标注)

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

猜你喜欢

转载自blog.csdn.net/crleep/article/details/130208922