2013-03-08 4 views
22

Я хотел бы определить некоторые общие декораторы для проверки аргументов перед вызовом некоторых функций.Как использовать декораторы Python для проверки аргументов функции?

Что-то вроде:

@checkArguments(types = ['int', 'float']) 
def myFunction(thisVarIsAnInt, thisVarIsAFloat) 
    ''' Here my code ''' 
    pass 

Боковые ноты:

  1. проверка типов только здесь, чтобы показать пример
  2. Я использую Python 2.7, но Python 3.0 whould интересно тоже
+5

Как примечание, это, как правило, очень плохая идея - это идет вразрез с Python. Проверка типов - это плохо во всех случаях. Также стоит отметить, что для использования аргументов с аргументами может возникнуть больше смысла, если вы находитесь в 3.x. –

+3

@Lattyware: Принудительные аргументы функции и типы возвращаемых данных являются одним из примеров в [оригинальной версии pep для декораторов] (http://www.python.org/dev/peps/pep-0318/) – jfs

+0

В чем вопрос? –

ответ

25

От Decorators for Functions and Methods:

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 

Использование:

@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'> 
+0

Я использую его по некоторому методу, но кажется, что f всегда имеет значение последней определенной функции. Вы случайно не знаете, откуда это взялось? – AsTeR

+1

@AsTeR: создайте пример [минимальный полный код] (http://sscce.org/), который воспроизводит вашу проблему и [опубликует ее как новый вопрос] (http://stackoverflow.com/questions/ask). – jfs

+0

Я буду, мое требование было на всякий случай, что в вашем сознании появилось что-то очевидное. – AsTeR

13

На 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')" 

Вы можете использовать функцию, а не лямбда к получить имя в ошибке утверждения.

+0

Выглядит интересно, но очень сложно понять на первый взгляд. Я посмотрю, когда буду меньше уставать. – AsTeR

+3

Это невероятная реализация. Технически это работает. Но это делает глаза кровоточащими. Для гораздо более читаемой (хотя и немного менее мощной) альтернативы см. [Sweeneyrod] (https://stackoverflow.com/users/2387370/sweeneyrod) краткое ['@ checkargs' decorator] (https://stackoverflow.com/a/19684962/2809027) по [аналогичному вопросу] (https://stackoverflow.com/a/19684962/2809027). –

1

Чтобы применить строковые аргументы для синтаксического анализатора, которые бросают загадочные ошибки при условии, с вводом нестроковой, я писал, что пытается избежать вызовов распределения и функций:

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 
+0

Я закончил использование этого: относительно просто, использует только стандартную библиотеку, и он работает с переменным числом * args и ** kwargs. Только оговорка заключается в том, что 'func_code' был переименован в' __code__' в Python 3, я не знаю, есть ли способ перекрестной версии для этого. – astrojuanlu

0

У меня есть слегка улучшенная версия @jbouwmans гуманного, используя модуль Python декоратора, что делает декоратор полностью прозрачным и сохраняет не только подпись, но и строки документации на месте и может быть самый элегантный способ использования декораторов

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 
5

Как вы наверняка знаете, , это не pythonic, чтобы отклонять аргумент только на основе его типа.
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 

Таким образом, ваша функция питается от типа вы ожидаете Но если параметр может шарлатан как поплавок, принят

@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' 

Я использую этот трюк (с правильным методом преобразования), чтобы иметь дело с vectors.
Многие методы, которые я пишу, ждут класс MyVector, поскольку он имеет множество функциональных возможностей; но иногда вы просто хотите написать

transpose ((2,4)) 
+0

«Как вы, конечно, знаете, это не pythonic, чтобы отклонять аргумент только по его типу». У вас есть ссылка на это? – spinkus

0

Я думаю, что Python 3.5 Ответ на этот вопрос beartype.Как объясняется в этом post, он поставляется с удобными функциями. Ваш код будет выглядеть как этот

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'>