所有的编程语言都有类型系统,类型系统规定了该语言支持的数据类型以及这些数据类型的行为。
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()
上述代买的运行结果:
在静态类型的语言中,如:C、Java,在运行前的编译环节就完成了。
String stuff;
stuff = "Hello";
第一行声明了变量名称为stuff且指定其编译时的类型为String,在该变量的声明周期内,其不可以再被指定为其他数据类型;
第二行中将一个字符串对象赋给了变量stuff,且在其生命周期内,变量的值只可以是字符串对象。
刚学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()的实现类似于下列代码:
在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]]}
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协议即可,故有如上的语法。
其实是符合型变量的注解,比如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。
类型提示发生嵌套时怎么办?
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()
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了,
这个可就牛逼大方了,类比实现了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),才会以提示或错误的形式显现出来。
在做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并且使你的代码架构更加清晰