之前有说过有空的话我会整理一个用于检查参数类型的装饰器的源代码放上来,那么。。过了好一阵子才完成,因为公司里的代码不方便不出来,所以这个模块完全是现敲的,尽管有一些简单的测试,但我无法保证100%没有问题。放上来的目的更多的是分享一下应用装饰器的思路:)
python是不带静态检查的动态语言,有时候需要在调用函数时保证参数合法。检查参数合法性是一个显著的切面场景,各个函数都可能有这个需求。但另一方面,参数合法性是不是应该由调用方来保证比较好也是一个需要结合实际才能回答的问题,总之双方约定好,不要都不检查或者都检查就可以了。下面这个模块用于在函数上使用装饰器进行参数的合法性验证。
你可以直接执行这个模块进行测试,如果完全没有输出则表示通过。你也可以找到几个以_test开头的函数,所有的测试用例都包含在这几个函数中。使用方法参见模块文档和测试用例。
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
|
# -*- coding: UTF-8 -*-
'''
@summary: 验证器
该模块提供了一个装饰器用于验证参数是否合法,使用方法为:
from validator import validParam, nullOk, multiType
@validParam(i=int)
def foo(i):
return i+1
编写验证器:
1. 仅验证类型:
@validParam(type, ...)
例如:
检查第一个位置的参数是否为int类型:
@validParam(int)
检查名为x的参数是否为int类型:
@validParam(x=int)
验证多个参数:
@validParam(int, int)
指定参数名验证:
@validParam(int, s=str)
针对*和**参数编写的验证器将验证这些参数实际包含的每个元素:
@validParam(varargs=int)
def foo(*varargs): pass
@validParam(kws=int)
def foo7(s, **kws): pass
2. 带有条件的验证:
@validParam((type, condition), ...)
其中,condition是一个表达式字符串,使用x引用待验证的对象;
根据bool(表达式的值)判断是否通过验证,若计算表达式时抛出异常,视为失败。
例如:
验证一个10到20之间的整数:
@validParam(i=(int, '10<x<20'))
验证一个长度小于20的字符串:
@validParam(s=(str, 'len(x)<20'))
验证一个年龄小于20的学生:
@validParam(stu=(Student, 'x.age<20'))
另外,如果类型是字符串,condition还可以使用斜杠开头和结尾表示正则表达式匹配。
验证一个由数字组成的字符串:
@validParam(s=(str, '/^\d*$/'))
3. 以上验证方式默认为当值是None时验证失败。如果None是合法的参数,可以使用nullOk()。
nullOk()接受一个验证条件作为参数。
例如:
@validParam(i=nullOk(int))
@validParam(i=nullOk((int, '10<x<20')))
也可以简写为:
@validParam(i=nullOk(int, '10<x<20'))
4. 如果参数有多个合法的类型,可以使用multiType()。
multiType()可接受多个参数,每个参数都是一个验证条件。
例如:
@validParam(s=multiType(int, str))
@validParam(s=multiType((int, 'x>20'), nullOk(str, '/^\d+$/')))
5. 如果有更复杂的验证需求,还可以编写一个函数作为验证函数传入。
这个函数接收待验证的对象作为参数,根据bool(返回值)判断是否通过验证,抛出异常视为失败。
例如:
def validFunction(x):
return isinstance(x, int) and x>0
@validParam(i=validFunction)
def foo(i): pass
这个验证函数等价于:
@validParam(i=(int, 'x>0'))
def foo(i): pass
@author: HUXI
@since: 2011-3-22
@change:
'''
import
inspect
import
re
class
ValidateException(Exception):
pass
def
validParam(
*
varargs,
*
*
keywords):
'''验证参数的装饰器。'''
varargs
=
map
(_toStardardCondition, varargs)
keywords
=
dict
((k, _toStardardCondition(keywords[k]))
for
k
in
keywords)
def
generator(func):
args, varargname, kwname
=
inspect.getargspec(func)[:
3
]
dctValidator
=
_getcallargs(args, varargname, kwname,
varargs, keywords)
def
wrapper(
*
callvarargs,
*
*
callkeywords):
dctCallArgs
=
_getcallargs(args, varargname, kwname,
callvarargs, callkeywords)
k, item
=
None
,
None
try
:
for
k
in
dctValidator:
if
k
=
=
varargname:
for
item
in
dctCallArgs[k]:
assert
dctValidator[k](item)
elif
k
=
=
kwname:
for
item
in
dctCallArgs[k].values():
assert
dctValidator[k](item)
else
:
item
=
dctCallArgs[k]
assert
dctValidator[k](item)
except
:
raise
ValidateException,\
(
'%s() parameter validation fails, param: %s, value: %s(%s)'
%
(func.func_name, k, item, item.__class__.__name__))
return
func(
*
callvarargs,
*
*
callkeywords)
wrapper
=
_wrapps(wrapper, func)
return
wrapper
return
generator
def
_toStardardCondition(condition):
'''将各种格式的检查条件转换为检查函数'''
if
inspect.isclass(condition):
return
lambda
x:
isinstance
(x, condition)
if
isinstance
(condition, (
tuple
,
list
)):
cls
, condition
=
condition[:
2
]
if
condition
is
None
:
return
_toStardardCondition(
cls
)
if
cls
in
(
str
,
unicode
)
and
condition[
0
]
=
=
condition[
-
1
]
=
=
'/'
:
return
lambda
x: (
isinstance
(x,
cls
)
and
re.match(condition[
1
:
-
1
], x)
is
not
None
)
return
lambda
x:
isinstance
(x,
cls
)
and
eval
(condition)
return
condition
def
nullOk(
cls
, condition
=
None
):
'''这个函数指定的检查条件可以接受None值'''
return
lambda
x: x
is
None
or
_toStardardCondition((
cls
, condition))(x)
def
multiType(
*
conditions):
'''这个函数指定的检查条件只需要有一个通过'''
lstValidator
=
map
(_toStardardCondition, conditions)
def
validate(x):
for
v
in
lstValidator:
if
v(x):
return
True
return
validate
def
_getcallargs(args, varargname, kwname, varargs, keywords):
'''获取调用时的各参数名-值的字典'''
dctArgs
=
{}
varargs
=
tuple
(varargs)
keywords
=
dict
(keywords)
argcount
=
len
(args)
varcount
=
len
(varargs)
callvarargs
=
None
if
argcount <
=
varcount:
for
n, argname
in
enumerate
(args):
dctArgs[argname]
=
varargs[n]
callvarargs
=
varargs[
-
(varcount
-
argcount):]
else
:
for
n, var
in
enumerate
(varargs):
dctArgs[args[n]]
=
var
for
argname
in
args[
-
(argcount
-
varcount):]:
if
argname
in
keywords:
dctArgs[argname]
=
keywords.pop(argname)
callvarargs
=
()
if
varargname
is
not
None
:
dctArgs[varargname]
=
callvarargs
if
kwname
is
not
None
:
dctArgs[kwname]
=
keywords
dctArgs.update(keywords)
return
dctArgs
def
_wrapps(wrapper, wrapped):
'''复制元数据'''
for
attr
in
(
'__module__'
,
'__name__'
,
'__doc__'
):
setattr
(wrapper, attr,
getattr
(wrapped, attr))
for
attr
in
(
'__dict__'
,):
getattr
(wrapper, attr).update(
getattr
(wrapped, attr, {}))
return
wrapper
#===============================================================================
# 测试
#===============================================================================
def
_unittest(func,
*
cases):
for
case
in
cases:
_functest(func,
*
case)
def
_functest(func, isCkPass,
*
args,
*
*
kws):
if
isCkPass:
func(
*
args,
*
*
kws)
else
:
try
:
func(
*
args,
*
*
kws)
assert
False
except
ValidateException:
pass
def
_test1_simple():
#检查第一个位置的参数是否为int类型:
@validParam
(
int
)
def
foo1(i):
pass
_unittest(foo1,
(
True
,
1
),
(
False
,
's'
),
(
False
,
None
))
#检查名为x的参数是否为int类型:
@validParam
(x
=
int
)
def
foo2(s, x):
pass
_unittest(foo2,
(
True
,
1
,
2
),
(
False
,
's'
,
's'
))
#验证多个参数:
@validParam
(
int
,
int
)
def
foo3(s, x):
pass
_unittest(foo3,
(
True
,
1
,
2
),
(
False
,
's'
,
2
))
#指定参数名验证:
@validParam
(
int
, s
=
str
)
def
foo4(i, s):
pass
_unittest(foo4,
(
True
,
1
,
'a'
),
(
False
,
's'
,
1
))
#针对*和**参数编写的验证器将验证这些参数包含的每个元素:
@validParam
(varargs
=
int
)
def
foo5(
*
varargs):
pass
_unittest(foo5,
(
True
,
1
,
2
,
3
,
4
,
5
),
(
False
,
'a'
,
1
))
@validParam
(kws
=
int
)
def
foo6(
*
*
kws):
pass
_functest(foo6,
True
, a
=
1
, b
=
2
)
_functest(foo6,
False
, a
=
'a'
, b
=
2
)
@validParam
(kws
=
int
)
def
foo7(s,
*
*
kws):
pass
_functest(foo7,
True
, s
=
'a'
, a
=
1
, b
=
2
)
def
_test2_condition():
#验证一个10到20之间的整数:
@validParam
(i
=
(
int
,
'10<x<20'
))
def
foo1(x, i):
pass
_unittest(foo1,
(
True
,
1
,
11
),
(
False
,
1
,
'a'
),
(
False
,
1
,
1
))
#验证一个长度小于20的字符串:
@validParam
(s
=
(
str
,
'len(x)<20'
))
def
foo2(a, s):
pass
_unittest(foo2,
(
True
,
1
,
'a'
),
(
False
,
1
,
1
),
(
False
,
1
,
'a'
*
20
))
#验证一个年龄小于20的学生:
class
Student(
object
):
def
__init__(
self
, age):
self
.age
=
age
@validParam
(stu
=
(Student,
'x.age<20'
))
def
foo3(stu):
pass
_unittest(foo3,
(
True
, Student(
18
)),
(
False
,
1
),
(
False
, Student(
20
)))
#验证一个由数字组成的字符串:
@validParam
(s
=
(
str
, r
'/^\d*$/'
))
def
foo4(s):
pass
_unittest(foo4,
(
True
,
'1234'
),
(
False
,
1
),
(
False
,
'a1234'
))
def
_test3_nullok():
@validParam
(i
=
nullOk(
int
))
def
foo1(i):
pass
_unittest(foo1,
(
True
,
1
),
(
False
,
'a'
),
(
True
,
None
))
@validParam
(i
=
nullOk(
int
,
'10<x<20'
))
def
foo2(i):
pass
_unittest(foo2,
(
True
,
11
),
(
False
,
'a'
),
(
True
,
None
),
(
False
,
1
))
def
_test4_multitype():
@validParam
(s
=
multiType(
int
,
str
))
def
foo1(s):
pass
_unittest(foo1,
(
True
,
1
),
(
True
,
'a'
),
(
False
,
None
),
(
False
,
1.1
))
@validParam
(s
=
multiType((
int
,
'x>20'
), nullOk(
str
,
'/^\d+$/'
)))
def
foo2(s):
pass
_unittest(foo2,
(
False
,
1
),
(
False
,
'a'
),
(
True
,
None
),
(
False
,
1.1
),
(
True
,
21
),
(
True
,
'21'
))
def
_main():
d
=
globals
()
from
types
import
FunctionType
print
for
f
in
d:
if
f.startswith(
'_test'
):
f
=
d[f]
if
isinstance
(f, FunctionType):
f()
if
__name__
=
=
'__main__'
:
_main()
|