tags:

views:

93

answers:

5

Hello,

what is an appropriate solution to limit the number of optional, positional arguments for a function or method? E.g. I'd like to have a function that takes either two or three positional arguments (but not more). I cannot use an optional keyword argument (because the function needs to accept an unlimited number of arbitrarily named keyword arguments). What I've come up with so far is something like this:

def foo(x, y, *args, **kwargs):
    if len(args) == 1:
        # do something
    elif len(args) > 1:
        raise TypeError, "foo expected at most 3 arguments, got %d" % (len(args) + 2)
    else
        # do something else

Does this reasonable or is there a better way?

+1  A: 

Your solution seems reasonable to me.

Ned Batchelder
A: 

It looks good to me.

If you want to abstract that logic out of the body of the function you can put it in a decorator:

def validate_num_args(num_args=None, num_kwargs=None):
    def entangle(f):
        def inner(*args, **kwargs):
            if not num_args is None and len(args) > num_args:
                raise ValueError("Too many arguments, got %s, wanted %s." % (len(args), num_args))
            if not num_kwargs is None and len(kwargs) > num_kwargs:
                raise ValueError("Too many keyword arguments, got %s, wanted %s." % (len(kwargs), num_kwargs))
            return f(*args, **kwargs)
        return inner
    return entangle

@validate_num_args(num_args=3)
def foo(x, y, *args, **kwargs):
    return "do something with these variables:", x, y, args, kwargs

print "Good:\n", foo(1,2,3)
print ""
print "Bad:\n", foo(1,2,3,4)

The output of this code is:

Good:
('do something with these variables:', 1, 2, (3,), {})

Bad:
Traceback (most recent call last):
  File "c:\so.py", line 18, in <module>
    print "Bad:\n", foo(1,2,3,4)
  File "c:\so.py", line 5, in inner
    raise ValueError("Too many arguments, got %s, wanted %s." % (num_args, len(args)))
ValueError: Too many arguments, got 4, wanted 3.
lost-theory
+2  A: 

this works:

>>> def foo(a, b, c=3, **kwargs):
    print(a, b, c, kwargs)


>>> foo(3, 4, 2)
3 4 2 {}
>>> foo(3, 4)
3 4 3 {}
SilentGhost
This also works in Py2k. I think we all missed this simple solution!
Ned Batchelder
indeed, thanks, Ned
SilentGhost
Unfortunately, `def foo(a,b,c=3,d=4,e=5,**kwargs)` allows for one optional positionaly argument, which NielsB doesn't want.
unutbu
This has the problem that it pollutes kwargs, i.e. c cannot be used any more
NielsB
@~unutbu: I beg your pardon?
SilentGhost
@NielsB: can you select unique enough name for your variable? btw, I believe Python doesn't support *unlimited* number of arguments.
SilentGhost
@SilentGhost: I believe NielsB wants foo to accept either 0,2 or 3 positional arguments, but not 1 lone positional argument. Your solution allows sending foo 1 lone positional argument.
unutbu
no, it doesn't. `a`, `b` and `c` are all positional arguments, and it's not possible to pass 0 or 1 of them (of course it's possible it just raises `TypeError`). OP wants to be able to pass 2 or 3 arguments, and this is exactly what my code does.
SilentGhost
why the downvote?
SilentGhost
A: 

You can write a decorator:

class TooManyArgumentsException(Exception):
    pass

def limit_args(n):
    def limit_decorator(f):
        def new_f(*args, **kwargs):
            if len(args) > n:
                raise TooManyArgumentsException("%d args accepted at most, %d args passed" % (n, len(args)))
            return f(*args, **kwargs)
        return new_f
    return limit_decorator

And then use it like this:

>>> @limit_args(5)
... def f(a, b, *args):
...     return a + b + sum(args)
...
>>> f(1, 2, 3)
6
>>> f(1, 2, 3, 4)
10
>>> f(1, 2, 3, 4, 5)
15
>>> f(1, 2, 3, 4, 5, 6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "limit.py", line 8, in new_f
    raise TooManyArgumentsException("%d args accepted at most, %d args passed" % (n, len(args)))
limit.TooManyArgumentsException: 5 args accepted at most, 6 args passed
>>>
Roberto Bonvallet
+1  A: 

One way to find out what is consider "pythonic" is to search for examples in the python source code itself.

find '/usr/lib/python2.6' -name '*.py' -exec egrep 'len\(args\)' {} + | wc
    156     867   12946

If you peruse the results of the above command (without the wc), you'll find plenty of examples using exactly the technique you propose.

unutbu