Pulpcode

捕获,搅碎,拼接,吞咽

0%

关于类型检查

Introduction

python虽然是强类型语言(对此说法有异议,自行google),但同时又是动态语言。其灵活性带来的缺点就是不能像静态语言那样在编译期间通过静态类型检查”过滤”掉一部分错误。
我们目前的项目中,有两个地方需要进行类型检查。1是作为提供接口的server,要对请求json的字段缺失,参数类型和值范围进行校验。2是对某个函数的参数和返回值进行校验。这里把自己在项目中的一些处理经验拿出来分享,如有错误和不足,还望指正。

请求参数校验

请求参数校验会遇到这样几个问题:
为了重用和方便修改,你需要将它们“格式化”
你需要将错误原因返回给调用者或打印到日志,还不能写太多麻烦的代码
我的解决办法是使用“表驱动”的方法,将检查过程制作成可配置的。
比如下面是一段请求服务器的json字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
request = {
"app_key": "fkerafi",
"access_token": "aeraeriaufjaisjfdaisdfaasdf",
"client_id": "03928172",
"client_time": 532312344,
"payload": {
"merSeqId": "u637323283",
"product_id": "00001",
"channel": "001",
"account_id": "3982738273",
"amt": 550,
"account_realname": "王思聪",
"account_bank_num": "637",
}
}

字段缺失校验:

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
def fbk(d, keys):
'''
fetch by keys
对于一个深层字典,通过多个key进行获取
:param d:
:param keys:
:return:
'''
m = d
for k in keys:
m = m[k]
return m

tables = [
"app_key",
"access_token",
"client_id",
"client_time",
"payload,merSeqId",
"payload,product_id",
"payload,channel",
"payload,account_id",
"payload,amt",
"payload,account_realname",
"payload,account_bank_num"]

def field_lack_check(request, table):
for i in table:
path = i.split(',')
try:
fbk(request, path)
except KeyError:
return False, "lack of field: {}".format(i)
else:
return True, None

字段类型校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 table = {
"app_key": (str, unicode),
"access_token": (str, unicode),
"client_id": (str, unicode),
"client_time": int,
"payload,merSeqId": (str, unicode),
"payload,product_id": (str, unicode),
"payload,channel": (str, unicode),
"payload,account_id": int,
"payload,amt": int,
"payload,account_realname": (str, unicode),
"payload,account_bank_num": (str, unicode)
}

def field_type_check(request, table):
for k, t in table.items():
path = k.split(',')
v = fbk(request, path)
if not isinstance(v, t):
return False, "Type Error: {} expect:{} but:{}".format(k, table[k], type(v))
else:
return True, None

字段范围校验:

字段范围校验的代码与上面类似,实现一些具体的校验规则,将它们配置成表就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
# o: open interval [
# c: close interval (
def str_range_cc(s, b, e):
'''
[]
'''
if len(s) < b:
return False, "must be longer or equal {}".format(b)
elif len(s) > e:
return False, "must be shorter or equal {}".format(e)
else:
return True, None

def str_range_oo(s, b, e):
'''
()
'''
if len(s) <= b:
return False, "must be longer than {}".format(b)
elif len(s) >= e:
return False, "must be shorter than {}".format(e)
else:
return True, None

说明:
你会发现,上面的例子中,校验错误时,会将错误原因返回,方便传递给请求者或用于日志打印。
而且函数部分是可以重用的,你只需要根据不同的请求接口,配置那张表就行了。

函数类型检查

你可以在自己的每个函数中进行类型检查,然后在失败的时候抛出异常,只要你不觉得麻烦就行了。
或者你可以试着使用装饰器这种神奇的东西,直接上代码了:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
'''
One of three degrees of enforcement may be specified by passing
the 'debug' keyword argument to the decorator:
0 -- NONE: No type-checking. Decorators disabled.
#!python
-- MEDIUM: Print warning message to stderr. (Default)
2 -- STRONG: Raise TypeError with message.
If 'debug' is not passed to the decorator, the default level is used.

Example usage:
>>> NONE, MEDIUM, STRONG = 0, 1, 2
>>>
>>> @accepts(int, int, int)
... @returns(float)
... def average(x, y, z):
... return (x + y + z) / 2
...
>>> average(5.5, 10, 15.0)
TypeWarning: 'average' method accepts (int, int, int), but was given
(float, int, float)
15.25
>>> average(5, 10, 15)
TypeWarning: 'average' method returns (float), but result is (int)
15

Needed to cast params as floats in function def (or simply divide by 2.0).

>>> TYPE_CHECK = STRONG
>>> @accepts(int, debug=TYPE_CHECK)
... @returns(int, debug=TYPE_CHECK)
... def fib(n):
... if n in (0, 1): return n
... return fib(n-1) + fib(n-2)
...
>>> fib(5.3)
Traceback (most recent call last):
...
TypeError: 'fib' method accepts (int), but was given (float)

'''
import sys

def accepts(*types, **kw):
'''Function decorator. Checks decorated function's arguments are
of the expected types.

Parameters:
types -- The expected types of the inputs to the decorated function.
Must specify type for each parameter.
kw -- Optional specification of 'debug' level (this is the only valid
keyword argument, no other should be given).
debug = ( 0 | 1 | 2 )

'''
if not kw:
# default level: MEDIUM
debug = 1
else:
debug = kw['debug']
try:
def decorator(f):
def newf(*args):
if debug is 0:
return f(*args)
assert len(args) == len(types)
argtypes = tuple(map(type, args))
if argtypes != types:
msg = info(f.__name__, types, argtypes, 0)
if debug is 1:
print >> sys.stderr, 'TypeWarning: ', msg
elif debug is 2:
raise TypeError, msg
return f(*args)
newf.__name__ = f.__name__
return newf
return decorator
except KeyError, key:
raise KeyError, key + "is not a valid keyword argument"
except TypeError, msg:
raise TypeError, msg


def returns(ret_type, **kw):
'''Function decorator. Checks decorated function's return value
is of the expected type.

Parameters:
ret_type -- The expected type of the decorated function's return value.
Must specify type for each parameter.
kw -- Optional specification of 'debug' level (this is the only valid
keyword argument, no other should be given).
debug=(0 | 1 | 2)
'''
try:
if not kw:
# default level: MEDIUM
debug = 1
else:
debug = kw['debug']
def decorator(f):
def newf(*args):
result = f(*args)
if debug is 0:
return result
res_type = type(result)
if res_type != ret_type:
msg = info(f.__name__, (ret_type,), (res_type,), 1)
if debug is 1:
print >> sys.stderr, 'TypeWarning: ', msg
elif debug is 2:
raise TypeError, msg
return result
newf.__name__ = f.__name__
return newf
return decorator
except KeyError, key:
raise KeyError, key + "is not a valid keyword argument"
except TypeError, msg:
raise TypeError, msg

def info(fname, expected, actual, flag):
'''Convenience function returns nicely formatted error/warning msg.'''
format = lambda types: ', '.join([str(t).split("'")[1] for t in types])
expected, actual = format(expected), format(actual)
msg = "'{}' method ".format( fname )\
+ ("accepts", "returns")[flag] + " ({}), but ".format(expected)\
+ ("was given", "result is")[flag] + " ({})".format(actual)
return msg