6

Is there a way to get the number of actual parameters passed to a function

def foo(a, optional=42):
    if ????
        print "1 arg"
    else:
        print "2 args"

foo(11)      # should print 1 arg
foo(22, 42)  # should print 2 args

without changing its signature to accept *args?

georg
  • 211,518
  • 52
  • 313
  • 390
  • 2
    Is changing the default value for `optional` acceptable? Otherwise, you are out of options (you could inspect the stack but that is unreliable at best). – Martijn Pieters Oct 25 '13 at 12:33
  • Whats wrong with `def foo(*args): return len(args)`? By actual, do you simply mean that in the case provided to test whether optional has been provided as an argument and not set to the default value? – Farmer Joe Oct 25 '13 at 12:39

3 Answers3

6

You could change the default value to a sentinel:

_sentinel = object()

def foo(a, optional=_sentinel):
    if optional is _sentinel:
        optional = 42
        print "1 arg"
    else:
        print "2 args"

or by accessing it directly in the func_defaults tuple:

def foo(a, optional=object()):
    if optional is foo.func_defaults[0]:
        optional = 42
        print "1 arg"
    else:
        print "2 args"

but don't actually use that; that'll just serve to confuse those not familiar with standard function object attributes.

Yes, the _sentinel object is introspectable and can be obtained still by a determined developer, but then again that same developer could just monkeypatch your function as well. :-)

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Is that possible without a global variable? – georg Oct 25 '13 at 12:42
  • 1
    Sure, as long as `foo` can reach both at define time (when the function is created) and when the function is run.. You could move it to a local in the function, then delete the global, etc. Too much effort, as that is *still* reachable by everyone else too with introspection (`foo.func_defaults` lists it, for example). – Martijn Pieters Oct 25 '13 at 12:43
  • So, `if optional is foo.func_defaults[0]:` inside the function, then after the function definition add a `del _sentinel`. But again, that's not going to make it any harder on a determined hacker, only harder for you to read and maintain. – Martijn Pieters Oct 25 '13 at 12:45
  • @MartijnPieters You wouldn't need to declare sentinel as global then. `def foo(a, optional=object()): if optional is foo.func_defaults[0] …` should work the same. – millimoose Oct 25 '13 at 12:46
  • @millimoose: good point; the global only acts as a convenient reference. – Martijn Pieters Oct 25 '13 at 12:47
  • Ok, what I learned here is that there's no easy way, and YAGNI, because the only real use of this is `None` as a meaningful argument - which should never be the case. – georg Oct 25 '13 at 13:23
6

You can use a decorator to do that. From the Preserving signatures of decorated functions question we know how to do this correctly.

import decorator


@decorator.decorator
def count_args(f):
    def new(*args, **kwargs):
        kwargs['nargs'] = len(args)+len(kwargs)
        return f(*args, **kwargs)
    return new

@count_args
def foo(a, optional=42, nargs=1):
    print nargs

foo(1) # 1
foo(1, 4) # 2
foo(1, optional=4) # 2

Update:

I have just added keyword argument for the number of args, that we passed to the function. As you can see, by default it is 1, which is True. This looks like a hack, but it works.

Community
  • 1
  • 1
Alexander Zhukov
  • 4,357
  • 1
  • 20
  • 31
  • Sure, but how to access it in the function? – georg Oct 25 '13 at 12:42
  • this has the side effect of changing the signature though, doesn't it? i mean, if you had `def foo(a=1, optional=42)`, and used this decorator, i think you can no longer do `foo(optional=2)`... – Corley Brigman Oct 25 '13 at 12:42
  • 1
    Your new version will still swallow all keyword arguments, and has another bug too. It'd be better to add `nargs` to `kwargs` then call `f(*args, **kwargs)`. – millimoose Oct 25 '13 at 12:53
0

I'd use a decorator:

def wrapper(func):
    def wrapped(*args):
        if len(args) == 2:
            print "2 arg"
        else:
            print "1 arg"
    return wrapped

@wrapper
def foo(a,optional=None):
    pass

foo(11)
#1 arg

foo(22, 42)
#2 arg
K DawG
  • 13,287
  • 9
  • 35
  • 66