简体   繁体   中英

Set argument equal to another argument's value by default

I've seen many times where Python programmers (myself included) want a variable in a given function to default to another variable if that value is not given.

This is designed as a walkthrough with three different solutions to the problem, each of increasing complexity and robustness. So, onward!

This is for you, if you would say, I'm trying to do this:

def my_fun(a, b, c=a):
  return str(a) + str(b) + str(c)

In this example, if c is not given, then we would append a str(a) to the end. This is simple, and just a simple toy example, and I do not doubt your use case is likely much more complicated. However, this is not syntactically correct Python, and it will not run, as a is not defined.

Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'a' is not defined

However, most often I see the accepted answers of:

  • "No, this is not possible"
  • "You should use the default value of None , then check if the value is None ."

If this sounds like a problem you've encountered, I hope I can help!

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() , which removes the use of None, and replaces it with the object() through using the implied private sentinel . (I briefly talk about another similar (but failed) attempt in the Addendum .)


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!


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 aa 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)}.')

As my favorite computer science professor Dr. James Cain, an articulate advocate of utmost security and completeness, said, "Computing is contextural [sic]", so always use what works for you! But for me, I'll be using Option 3 ;)

Thanks for reading!

-Spencer


Addendum: The next three links suggest using some semblance of a class or import statement, but I chose not to go down this route. If this looks like what you're after, give it a go!

For answers utilizing a class, read these:

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM