【python】类型约束(类型提示的作用)

文章目录

  • 前言
  • 一、类型系统
    • 1.动态类型
    • 2.静态类型
    • 3.鸭子类型
  • 二、变量注解
    • 1.变量注解的语法
    • 2.注解鸭子类型
  • 三、复杂(复合型)变量的注解
    • 1.引入
    • 2.难题
    • 3. Any的妙用
    • 4.类型变量
    • 5.类型Optional
  • 总结


前言

  1. python是一种解释型强类型动态语言
  2. python3.5以前是没有类型约束(类型提示)这一功能的
  3. python的类型提示只能起到提示的作用,是为了方便编码和阅读代码,但是仍然程序员可以xjb传,这点挺坑的
  4. 因为python是强类型的动态语言,Python解释器只在程序运行时才会做类型的检查,并且变量的类型在其生命周期内是可以改变的。所以,根本约束不住啊。
    综上,python的这一功能叫做类型提示更为合适,不应叫类型约束;php作为解释型若类型语言的代表,在php7(2016年)都引入了类型约束;python作为最流行的解释性语言,而且还是强类型的,实在不具备可解释性。

一、类型系统

所有的编程语言都有类型系统,类型系统规定了该语言支持的数据类型以及这些数据类型的行为。

1.动态类型

python就是一门典型的动态语言,只在程序运行时才会做类型的检查,并且变量的类型在其生命周期内是可以改变的。 (大白话:不运行我都不知道它是啥类型的,我约束个屁啊,但是php7做到了,作为曾经的phper,我骄傲,但我跑了)

def main():
    if False:
        1 + "Two"  # 1  这里故意写错,整数怎么能和字符串拼接在一起呢,(PHP可以,应该是做了隐士转换,或者重写了+运算符)
    else:
        1 + 2

    stuff = "Hello Eric Idle!"
    print(type(stuff))

    stuff = 2020
    print(type(stuff))


if __name__ == '__main__':
    main()

上述代买的运行结果:



2.静态类型

在静态类型的语言中,如:C、Java,在运行前的编译环节就完成了。

String stuff;
stuff = "Hello";

第一行声明了变量名称为stuff且指定其编译时的类型为String,在该变量的声明周期内,其不可以再被指定为其他数据类型;
第二行中将一个字符串对象赋给了变量stuff,且在其生命周期内,变量的值只可以是字符串对象。

3.鸭子类型

刚学python时,我是理解不了这句话的。
鸭子类型(Duck Typing):如果它走路像只鸭子,呱呱呱的叫声也像一只鸭子,那么它肯定是一只鸭子。
实际上,他讲的是定义数据类型的另一种方式:结构类型,
a str = 1 是名义类型(nominal type,如:str、float、int、list、tuple、dict等)
所以在使用鸭子类型时不关心对象的名义类型,而只关心对象是否实现了某协议所指定的方法。
Python中常见的为支持各类协议所预定义的类及需实现的方法如下表:

协议 方法
Container contains
Hashable hash
Iterable iter
Sized len
Callable call
Sequence getitemlen
假设我们规定,一种动物如果实现了“鸭子协议”,且该协议中指定了描述鸭子特征的两个方法__waddle__()和__quack__(),那么不管一个动物到底是鹅还是鸡,我们都可以称之为鸭子,这就是鸭子类型的含义。

现在你再看上面这个定义,可能就懂了。

class EricIdle(object):
    def __len__(self):
        return 2020


def main():
    eric = EricIdle()
    print("len(eric) = ", len(eric))


if __name__ == '__main__':
    main()

上述代码的运行结果为:

len(eric) = 2020

由上述代码的运行结果可知,调用len()得到了__len__()的返回值,即要想成功调用len(obj),唯一的限制仅在于obj中定义了__len__()方法。实际上,len()的实现类似于下列代码:

二、变量注解

1.变量注解的语法

在PEP 526中向Python引入了注解变量的语法.

from typing import ClassVar, Dict, List
import sys


primes: List[int] = []

captain: str


class Starship:
    stats: ClassVar[Dict[str, int]] = {}


def main():
    print(sys.modules[__name__].__annotations__)  # 1
    print(Starship.__annotations__)


if __name__ == '__main__':
    main()

上述代码的运行结果为:

{‘primes’: typing.List[int], ‘captain’: }
{‘stats’: typing.ClassVar[typing.Dict[str, int]]}
  1. 注解变量的列表和字典都是typing模块中的,其首字母均为大写,且通过方括号指定列表或字典中元素数据类型
  2. 对于被注解的变量,注解信息保存在模块层级的__annotation__字典属性中,这即为# 1处代码的含义。

2.注解鸭子类型

from typing import Sized


def len(obj: Sized) -> int:  # 1
    return obj.__len__()


def main():
    print(len.__annotations__)


if __name__ == '__main__':
    main()


输出:

{‘obj’: , ‘return’: }

对于参数obj,函数len()并不关心其名义类型是什么,只要其实现了__len__方法即可,进一步地,即只要其支持Sized协议即可,故有如上的语法。


三、复杂(复合型)变量的注解

1.引入

其实是符合型变量的注解,比如List[Dict],和自定义的Class
实际上,对于列表、元组等复合类型数据,为其添加类型提示,和为简单类型数据添加类型提示基本一致,如:

import sys
names: list = ["Guido", "Jukka", "Ivan"]
version: tuple = (3, 7, 1)
options: dict = {"centered": False, "capitalize": True}
print(sys.modules[__name__].__annotations__)

上述代码的运行结果为:

{‘names’: , ‘version’: , ‘options’: }

如names[2]是str,options[“centered”]是bool,但是如果变量names中的元素也是通过其他变量来表示,通过上述类型提示就无法获知如names[2]的数据类型。
上面一句话没看懂没关系,我给个例子:

import sys
from typing import Dict, List, Tuple
aaa = "abc"
names: List[str] = ["Guido", "Jukka", "Ivan",aaa]
version: Tuple[int, int, int] = (3, 7, 1)
options: Dict[str, bool] = {"centered": False, "capitalize": True}


print(sys.modules[__name__].__annotations__)

names:List[str] 就在告诉你少废话,你里面都是 str,不管是常量"123",还是变量aaa
上述代码的运行结果为:

{‘names’: typing.List[str], ‘version’: typing.Tuple[int, int, int], ‘options’: typing.Dict[str, bool]}

一个函数可能只期望接收的参数是一个序列(sequence),而并不关心其究竟是list、 str、 tuple,使用typing.Sequence来注解函数的参数。

from typing import List, Sequence

def square(elems: Sequence[float]) -> List[float]:
    return [x**2 for x in elems]

根据Python官方定义,sequence并不特指某一特定的数据类型,而仅是:

An iterable which supports efficient element access using integer indices via the __getitem__() special method and defines a __len__() method that returns the length of the sequence.
一个可迭代对象,该可迭代对象实现魔法方法__getitem__()并定义一个返回序列长度的__len__()方法,可以支持使用整数索引高效的访问其中的元素。
Some built-in sequence types are list, str, tuple, and bytes.
一些內置的序列类型有list、 str、 tuple以及bytes。

2.难题

类型提示发生嵌套时怎么办?

from typing import List, Tuple
List[Tuple[str, str]] # 是什么含义?

List[Tuple[str, str]] # 是什么含义? 他会越嵌套越多
别名的方式解决

from typing import List, Tuple

Card = Tuple[str, str]
Deck = List[Card]


def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
    """模拟4人抓牌的方式将一副牌平均分成4份"""
    return deck[0::4], deck[1::4], deck[2::4], deck[3::4]


def main():
    print(Card)
    print(Deck)
    print(deal_hands.__annotations__)
    

if __name__ == '__main__':
    main()

3. Any的妙用

any类型是我们的“躺平“方案, 实在嵌套太多了,类型提示本来是帮助我们理解代码,但层层嵌套,层层定义后,会伤害我们理解代码,我实在不能描述嵌套的是个啥了。就上any
在typing模块中,有一个名为Any的类型恰好可以用来注解无法确定的任意类型。

import random
from typing import Any, Sequence


def choose(items: Sequence[Any]) -> Any:
    return random.choice(items)


def main():
    print(choose.__annotations__)

    
if __name__ == '__main__':
    main()

上述代码的运行结果为:

{‘items’: typing.Sequence[typing.Any], ‘return’: typing.Any}

在我写这篇文章之前,我不知道这种类型应该返回啥
php上好像有mix:array,exception

def get_num(n:int)->float:
	if n == 0:
	   raise ValueError('n!=0')
	return 100/n

这种代码不会报错,但会给看的人造成困扰,以为任何情况下都返回float。
这个时候就可以用Any了,

4.类型变量

这个可就牛逼大方了,类比实现了JAVA/C++的泛型,使用typing模块中TypeVar类可以实现类似泛型的功能

import random
from typing import Tuple, Sequence, TypeVar


Card = Tuple[str, str]
Chooseable = TypeVar("Chooseable", str, Card)


def choose(items: Sequence[Chooseable]) -> Chooseable:
    return random.choice(items)


def main():
    choose([("♠", "A"), ("♡", "K")])
    choose(["P1", "P2", "P3", "4"])
    choose([1, 2, 3, 4])
    print(choose.__annotations__)


if __name__ == '__main__':
    main()

上述代码运行结果如下:

(‘♠’, ‘A’)
P2
2
{‘items’: typing.Sequence[~Chooseable], ‘return’: ~Chooseable}

虽然我们在程序中通过TypeVar创建了一个自定义类型Chooseable,并指定其可且仅可为str或Card,但实际上程序依然会正常执行,只是在带有类型检查器的IDE(如此处使用的PyCharm)或第三方类型检查器中(如大名鼎鼎的mypy),才会以提示或错误的形式显现出来。

【python】类型约束(类型提示的作用)_第1张图片

5.类型Optional

在做web api时,可选项参数(非必填项)建议的类型,在fastapi框架中很常见。

import random
from typing import List, Tuple, Sequence, TypeVar, Optional

Card = Tuple[str, str]
Chooseable = TypeVar("Chooseable", str, Card)


def choose(items: Sequence[Chooseable]) -> Chooseable:
    return random.choice(items)


def player_order(names: List[str], start: Optional[str] = None) -> List[str]:
    """旋转玩家顺序,使得start_player第一个出牌"""
    if start is None:
        start = choose(names)
    start_idx = names.index(start)
    return names[start_idx:] + names[:start_idx]


def main():
    print(player_order.__annotations__)
    

if __name__ == '__main__':
    main()

用来解决这个问题,通常变量start都应该是一个字符串,但是其也可以不传,此事default一个非字符串值None。

参考资料
[1] Python Type Checking (Guide)
[2] PEP 484 – Type Hints

总结

虽然Python中的类型提示是其一个非必须的特性,有和没有这个特性你都可以写出任何代码,但是在你的代码中使用类型提示可以使你的代码更具有可读性、更容易查找隐藏的bug并且使你的代码架构更加清晰

你可能感兴趣的:(python,python,开发语言)