Reputation: 589
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!
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:
None
, then check if the value is None
."If this sounds like a problem you've encountered, I hope I can help!
Upvotes: 4
Views: 3058
Reputation: 589
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.)
_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.
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:
def cast_to_string_concat(*args):
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 """
def cast_to_string_concat(a, b, c=None):
c = a if c is None else c
return str(a) + str(b) + str(c)
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)
*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 showsc == object()
isFalse
, andstr(c) == str(object())
is alsoFalse
, and that's why I'm using the implementation from Martin Pieters.
Upvotes: 3