1

I'm searching for a good way to pass default values for arguments to a calling function. Let me explain this by example:

def greet(name, greeting='Hello', punctuation='!'):
    return greeting+' '+name+punctuation

def greet_with_hi(name, punctuation='!'):
   return greet(name, 'Hi', punctuation)

This is an example without use but here's my question: How can I omit the default value for the puncuation argument of greet_with_hi? A default value for this argument is already defined in greet.

Solutions I can think of:

  • let it as it is, cons: changing the default value must be done on every function definition instead of only changing it in one place
  • use None as value for default argument and handle None inside greet, cons: special handling
  • use a global "constant" like DEFAULT_PUNCTUATION='!' and use this as value for the default argument, cons: seems not to be very pythonic

I haven't seen a common pattern in APIs and other code. How would you handle this pattern?

Günther Jena
  • 3,706
  • 3
  • 34
  • 49
  • Could you add a example usage of your functions and explain what output are you expecting? – Jezor Jun 26 '16 at 20:45
  • You could also create a decorator that would orchestrate it the way you like. – dmitryro Jun 26 '16 at 20:47
  • @dmitryro: The example is pretty straight forward, I've sometime situation it's not that clear. Building a decorator flexible enough is a little bit tricky, but maybe worth trying – Günther Jena Jun 26 '16 at 20:51

3 Answers3

3

Why not use None?

def greet(name, greeting='Hello', punctuation='!'):
    return greeting+' '+name+punctuation

def greet_with_hi(name, punctuation=None):
    if punctuation:
        return greet(name, 'Hi', punctuation)
    else:
        return greet(name, 'Hi')

Really any special value would work, but None nicer on the if/else.

If you want "" the null string to be valid punctuation, you have to specify: if punctuation is not None:

Usually, the pattern is the wrapper sets the default for a mandatory parameter in the wrapped function:

def greet(name, greeting, punctuation):
    return greeting+' '+name+punctuation

def greet_with_hi(name, punctuation='!'):
   return greet(name, 'Hi', punctuation)

The idea is the greet() is the general case and the greet_with_hi does some of the work for you.

TemporalWolf
  • 7,727
  • 1
  • 30
  • 50
  • 2
    What if no punctuation is wanted? One couldn't do `greet_with_hi(name, '')` because `''` is Falsey. I would suggest using `is` and `is not` `None` instead of relying on its boolean value. – zondo Jun 26 '16 at 20:56
  • True story. That being said, I guess it depends on whether a greeting is valid without punctuation... and whether a null string should trigger the default punctuation. When we do default string manipulation, we always use the null string as default > `punctuation=''` then just `if punctuation` because it's cleaner, but you're right, he may desire to be able to use the null string. Updated. – TemporalWolf Jun 26 '16 at 21:01
  • While I agree that placing the default argument only on the wrapped function is a good and simple solution, note that this does not necessarily work all the time. A wrapped function should not necessarily know that it’s being wrapped, so just because it is wrapped, removing the default argument might not be a good solution. Also consider what happens once you want to wrap `greet_with_hi` again, then you’re back at the start and have no solution then since you wouldn’t want to make the argument non-default on that function. – poke Jun 26 '16 at 21:18
2

Without modifying your greet function, you can simply remove the argument from the greet_with_hi function and pass nothing for the parameter. That way, the default defined on greet will be used:

def greet_with_hi(name):
    return greet(name, 'Hi')

If you want to be able to specify a different value for punctuation with greet_with_hi but still want to use the default value as specified on greet otherwise, then you could do it like this:

def greet_with_hi(name, punctuation = None):
    if punctuation is None:
        return greet(name, 'Hi')
    else:
        return greet(name, 'Hi', punctuation)

Unfortunately, there is no value that conveys a “use the default” meaning, so you can only trigger the default from greet by not passing an argument to it.

Alternatively, you could also use tuple unpacking here so you only have to specify the arguments once here. This might make more sense when you have multiple default arguments though—at which point you might want to use named arguments too):

def greet_with_hi(name, punctuation = None):
    additionalArgs = []
    if punctuation is not None:
        additionalArgs.append(punctuation)
    return greet(name, 'Hi', *additionalArgs)
poke
  • 369,085
  • 72
  • 557
  • 602
  • I like this for being more robust, but in this case, I don't think the extra strength is necessary. That being said, I think others may find this question who may need the extra capability. You also could add code to "use the default": all valid inputs for punctuation are strings, therefore, you can use anything that is not a string to relay information back to the wrapped function... `def greet_with_hi(name, punctuation = )` and greet can check `if isinstance(punctuation, basestring):` and then handle special cases in the `else`. – TemporalWolf Jun 27 '16 at 17:05
  • @TemporalWolf I don’t see how `greet_with_hi('foo', 5)` conveys more meaning than `gree_with_hi('foo', None)`. The `None` is actually a pretty good and well accepted value for triggering the default case. – poke Jun 27 '16 at 17:24
  • 1
    @TemporalWolf Also, thinking about what’s necessary *“in this case”* is not really useful here when OP describes his example as just *“an example without use”*. So it’s likely that he has less trivial use cases and is just asking for a general pattern. – poke Jun 27 '16 at 17:28
1

Let's simplify your question a little bit. We have two functions, a and b that is using it.

def a(arg = 0):
    return arg

def b(arg = 0):
    return a(arg)

And you want them to share the default argument value.

You could simply create a global variable and use it in both definitions:

__DEFAULT_VALUE = 0

def a(arg = __DEFAULT_VALUE):
    return arg

def b(arg = __DEFAULT_VALUE):
    return a(arg)

Other solution would be to make b's defalt argument to be None and then check against it.

def a(arg = 0):
    return arg

def b(arg = None):
    if arg is None:
        return a()
    return a(arg)

EDIT: don't code like in the solution below. It's late, I'm sleepy, that's where it came from.

Or you can use a more complex and pythoney approach. First, extract default arguments from the "base" a function, then set b's default value as None and check if it's used. If it is, then use a's default value instead.

import inspect

def a(arg=0):
    return arg

def b(arg=None):
    if arg is None:
        signature = inspect.getargspec(a)
        unpacked_signature = dict(zip(signature.args[-len(signature.defaults):], signature.defaults))
        arg = unpacked_signature['arg']

    return a(arg)
Community
  • 1
  • 1
Jezor
  • 3,253
  • 2
  • 19
  • 43
  • Oh, okay, I get it now. – Jezor Jun 26 '16 at 20:48
  • @zondo I think editing is better than deleting -> posting answer again. But I might be wrong (; – Jezor Jun 26 '16 at 21:00
  • Okay, well, looking at the new answers I think I overdid mine a little bit. – Jezor Jun 26 '16 at 21:01
  • 1
    That edit makes me give you a double whammy: undownvote and upvote. – zondo Jun 26 '16 at 21:02
  • 1
    Your inspect solution is a very bad idea. This is reflection at run time which is very expensive, rather complex, and also *very uncessary* since you already trigger the default value by simply not passing it. So if you already do a `arg is None` check, then simply call the function without an argument. – poke Jun 26 '16 at 21:16