静态与动态类型化是软件工程中的热门话题,每个人都有自己的独道简洁, Python作为一种动态类型语言,这使得程序不需要指定变量的类型,这一点是不会改变的。 但python创始人Guido van Rossum在python3.5中引入了一个类型系统, 它允许开发人员指定变量类型,主要作用是便于开发维护代码,供IDE和开发工具使用, 对代码运行不产生任何影响,运行时会过滤类型信息。
python在构建大型项目上一直遭人诟病, 除了自身性能在个别领域不尽如人意外,动态类型的语言特点也使得python并不适合构建大型项目。
构建大型项目对于python来说是一个技术可行但工程上困难重重的事情,设想编写了一个函数, 定义了若干个参数,期初还记得这些参数的类型是什么,但天长日久, 写了几万行代码之后还会记得这个函数的参数最初都是如何定义的么?倘若已经遗忘, 又如何要求其他人能够读懂并维护代码呢?其他强类型语言诸如C++, java 在定义变量时必须指定其类型,因此在维护代码方面,及时缺少注释也仍然可以通过阅读代码来获取变量的信息, 而python不可以,如果python开发人员不注意变量命名规则,也不喜欢写注释, python的代码真的很难理解和维护,尤其在使用了一大堆诸如装饰器等特性之后, 理解起来更是难上加难。
尽管pyhton从3.5版本开始就引入了类型系统。 本文将通过实际的例子展示如何在python代码里做类型标注。
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: None = None
e: float = 9.8
f: bytes = b'32'
使用 mypy
对类型标注进行检查
检查结果
这说明对这4种变量的类型标注是正确的,但上面的代码存在严重的缺陷,
变量 d
为它标注为 None
,那么 d
这个变量就永远只能为 None
了,
如果将其赋值为其他类型,类型标注检查就会报错, 修改代码。
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: None = None
e: float = 9.8
f: bytes = b'32'
d = 5
使用 mypy
检查结果:
typehint.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "None")
Found 1 error in 1 file (checked 1 source file)
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: Optional[int] = None
e: float = 9.8
f: bytes = b'32'
d = 5
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: Optional[Union[int, float]] = None
e: float = 9.8
f: bytes = b'32'
d = 5
d = 9.8
d = None
Union
表示或的意思,d
变量的类型,可以是 None
,也可以是 int
或者 float
。
可不可以将a变量的类型标注设置为 Union[int, float]
,
让 a
以赋值成 int
也可以赋值成为 float
? 从纯粹的技术实现上讲这样做没有问题。
from typing import Optional, Union, Any
a: Union[int, float] = 8 # 坚决反对你这样做
b: bool = True
c: str = 'ok'
d: Optional[Union[int, float]] = None
e: float = 9.8
f: bytes = b'32'
d = 5
d = 9.8
d = None
a = 8.9
但从工程实践的角度来看,这种做法简直就是脱裤子放屁,多此一举。 为变量进行类型标注的目的就是为了防止变量在使用过程中由于缺乏类型检查导致类型变来变去, 这样不就是又回到了之前的状态了么,那做类型标注还有什么意义呢,还不如不做。
d
变量与其他几个变量不同,d
变量初始值赋值为 None
,我们心里很清楚,它的值一定会被改变的,
不然留着它毫无意义, 而一旦改变,就必然导致数据类型发生变化,因此才需要使用 Optional
。
其他变量呢,值改变了,数据类型可以不发生变化,如果类型发生了变化,说明操作就违背了类型标注的初衷。
为容器类型做标注
list
, tuple
, dict
, set
, 为这4个容器类型数据做标注,
要稍微麻烦一点点, 先来看最简单的 set
。
为集合做标注
在使用 set
时,默认只会向集合中添加相同数据类型的值,但要明确一点,
集合可以存储不同类型的数据。
from typing import Optional, Union, Any, Set
s: Set[int] = {1, 2, 3}
from typing import Optional, Union, Any, Set, List, Tuple
s: Set[int] = {1, 2, 3}
l: List[int] = [1, 2, 3]
列表标注的方式与集合是一样的,但都清楚, 列表里存储的数据往往都是类型不相同的,比如下面的列表。
[1, 2, 3, 'a', 'b', True]
[1, 2, 3, 'a', 'b', True]
对这种情况,就需要使用1.3小节所介绍的 Union
。
from typing import Optional, Union, Any, Set, List, Tuple
s: Set[int] = {1, 2, 3}
l: List[Union[int, str, bool]] = [1, 2, 3, 'a', 'b', True]
from typing import Optional, Union, Any, Set, List, Tuple
t: Tuple[int, str, bool] = (3, 'ok', True)
from typing import Optional, Union, Any, Set, List, Tuple, Dict
d: Dict[str, int] = {'ok': 4}
这是最理想的情况,但实际情况往往更复杂,字典的 key
可以有 str
类型,
也可以有 int
类型,当类型不确定的时候,就可以使用 Union
。
from typing import Optional, Union, Any, Set, List, Tuple, Dict
d: Dict[str, int] = {'ok': 4}
d1: Dict[Union[str, int], Union[str, int, float]] = {'ok': 4, 3: 'ok', 4: 3.2}
还有更复杂的情况。
from typing import Optional, Union, Any, Set, List, Tuple, Dict
dic: Dict[str, Union[Tuple[int, int], Dict[int, int]]] = {
'ok': (1, 2),
'dic': {5: 9}
}
字典里的 value
,可以是元组,也可以是字典,字典嵌套了字典,在做类型标注的时候,
也就需要以嵌套的形式进行标注。对于这种复杂的字典,就是简化处理。
from typing import Optional, Union, Any, Set, List, Tuple, Dict
dic: Dict[str, Union[Tuple, Dict]] = {
'ok': (1, 2),
'dic': {5: 9}
}
from typing import Optional, Union, Any, Set, List, Tuple, Dict
l: List = [1, 2, ['2', '3']] # 粗略标注
l2 : List[Union[int, List[str]]] = [1, 2, ['2', '3']] # 详细标注
def add(x: int, y: int) -> int:
return x + y
print(add(2, 5))
7
形参的变量类型,事先是清楚的,因此只需要按照第一节里的讲解对形参进行标注就可以了,
函数的返回值在函数定义时进行标注,在有括号后面紧跟着进行标注,注意需要用到 “ ->
”。
如果返回值的类型可能是 int
,也可能是 None
,该怎么标注呢?其实这种情况完全可以参考对变量的标注。
from typing import Optional
def add(x: Optional[int], y: int) -> Optional[int]:
if not isinstance(x, int):
return None
return x + y
add(3, 4)
add(None, 4)
def add(*args: int) ->int:
sum_value = sum(args)
return sum_value
print(add(1, 2, 3))
6
确定 args
里的元素都是 int
类型,那么直接标注为 int
就可以了,如果还有其他类型,那么就需要使用 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(1, '2', 3.8))
6.8
传入的可变参数可以是 str
, int
, float
中的任意一个,
args
虽然是元组,但是不是按照元组来进行标注,
标注的是对这些参数的期望值,再来看 **kwargs
。
from typing import Any, Union
def add(**kwargs: Union[int, str, float]) -> None:
print(kwargs)
dic = {
'a': 3,
'b': '5',
'c': 9.3
}
add(**dic)
{'a': 3, 'b': '5', 'c': 9.3}
add(a=3, b='5', c=9.3)
{'a': 3, 'b': '5', 'c': 9.3}
from typing import Callable, Any, Union
import time
from functools import wraps
def cost(func: Callable):
@wraps(func)
def warpper(*args: Any, **kwargs: Any):
t1 = time.time()
res = func(*args, **kwargs)
t2 = time.time()
print(func.__name__ + "执行耗时" + str(t2-t1))
return res
return warpper
@cost
def test(sleep_time: Union[float, int]) -> None:
"""
测试装饰器
:param sleep_time:
:return:
"""
time.sleep(sleep_time)
test(1)
test执行耗时1.0000746250152588
class Demo():
pass
d : Demo = Demo()
def test(demo: Demo):
pass
test(d)
from typing import ClassVar
class Demo():
count: ClassVar[int] = 0
d: Demo = Demo()
print(d.count)
d.count = 20 # mypy 检查会报错
0
from typing import Iterator
def my_generator(n: int) -> Iterator:
index = 0
while index < n:
yield index
index += 1
generate = my_generator(5)
from typing import ItemsView, KeysView, ValuesView
def test_1() -> ItemsView:
dic = {'name': 'python'}
return dic.items()
def test_2() ->KeysView:
dic = {'name': 'python'}
return dic.keys()
def test_3() ->ValuesView:
dic = {'name': 'python'}
return dic.values()
from typing import Sequence, List
lst: Sequence[int] = []
name: Sequence = 'python'
tup: Sequence = (1, 2, 4.5)
bstring: Sequence = b'sds'
from typing import List
class Stack():
def __init__(self):
self.data: List[int] = []
但如果这个栈只允许 float
类型的数据入栈,你就只能这样来定义
class Stack():
def __init__(self):
self.data: List[float] = []
这样就有点犯难了,两个存储不同数据类型的栈就需要两个定义,但这两个类的代码是完全一致的, 只是类型标注不同,有没有什么办法,可以用一套代码实现不同类型的标注呢?
这就要用到泛型和 TypeVar
函数
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self):
self.data: List[T] = []
def push(self, item: T):
self.data.append(item)
def pop(self) -> T:
return self.data.pop(-1)
def top(self) -> T:
return self.data[-1]
def size(self) -> int:
return len(self.data)
def is_empty(self) -> bool:
return len(self.data) == 0
stack = Stack[int]()
stack.push(3)
stack.push(5)
print(stack.pop())
5
定义一个泛型,所谓泛型,就是先不明确它的类型, 那么什么时候明确它的类型呢,等到实际调用的时候,比如:
stack = Stack[int]()
在创建 stack
对象时来确定泛型T的数据类型,
如果希望栈只存储 float
类型数据,你就可以这样来写。
stack = Stack[float]()
使用泛型,相当于创建了一个模板,在调用模板前, 来确定泛型的数据类型,一套代码,实现了多套数据类型标注,岂不美哉。