Praxeolitic
Praxeolitic

Reputation: 24039

Wrap function without clobbering default arguments

Is there a way to forward function arguments without hiding the fact that the original call did or did not provide optional arguments?

def func1(a=x):
    # do stuff

def func2(b=y):
    # pass args to func1 without masking func1 defaults
    return func1(?)

A call to func2() should result in func1() being called without arguments or at least with its default arguments, whatever they may be.

The following almost works but fundamentally I don't know if there is a way for func2 to determine if its defaults were invoked on not.

def func2(b=y):
    # this comes close but what if func2(y) is called?
    if b == y:
        return func1()
    else:
        return func1(b)

Upvotes: 2

Views: 723

Answers (5)

Ethan Furman
Ethan Furman

Reputation: 69031

What is your exact use case? func2 should be smart enough to only pass on the appropriate params to func1, and that should rely on the default values of any parameters.

The only time I have ever found it necessary to change how func2 calls func1 is when func1 is a c function with a screwy signature:

def func2(this, that, those=None):
    if those is None:
        return func1(this, that)
    else:
        return func1(this, that, those)

Upvotes: 0

nmclean
nmclean

Reputation: 7724

See this answer:

https://stackoverflow.com/a/2088101/933416

There is no way to get the information you want from the internals. To detect whether defaults were used, you would need to re-implement the internal default argument processing within the function, i.e.:

def func2(*args, **kwargs):
    if len(args) == 0 and "b" not in kwargs:
        b = y
        return func1()
    else:
        return func1(b)

Now from the first check we guarantee that func2() was called as opposed to func2(y) or func2(b=y). In almost every case, the unique object sentinel is good enough to avoid having to truly guarantee how it was called, but it can be done.

But judging from the fact that you immediately return the result of func1, I see no reason why func2 even has default arguments. In the default call (func2()), that y is never used. So why is it there? Why don't you just use define func2(*a, **k) and pass them directly to func1?

Upvotes: 1

Blckknght
Blckknght

Reputation: 104712

I suspect the right way to do this is to have your func2 function use a sentinel value as its default argument, so you can recognize it easily. If you get that sentinel, you can set up the arguments you'll pass on to func1 however you want (e.g. not passing any argument). You can use argument unpacking to handle passing a variable number of arguments (such as 0-1).

A common sentinel is None, though if that could be a meaningful value for a caller to pass, you may want to use something else (an instance of object is a common choice). Here's an example:

def func1(a="default value"): # lets assume we don't know what this default is
    # do stuff with a

# later, perhaps in a different module

_sentinel = object()    # our sentinel object

def func2(b=_sentinel):
    if b is _sentinel:  # test for the sentinel
        b = "some useful value"
        a_args = ()     # arguments to func1 is an empty tuple
    else:
        a_args = (b,)   # pack b into a 1-tuple

    # do stuff with b perhaps

    func1(*a_args)      # call func1 with appropriate arguments (either b or nothing)

Note that this design is relatively silly. Most of the time you'll either call func1 with an argument in all cases, or you'll call it without an argument in all cases. You rarely need to conditionally pass an argument like this.

Upvotes: 1

Veedrac
Veedrac

Reputation: 60137

Argument forwarding should be done with variadic arguments:

def func2(*args, **kwargs):
    func1(*args, **kwargs)

Everything will just work, although introspection can suffer a bit.


If you need to sometimes not pass on an argument, you can remove an argument whenever:

del kwargs["name"]

An example:

def print_wrapper(*args, extrabig=False, **kwargs):
    if extrabig:
        args = [arg*2 for arg in args]
        kwargs["sep"] = kwargs.get("sep", " ") * 2

    print(*args, **kwargs)

print_wrapper(2, 4, 8, end="!!!\n")
#>>> 2 4 8!!!

print_wrapper(2, 4, 8, sep=", ", end="!!!\n")
#>>> 2, 4, 8!!!

print_wrapper(2, 4, 8, extrabig=True, end="!!!\n")
#>>> 4  8  16!!!

If you really don't want to do this (although you'd be wrong), you can use object to generate a unique sentinel.

# Bad! Won't let you print None
def optionally_print_one_thing(thing=None):
    if thing is not None:
        print(thing)

# Better
_no_argument = object()
def optionally_print_one_thing(thing=_no_argument):
    if thing is not _no_argument:
        print(thing)

Upvotes: 0

Mark Ransom
Mark Ransom

Reputation: 308130

The usual way of determining if a parameter is left off is to use None as the default. It's unlikely that you'll be calling a function with None so it's a useful marker.

def func2(b=None):
    if b is None:
        return func1()
    else:
        return func1(b)

Upvotes: 1

Related Questions