0%

实现python的强类型检查

通过描述符、描述符MXIN、参数注解等方法来实现强制类型转换

类型检查

python本身是弱类型的语言,虽然使用方便,但是也会带来一些问题,比如在函数中传入了错误的类型,或者在使用某个变量时,忘记了它的类型,这些问题都会导致程序出错,而且很难发现。

有时候我们需要对传入的参数进行类型检查,以减少错误,这里介绍几种实现强类型检查的方法。

使用描述符

限制类型时首先想到的应该就是描述符。描述符本身也很简单,用的也比较多,这里就不再赘述了,下面是一个简单的描述符类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class DecInt():
def __init__(self,key):
self.key=""

def __set__(self, instance, value):
# 设置时判断类型
if not isinstance(value, int):
raise TypeError("not int")
else:
instance.__dict__[self.key]=value

def __get__(self, instance, owner):
return instance.__dict__[self.key]

def __delete__(self, instance):
instance.__dict__.pop(self.key)

class X:
nums=DecInt("nums")

def __init__(self,nums):
self.nums = nums

a = X(1) # 正常
b = X("1") # 报错

使用描述符与MXIN方法

描述符类可以玩的更花一点,就是使用继承关系,实现MXIN。不过这样理解起来有点复杂,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 先创建一个描述符类
class Descriptor:
def __init__(self, name = None, **opts):
self.name = name
for key, value in opts.items():
setattr(self, key, value)

def __set__(self, instance, value):
instance.__dict__[self.name] = value

# 下面创建出用来类型检查的的二级基类,也不应实例化,本质还是描述符
# 这些类不应该被实例化
class Typed(Descriptor):
# 在赋值的时候会检查类型是否相符
expected_type = type(None)

def __set__(self, instance, value):
if not isinstance(value, expected_type):
raise TypeError("expected " + str(self.expected_type))
super().__set__(instance, value)

class Unsigned(Descriptor):
# 继承自描述符类,改写set方法,用来赋值时检查值是否大于0
def __set__(self, instance, value):
if value < 0:
raise ValueError("value must > 0")
super().__set__(instance, value)

class MaxSized(Descriptor):
# 继承自描述符类,检查赋值的字符串长度是否超出限制
def __init__(self, name = None, **opt):
if 'size' not in opt:
raise TypeError("missing size option")
super.__init__(name, **opt)

def __set__(self, instance, value):
if len(value) >= self.size:
raise ValueError("> size")
super.__set__(instance, value)

# 下面基于描述符类来创建出需要用到检测类型的类
class Integer(Typed):
expected_type = int

class UnsignedInteger(Integer, Unsigned):
...

class String(Typed):
expected_type = str

class SizedString(String, MaxSized):
...

# 然后就可以使用这些类来进行类型约束
class Stock:
name = SizedString('name', size = 8)
shares = UnsignedInteger('shares')

def __init__(self, *args, **kargs):
...

下面来解释一下流程:

  • 首先Descriptor只是一个简单的描述符基类,且只有set方法
  • Typed, Unsigned, MaxSized也是描述符,这些二级描述符继承自Descriptor,并重写了其set方法,分别检测赋值时类型、长度。
  • Integer, UnsignedInteger, String, SizedString是继承上述描述符类,并基于上述限制来进行组合。由于默认没有构造函数,会使用基类的构造函数。同样也是描述符。
  • 直接使用的描述符是Integer, UnsignedInteger, String, SizedString

同样,也可以使用装饰器的方式来完成,且速度更快!

当然,上面这个例子不使用MXIN也可以,只使用描述符也能达成相同效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

class DecType():
def __init__(self, key, type_):
self.key=""
self.type_ = type_

def __set__(self, instance, value):
# 设置时判断类型
if not isinstance(value, self.type_):
raise TypeError("not type {}".format(self.type_))
else:
instance.__dict__[self.key]=value

def __get__(self, instance, owner):
return instance.__dict__[self.key]

def __delete__(self, instance):
instance.__dict__.pop(self.key)

class X:
nums=DecType("nums", int)
name=DecType("names", str)

def __init__(self, nums, name):
self.nums = nums
self.name = name

a = X(1, "1") # 正常
b = X("1", 1) # 报错

通过函数注解来限制函数传入参数

上面的描述符都是限制类中的数据类型,但是检查函数的类型往往也是很常见的。

在某一次更新之后,python引入了注解语法,但是这些注解并不会限制参数类型,只是用于提示而已

1
2
3
4

def f(x:int, y:float):
...

但是我们可以使用一个装饰器来利用注解信息来检查参数

1
2
3
4
5
6
7
8
9
10
11
12
13

def check(func):
def inner(*args):
check_list = func.__annotations__
for arg, type_ in zip(args,check_list.values()):
if not isinstance(arg, type_):
raise TypeError(f"{arg} not {type_}")
return inner

@check
def f(x:int, y:float):
...

上述代码主要是通过__annotations__来获取注解:

1
2
3
4
5
6
7

def f(x:int, y:float):
...

print(f.__annotations__)
# {'x': <class 'int'>, 'y': <class 'float'>, 'return': <class 'int'>}

值得一提的是,FastAPI也是使用类似的方式来检查参数类型。

总结

  • 检查类中的参数类型可以使用描述符、描述符MXIN的方式
  • 检查函数传入的参数可以通过__annotations__方法+装饰器来实现