Как использовать декораторы Python для проверки аргументов функции?
Я хотел бы определить некоторые общие декораторы для проверки аргументов перед вызовом некоторых функций.
Что-то вроде:
@checkArguments(types = ['int', 'float'])
def myFunction(thisVarIsAnInt, thisVarIsAFloat)
''' Here my code '''
pass
Боковые заметки:
- Проверка типов только здесь, чтобы показать пример
- Я использую Python 2.7, но Python 3.0 тоже интересен.
Ответы
Ответ 1
Из Декораторов для функций и методов:
Python 2
def accepts(*types):
def check_accepts(f):
assert len(types) == f.func_code.co_argcount
def new_f(*args, **kwds):
for (a, t) in zip(args, types):
assert isinstance(a, t), \
"arg %r does not match %s" % (a,t)
return f(*args, **kwds)
new_f.func_name = f.func_name
return new_f
return check_accepts
Python 3
В Python 3 func_code
изменился на __code__
, а func_name
изменился на __name__
.
def accepts(*types):
def check_accepts(f):
assert len(types) == f.__code__.co_argcount
def new_f(*args, **kwds):
for (a, t) in zip(args, types):
assert isinstance(a, t), \
"arg %r does not match %s" % (a,t)
return f(*args, **kwds)
new_f.__name__ = f.__name__
return new_f
return check_accepts
Использование:
@accepts(int, (int,float))
def func(arg1, arg2):
return arg1 * arg2
func(3, 2) # -> 6
func('3', 2) # -> AssertionError: arg '3' does not match <type 'int'>
arg2
может быть либо int
, либо float
Ответ 2
В Python 3.3 вы можете использовать аннотации функций и проверить:
import inspect
def validate(f):
def wrapper(*args):
fname = f.__name__
fsig = inspect.signature(f)
vars = ', '.join('{}={}'.format(*pair) for pair in zip(fsig.parameters, args))
params={k:v for k,v in zip(fsig.parameters, args)}
print('wrapped call to {}({})'.format(fname, params))
for k, v in fsig.parameters.items():
p=params[k]
msg='call to {}({}): {} failed {})'.format(fname, vars, k, v.annotation.__name__)
assert v.annotation(params[k]), msg
ret = f(*args)
print(' returning {} with annotation: "{}"'.format(ret, fsig.return_annotation))
return ret
return wrapper
@validate
def xXy(x: lambda _x: 10<_x<100, y: lambda _y: isinstance(_y,float)) -> ('x times y','in X and Y units'):
return x*y
xy = xXy(10,3)
print(xy)
Если есть ошибка проверки, печатает:
AssertionError: call to xXy(x=12, y=3): y failed <lambda>)
Если ошибка проверки не выполняется, печатает:
wrapped call to xXy({'y': 3.0, 'x': 12})
returning 36.0 with annotation: "('x times y', 'in X and Y units')"
Вы можете использовать функцию, а не лямбду, чтобы получить имя в ошибке утверждения.
Ответ 3
Как вы, конечно, знаете, это не pythonic, чтобы отклонить аргумент только на основе его типа.
Питонический подход скорее "попытается разобраться с ним в первую очередь"
Поэтому я предпочел бы сделать декоратор для преобразования аргументов
def enforce(*types):
def decorator(f):
def new_f(*args, **kwds):
#we need to convert args into something mutable
newargs = []
for (a, t) in zip(args, types):
newargs.append( t(a)) #feel free to have more elaborated convertion
return f(*newargs, **kwds)
return new_f
return decorator
Таким образом, ваша функция загружается с типом, который вы ожидаете
Но если параметр может оцепиться как float, он принимается
@enforce(int, float)
def func(arg1, arg2):
return arg1 * arg2
print (func(3, 2)) # -> 6.0
print (func('3', 2)) # -> 6.0
print (func('three', 2)) # -> ValueError: invalid literal for int() with base 10: 'three'
Я использую этот трюк (с правильным методом преобразования), чтобы иметь дело с векторами.
Многие методы, которые я пишу, предполагают класс MyVector, поскольку он имеет множество функциональных возможностей; но иногда вы просто хотите написать
transpose ((2,4))
Ответ 4
Чтобы принудительно вводить строковые аргументы в синтаксический анализатор, который генерирует критические ошибки при предоставлении нестрокового ввода, я написал следующее, которое пытается избежать вызовов распределения и функций:
from functools import wraps
def argtype(**decls):
"""Decorator to check argument types.
Usage:
@argtype(name=str, text=str)
def parse_rule(name, text): ...
"""
def decorator(func):
code = func.func_code
fname = func.func_name
names = code.co_varnames[:code.co_argcount]
@wraps(func)
def decorated(*args,**kwargs):
for argname, argtype in decls.iteritems():
try:
argval = args[names.index(argname)]
except ValueError:
argval = kwargs.get(argname)
if argval is None:
raise TypeError("%s(...): arg '%s' is null"
% (fname, argname))
if not isinstance(argval, argtype):
raise TypeError("%s(...): arg '%s': type is %s, must be %s"
% (fname, argname, type(argval), argtype))
return func(*args,**kwargs)
return decorated
return decorator
Ответ 5
Все эти сообщения кажутся устаревшими - теперь pint предоставляет эту встроенную функциональность. Смотрите здесь. Скопировано здесь для потомков:
Проверка размерности Когда вы хотите, чтобы количества пинты использовались в качестве входных данных для ваших функций, pint предоставляет оболочку, чтобы гарантировать, что единицы имеют правильный тип - или, точнее, они соответствуют ожидаемой размерности физической величины.
Подобно wraps(), вы можете передать None, чтобы пропустить проверку некоторых параметров, но тип возвращаемого параметра не проверяется.
>>> mypp = ureg.check('[length]')(pendulum_period)
В формате декоратора:
>>> @ureg.check('[length]')
... def pendulum_period(length):
... return 2*math.pi*math.sqrt(length/G)
Ответ 6
У меня немного улучшенная версия @jbouwmans sollution, использующая модуль декоратора python, который делает декоратор полностью прозрачным и сохраняет не только подпись, но и docstrings на месте и может быть самым элегантным способом использования декораторов
from decorator import decorator
def check_args(**decls):
"""Decorator to check argument types.
Usage:
@check_args(name=str, text=str)
def parse_rule(name, text): ...
"""
@decorator
def wrapper(func, *args, **kwargs):
code = func.func_code
fname = func.func_name
names = code.co_varnames[:code.co_argcount]
for argname, argtype in decls.iteritems():
try:
argval = args[names.index(argname)]
except IndexError:
argval = kwargs.get(argname)
if argval is None:
raise TypeError("%s(...): arg '%s' is null"
% (fname, argname))
if not isinstance(argval, argtype):
raise TypeError("%s(...): arg '%s': type is %s, must be %s"
% (fname, argname, type(argval), argtype))
return func(*args, **kwargs)
return wrapper
Ответ 7
Я думаю, что ответ Python 3.5 на этот вопрос beartype. Как объясняется в этом сообщении, он поставляется с удобными функциями. Тогда ваш код будет выглядеть следующим образом:
from beartype import beartype
@beartype
def sprint(s: str) -> None:
print(s)
и приводит к
>>> sprint("s")
s
>>> sprint(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 13, in func_beartyped
TypeError: sprint() parameter s=3 not of <class 'str'>
Ответ 8
def decorator(function):
def validation(*args):
if type(args[0]) == int and \
type(args[1]) == float:
return function(*args)
else:
print('Not valid !')
return validation