Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Python decorators to check function arguments?

I would like to define some generic decorators to check arguments before calling some functions.

Something like:

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

Side notes:

  1. Type checking is just here to show an example
  2. I'm using Python 2.7 but Python 3.0 whould be interesting too

EDIT 2021: funny that type checking did not go antipythonic in the long run with type hinting and mypy.

like image 963
AsTeR Avatar asked Mar 08 '13 17:03

AsTeR


People also ask

How do you check a function argument in Python?

To extract the number and names of the arguments from a function or function[something] to return ("arg1", "arg2"), we use the inspect module. The given code is written as follows using inspect module to find the parameters inside the functions aMethod and foo.

Can Python decorators take arguments?

Implementing Decorator Arguments You may expect that decorator arguments are somehow passed into the function along with this f argument, but sadly Python always passes the decorated function as a single argument to the decorator function.

How do function decorators work in Python?

Python Decorators Summary Decorators dynamically alter the functionality of a function, method, or class without having to directly use subclasses or change the source code of the function being decorated. Using decorators in Python also ensures that your code is DRY(Don't Repeat Yourself).

Do decorators do anything Python?

A decorator in Python is a function that takes another function as its argument, and returns yet another function . Decorators can be extremely useful as they allow the extension of an existing function, without any modification to the original function source code.


2 Answers

From the Decorators for Functions and Methods:

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

In Python 3 func_code has changed to __code__ and func_name has changed to __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 

Usage:

@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 can be either int or float

like image 198
jfs Avatar answered Oct 02 '22 07:10

jfs


On Python 3.3, you can use function annotations and inspect:

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) 

If there is a validation error, prints:

AssertionError: call to xXy(x=12, y=3): y failed <lambda>) 

If there is not a validation error, prints:

wrapped call to xXy({'y': 3.0, 'x': 12})   returning 36.0 with annotation: "('x times y', 'in X and Y units')" 

You can use a function rather than a lambda to get a name in the assertion failure.

like image 36
dawg Avatar answered Oct 02 '22 07:10

dawg