Challenge #2: The Missing Ternary Operator, 24 July 2006
Find some of the ways to approximate the ternary operator in Python. This used to be a major pastime in the Python world, and still applies to us.
In C and other languages, we can write an expression like this::
iseven(x) ? "even" : "odd"
This evaluates as the string 'even' if the function is true, and as the string 'odd' otherwise. It's sort of like an if-then-else all in one expression.
We don't have one of these in Python 2.3.5. Come up with several different ways to approximate the same behavior. Try for at least three. For each approach, are there any cases where it will fail, or return the wrong answer? Does it short-circuit expressions that don't need to be evaluated?
Answers and discussion next Monday.
iseven(x) ? "even" : "odd"
This evaluates as the string 'even' if the function is true, and as the string 'odd' otherwise. It's sort of like an if-then-else all in one expression.
We don't have one of these in Python 2.3.5. Come up with several different ways to approximate the same behavior. Try for at least three. For each approach, are there any cases where it will fail, or return the wrong answer? Does it short-circuit expressions that don't need to be evaluated?
Answers and discussion next Monday.
Solutions and Commentary
Posted by
jccooper
at
2006-07-31 13:28
There are lots of way to do a ternary operator in Python. But as of Python 2.5 (http://docs.python.org/dev/whatsnew/pep-308.html) there exists an actual language feature for this::
x = true_value if condition else false_value
This is nicely short-circuiting, and also doesn't care what those values are. But we can't use 2.5 just yet.
In earlier versions of Python, the most common construction is like so::
x = condition and true_value or false_value
Chuck prefers this form in his answer, and Kyle provided it first. I'll note that due to order of operations no parens are necessary. But there is one problem with this construction, discussed below.
This is an odd construct in most languages, but because of the way Python booleans work it forms an effective ternary expression provided one condition is met. Since any value in Python can be interpreted as true or false, the 'and' and 'or' statements don't just return a boolean; they return whatever value made the statement true or false. For example::
>>> "hello" and "world"
'world'
>>> "hello" or "world"
'hello'
>>> "hello" and ""
''
>>> "hello" or ""
'hello'
>>> "" and "world"
''
>>> "" or "world"
'world'
>>> "" and 0
''
>>> "" or 0
0
In the 'and' examples, if the first value is false, then the entire expression will be false, and therefore the first value is returned. Otherwise, the second value determines trueness, and it is returned whatever its value. If it is false, then the whole thing is false; if true, then the expression is true.
In the 'or' examples, whichever value is first found true will make the expression true. If the first is true, then its value is returned. Same for the second. If neither is true, then the second value is returned, since it was the final straw in making the expression false.
Combining these characteristics, we can write the ternary above::
x = condition and true_value or false_value
First the expression 'condition and true_value' is evaluated. If condition is true and true_value, then the expression becomes the output of 'true_value'. If condition is false, then the expression evaluates to the (false) value of 'condition'. So we have either::
x = true_value (True) or false_value
or::
x = condition (False) or false_value
In the first case, we always get 'true_value', being true, since at that point the whole 'or' is decided. In the second case, we get whatever is 'false_value', since it will decide the expression.
But there's one wrinkle: what if 'true_value' is false? If condition is false, then nothing has changed, output-wise. But if condition is true, then we're faced with::
condition (true) and False
The outcome of this is still 'true_value', but is false. Then combined with the 'or' clause::
False or false_value
This will always return 'false_value'. Thus, if 'true_value' is false, it will never be returned, and our ternary operator is broken, always giving back the second value. The last pair of these expressions is clearly broken::
>>> True and "Yes" or "No"
'Yes'
>>> False and "Yes" or "No"
'No'
>>> True and "Yes" or 0
'Yes'
>>> False and "Yes" or 0
0
>>> True and 0 or "No"
'No'
>>> False and 0 or "No"
'No'
Usually we write this with 'true_value' being a static string, in which case it works fine. But if that value is a false value or an expression that may return a false value, then we have a problem.
There is one variant that guarantees the inner values to be true::
x = (condition and [true_value] or [false_value])[0]
We wrap them in a list and then unwrap. Since a list with contents is always true, the first value is always true. It's not as pretty, but it works.
There are a few other ways to do a conditional; most are safe with regards to values but are generally not short-circuiting. Chuck provided some functions to call, which will return the right values regardless of their truthfulness, but the expressions are evaluated before being passed. In a Page Template, we can use the 'test' function, which does exactly this.
Kyle provided several solutions based on Python's acceptance of boolean values as either 0 or 1 for index access purposes. None of these are short-circuiting, but are value-safe. For example::
>>> ["hello","world"][1]
'world'
>>> ["hello","world"][0]
'hello'
>>> ["hello","world"][True]
'world'
>>> ["hello","world"][False]
'hello'
With a conversion function, even complex values can work::
>>> ["hello","world"][bool("boo!")]
'world'
>>> ["hello","world"][bool("")]
'hello'
One can do this with dictionary access as well:
>>> {True:'even',False:'odd'}[bool("yohoho")]
'even'
>>> {True:'even',False:'odd'}[bool(0)]
'odd'
This method isn't even limited to two results::
{1:"onesies", 2:"twosies", 3:"threesies"}[condition]
Of course, 'condition' here must be or return an integer 1-3.
Those are the various ways to deal with inline conditional expressions. Of course, you can always use a multi-line 'if' statement!
x = true_value if condition else false_value
This is nicely short-circuiting, and also doesn't care what those values are. But we can't use 2.5 just yet.
In earlier versions of Python, the most common construction is like so::
x = condition and true_value or false_value
Chuck prefers this form in his answer, and Kyle provided it first. I'll note that due to order of operations no parens are necessary. But there is one problem with this construction, discussed below.
This is an odd construct in most languages, but because of the way Python booleans work it forms an effective ternary expression provided one condition is met. Since any value in Python can be interpreted as true or false, the 'and' and 'or' statements don't just return a boolean; they return whatever value made the statement true or false. For example::
>>> "hello" and "world"
'world'
>>> "hello" or "world"
'hello'
>>> "hello" and ""
''
>>> "hello" or ""
'hello'
>>> "" and "world"
''
>>> "" or "world"
'world'
>>> "" and 0
''
>>> "" or 0
0
In the 'and' examples, if the first value is false, then the entire expression will be false, and therefore the first value is returned. Otherwise, the second value determines trueness, and it is returned whatever its value. If it is false, then the whole thing is false; if true, then the expression is true.
In the 'or' examples, whichever value is first found true will make the expression true. If the first is true, then its value is returned. Same for the second. If neither is true, then the second value is returned, since it was the final straw in making the expression false.
Combining these characteristics, we can write the ternary above::
x = condition and true_value or false_value
First the expression 'condition and true_value' is evaluated. If condition is true and true_value, then the expression becomes the output of 'true_value'. If condition is false, then the expression evaluates to the (false) value of 'condition'. So we have either::
x = true_value (True) or false_value
or::
x = condition (False) or false_value
In the first case, we always get 'true_value', being true, since at that point the whole 'or' is decided. In the second case, we get whatever is 'false_value', since it will decide the expression.
But there's one wrinkle: what if 'true_value' is false? If condition is false, then nothing has changed, output-wise. But if condition is true, then we're faced with::
condition (true) and False
The outcome of this is still 'true_value', but is false. Then combined with the 'or' clause::
False or false_value
This will always return 'false_value'. Thus, if 'true_value' is false, it will never be returned, and our ternary operator is broken, always giving back the second value. The last pair of these expressions is clearly broken::
>>> True and "Yes" or "No"
'Yes'
>>> False and "Yes" or "No"
'No'
>>> True and "Yes" or 0
'Yes'
>>> False and "Yes" or 0
0
>>> True and 0 or "No"
'No'
>>> False and 0 or "No"
'No'
Usually we write this with 'true_value' being a static string, in which case it works fine. But if that value is a false value or an expression that may return a false value, then we have a problem.
There is one variant that guarantees the inner values to be true::
x = (condition and [true_value] or [false_value])[0]
We wrap them in a list and then unwrap. Since a list with contents is always true, the first value is always true. It's not as pretty, but it works.
There are a few other ways to do a conditional; most are safe with regards to values but are generally not short-circuiting. Chuck provided some functions to call, which will return the right values regardless of their truthfulness, but the expressions are evaluated before being passed. In a Page Template, we can use the 'test' function, which does exactly this.
Kyle provided several solutions based on Python's acceptance of boolean values as either 0 or 1 for index access purposes. None of these are short-circuiting, but are value-safe. For example::
>>> ["hello","world"][1]
'world'
>>> ["hello","world"][0]
'hello'
>>> ["hello","world"][True]
'world'
>>> ["hello","world"][False]
'hello'
With a conversion function, even complex values can work::
>>> ["hello","world"][bool("boo!")]
'world'
>>> ["hello","world"][bool("")]
'hello'
One can do this with dictionary access as well:
>>> {True:'even',False:'odd'}[bool("yohoho")]
'even'
>>> {True:'even',False:'odd'}[bool(0)]
'odd'
This method isn't even limited to two results::
{1:"onesies", 2:"twosies", 3:"threesies"}[condition]
Of course, 'condition' here must be or return an integer 1-3.
Those are the various ways to deal with inline conditional expressions. Of course, you can always use a multi-line 'if' statement!

>>> iseven(x) and 'even' or 'odd'
2. One that I just thought of that I'm rather partial to:
>>> ('odd','even')[int(iseven(x))]
2.b. To handle other forms of True and False, such as empty lists:
>>> ('odd','even')[int(bool(iseven(x)))]
3. A variation on #2:
>>> {True:'even',False:'odd'}[iseven(x)]
3.b. To handle other forms of True and False, such as empty lists:
>>> {True:'even',False:'odd'}[bool(iseven(x))]
#1 seems like it is probably the fastest to me. When the function is true, it will return after only the first half of the "or" statement. So, it only has to evaluate the function and one or two strings. The others have to build a data structure, evaluate the function, convert the result to an index and evaluate the index.
#2 and #3 Have the advantage/disadvantage or erroring if the result of the function is not a simple True/False, but maybe something different... like a string or list. So, if you desire an error in those cases, they could be better. But, if you don't desire an error, there are always 2b and 3b.
I'll post more analysis if I think or more advantages/disadvantages
(val and t) or f
e.g.
(4%2==0 and 'even') or 'odd'
It even looks a little like the ternary operator:
4%2==0 ? 'even' : 'odd'
This expression truly short-circuits in that 't' is evaluated only when 'val' is True, and 'f' is evaluated only when 'val' is False. Since it is an expression and not a statement, it's value can be assigned to a variable. It's really just the compounding of two operators to achieve the effect of the single ternary operator.
This short-circuiting behavior is nice when you want 't' or 'f' to be a function with side-effects that should only be called when the boolean value in the quasi-ternary has the appropriate value.
My other two solutions both work, but they involve statements and so have to be wrapped in functions, with the resulting value returned.
def ternary1(val, t, f):
if val: return t
return f
def ternary2(val, t, f):
try:
assert val
return t
except AssertionError:
return f
I don't like them because even though the statements within the functions short-circuit, both 't' and 'f' are evaluated when passed in to the function. Thus, if either 't' or 'f' are functions with side-effects, they will both be called when passed into the ternary functions, whether 'val' is True or False.
As for the question of whether or not these solutions could give incorrect results, I don't see how they could, as long as one understands how Python converts non-boolean values to booleans. More than once I have found myself surprised by Python's evaluation of the integer -1 as True. It seems to me that, given Python's rules for converting non-booleans to booleans, these expressions and functions by definition must yield the correct results. Of course I could be wrong.
Here, by the way, is how I tested for short-circuiting:
a = 'untouched'
b = 'untouched'
def myTrue(i):
global a
a = 'touched'
return i
def myFalse(i):
global b
b = 'touched'
return i
print (4%2==0 and myTrue('even')) or myFalse('odd') # 'even'
print a # 'touched'
print b # 'untouched'
print (3%2==0 and myTrue('even')) or myFalse('odd') # 'odd'
print a # 'untouched'
print b # 'touched'