文章目录
- 前言
- 一、类型系统
- 1.动态类型
- 2.静态类型
- 3.鸭子类型
- 二、变量注解
- 1.变量注解的语法
- 2.注解鸭子类型
- 三、复杂(复合型)变量的注解
- 1.引入
- 2.难题
- 3. Any的妙用
- 4.类型变量
- 5.类型Optional
- 总结
前言
- python是一种解释型强类型动态语言
- python3.5以前是没有类型约束(类型提示)这一功能的
- python的类型提示只能起到提示的作用,是为了方便编码和阅读代码,但是仍然程序员可以xjb传,这点挺坑的
- 因为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()
上述代买的运行结果:
<class ‘str’>
<class ‘int’>
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 | getitem、len |
假设我们规定,一种动物如果实现了“鸭子协议”,且该协议中指定了描述鸭子特征的两个方法__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’: <class ‘str’>}
{‘stats’: typing.ClassVar[typing.Dict[str, int]]}
- 注解变量的列表和字典都是typing模块中的,其首字母均为大写,且通过方括号指定列表或字典中元素数据类型
- 对于被注解的变量,注解信息保存在模块层级的__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’: <class ‘collections.abc.Sized’>, ‘return’: <class ‘int’>}
对于参数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’: <class ‘list’>, ‘version’: <class ‘tuple’>, ‘options’: <class ‘dict’>}
如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),才会以提示或错误的形式显现出来。
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并且使你的代码架构更加清晰