Is there a way to set a default parameter equal to another parameter value?

Question:

For example, I have a basic method that will return a list of permutations.

import itertools
def perms(elements, set_length=elements):
    data=[]
    for x in range(elements):
        data.append(x+1)
    return list(itertools.permutations(data, set_length))

Now I understand, that in its current state this code won’t run because the second elements isn’t defined, but is there and elegant way to accomplish what I’m trying to do here? If that’s still not clear, I want to make the default setLength value equal to the first argument passed in. Thanks.

Asked By: CopOnTheRun

||

Answers:

Because of the way Python handles bindings and default parameters…

The standard way is:

def perms(elements, setLength=None):
    if setLength is None:
        setLength = elements

And another option is:

def perms(elements, **kwargs):
    setLength = kwargs.pop('setLength', elements)

Although this requires you to explicitly use perms(elements, setLength='something else') if you don’t want a default…

Answered By: Jon Clements

No, function keyword parameter defaults are determined when the function is defined, not when the function is executed.

Set the default to None and detect that:

def perms(elements, setLength=None):
    if setLength is None:
        setLength = elements

If you need to be able to specify None as a argument, use a different sentinel value:

_sentinel = object()

def perms(elements, setLength=_sentinel):
    if setLength is _sentinel:
        setLength = elements

Now callers can set setLength to None and it won’t be seen as the default.

Answered By: Martijn Pieters

You should do something like :

def perms(elements,setLength=None):
    if setLength is None:
        setLength = elements
Answered By: SylvainD

Answer 1:

The solution from above looks like this:

def cast_to_string_concat(a, b, c=None):
  c = a if c is None else c

  return str(a) + str(b) + str(c)

While this approach will solve a myriad of potential problems, (and maybe yours)! I wanted to write a function where a possible input for variable "c" is indeed the singleton None, so I had to do more digging.

To explain that further, calling the function with the following variables:

A='A'
B='B'
my_var = None

Yields:

cast_to_string_concat(A, B, my_var):
>>>'ABA'

Whereas the user might expect that since they called the function with three variables, then it should print the three variables, like this:

cast_to_string_concat(A, B, my_var):
>>> 'ABNone' # simulated and expected outcome

So, this implementation ignores the third variable, even when it was declared, so this means the function no longer has the ability to determine whether or not variable "c" was defined.

So, for my use case, a default value of None would not quite do the trick.

For the answers that suggest this solution, read these:


But, if that doesn’t work for you, then maybe keep reading!


A comment in the first link above mentions using a _sentinel defined by object().

So this solution removes the use of a None, and replaces it with the object() through using the implied private sentinel.


Answer 2:

_sentinel = object()
def cast_to_string_concat(a, b, c=_sentinel):
  c = a if c == _sentinel else c

  return str(a) + str(b) + str(c)
A='A'
B='B'
C='C'

cast_to_string_append(A,B,C)
>>> 'ABC'

cast_to_string_concat(A,B)
>>> 'ABA'

So this is pretty awesome! It correctly handles the above edge case! See for yourself:


A='A'
B='B'
C = None

cast_to_string_concat(A, B, C)
>>> 'ABNone'

So, we’re done, right? Is there any plausible way that this might not work? Hmm… probably not! But I did say this was a three-part answer, so onward! 😉


For the sake of completeness, let’s imagine our program operates in a space where every possible scenario is indeed possible. (This may not be a warranted assumption, but I imagine that one could derive the value of _sentinel with enough information about the computer’s architecture and the implementation of the choice of the object. So, if you are willing, let us assume that is indeed possible, and let’s imagine we decide to test that hypothesis referencing _sentinel as defined above.


_sentinel = object()
def cast_to_string_concat(a, b, c=_sentinel):
  c = a if c == _sentinel else c

  return str(a) + str(b) + str(c)
A='A'
B='B'
S = _sentinel

cast_to_string_append(A,B,S)
>>> 'ABA'

Wait a minute! I entered three arguments, so I should see the string concatenation of the three of them together!


*queue entering the land of unforeseen consequences*

I mean, not actually. A response of: "That’s negligible edge case territory!!" or its ilk is perfectly warranted.

And that sentiment is right! For this case (and probably most cases) this is really not worth worrying about!

But if it is worth worrying about, or if you just want the mathematical satisfaction of eliminating all edge cases you’re aware of … onward!

Exercise left to reader:

Deviating from this technique, you can directly assert c=object(), however, in honesty, I haven’t gotten that way to work for me. My investigation shows c == object() is False, and str(c) == str(object()) is also False, and that’s why I’m using the implementation from Martin Pieters.


Okay, after that long exercise, we’re back!

Recall the goal is to write a function that could potentially have n inputs, and only when one variable is not provided – then you will copy another variable in position i.

Instead of defining the variable by default, what if we change the approach to allow an arbitrary number of variables?

So if you’re looking for a solution that does not compromise on potential inputs, where a valid input could be either None, object(), or _sentinel … then (and only then), at this point, I’m thinking my solution will be helpful. The inspiration for the technique came from the second part of Jon Clements’ answer.


Answer 3:

My solution to this problem is to change the naming of this function, and wrap this function with a a function of the previous naming convention, but instead of using variables, we use *args. You then define the original function within the local scope (with the new name), and only allow the few possibilities you desire.

In steps:

  1. Rename function to something similar
  2. Remove the default setup for your optional parameter
  3. Begin to create a new function just above and tab the original function in.
 def cast_to_string_concat(*args):
  1. Determine the the arity of your function – (I found that word in my search… that is the number of the parameters passed into a given function)
  2. Utilize a case statement inside that determines if you entered a valid number of variables, and adjust accordingly!
def cast_to_string_append(*args):

    def string_append(a, b, c):
        # this is the original function, it is only called within the wrapper
        return str(a) + str(b) + str(c)

    if len(args) == 2:
        # if two arguments, then set the third to be the first
        return string_append(*args, args[0])

    elif len(args) == 3:
        # if three arguments, then call the function as written
        return string_append(*args)

    else:
        raise Exception(f'Function: cast_to_string_append() accepts two or three arguments, and you entered {len(args)}.')

# instantiation

A='A'
B='B'
C='C'
D='D'

_sentinel = object()
S = _sentinel

N = None
""" Answer 3 Testing """

# two variables

cast_to_string_append(A,B)

>>> 'ABA'


# three variables

cast_to_string_append(A,B,C)

>>> 'ABC'


# three variables, one is _sentinel

cast_to_string_append(A,B,S)

>>>'AB<object object at 0x10c56f560>'


# three variables, one is None

cast_to_string_append(A,B,N)

>>>'ABNone'


# one variable

cast_to_string_append(A)

>>>Traceback (most recent call last):
>>>  File "<input>", line 1, in <module>
>>>  File "<input>", line 13, in cast_to_string_append
>>>Exception: Function: cast_to_string_append() accepts two or three arguments, and you entered 1.

# four variables

cast_to_string_append(A,B,C,D)

>>>Traceback (most recent call last):
>>>  File "<input>", line 1, in <module>
>>>  File "<input>", line 13, in cast_to_string_append
>>>Exception: Function: cast_to_string_append() accepts two or three arguments, and you entered 4.


# ten variables

cast_to_string_append(0,1,2,3,4,5,6,7,8,9)

>>>Traceback (most recent call last):
>>>  File "<input>", line 1, in <module>
>>>  File "<input>", line 13, in cast_to_string_append
>>>Exception: Function: cast_to_string_append() accepts two or three arguments, and you entered 10.


# no variables

cast_to_string_append()

>>>Traceback (most recent call last):
>>>  File "<input>", line 1, in <module>
>>>  File "<input>", line 13, in cast_to_string_append
>>>Exception: Function: cast_to_string_append() accepts two or three arguments, and you entered 0.

""" End Answer 3 Testing """

So, in summary:

  • Answer 1 – the simplest answer, and works for most cases.
def cast_to_string_concat(a, b, c=None):
  c = a if c is None else c

  return str(a) + str(b) + str(c)
  • Answer 2 – use if None does not actually signify an empty parameter by switching to object() , through _sentinel .
_sentinel = object()
def cast_to_string_concat(a, b, c=_sentinel):
  c = a if c == _sentinel else c

  return str(a) + str(b) + str(c)
  • Answer 3 seeks out a general solution utilizing a wrapper function with arbitrary arity using *args, and handles the acceptable cases inside:
def cast_to_string_append(*args):

    def string_append(a, b, c):
        # this is the original function, it is only called within the wrapper
        return str(a) + str(b) + str(c)

    if len(args) == 2:
        # if two arguments, then set the third to be the first
        return string_append(*args, args[0])

    elif len(args) == 3:
        # if three arguments, then call the function as written
        return string_append(*args)

    else:
        raise Exception(f'Function: cast_to_string_append() accepts two or three arguments, and you entered {len(args)}.')

Use what works for you! But for me, I’ll be using Option 3 😉

Answered By: spen.smith
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.