= 1; num_1_2 = 1.0 num_1_1
15 Python special features
15.1 Conditional expressions
Python has 3 specials concepts related to conditional expressions.
- truthy/falsy: object’s associated truth value
- execution context: decides evaluation result
- short circuit
All 3 concepts can be combined and used in interesting ways which allow some newer cleaner code styles for some common situations that occur while coding.
15.1.1 Truthy and Falsy
- Every object in Python has an associated truth value
- referred to as object’s truth value
- if the object’s truth value is
True
\(\implies\) truthy - if the object’s truth value is
False
\(\implies\) falsy - hence the object is truthy or falsy
- Below cases are falsy and everything else truthy:
None
False
0
in any numeric type (e.g. 0, 0.0, 0+0j, …)len(c) = 0
: empty collections- custom classes that implement
__bool__
or__len__
method that returnFalse
or0
bool(any_object)
function returns object’s truth value
15.1.1.1 Examples
15.1.1.1.1 Numeric type
>>> num_1_1 = 1, bool(num_1_1) = True, type(num_1_1) = <class 'int'>
>>> num_1_2 = 1.0, bool(num_1_2) = True, type(num_1_2) = <class 'float'>
= 0; num_2_2 = 0.0 num_2_1
>>> num_2_1 = 0, bool(num_2_1) = False, type(num_2_1) = <class 'int'>
>>> num_2_2 = 0.0, bool(num_2_2) = False, type(num_2_2) = <class 'float'>
15.1.1.1.2 Collections
= ""; empty_tuple = (); empty_list = []; empty_dict = {} empty_string
>>> bool(empty_string) = False, bool(empty_tuple) = False
>>> bool(empty_list) = False, bool(empty_dict) = False
= "abc"; non_empty_tuple = (1, 2)
non_empty_string
= ["a", 1]; non_empty_dict = {"key 1": empty_list} non_empty_list
>>> bool(non_empty_string) = True, bool(non_empty_tuple) = True
>>> bool(non_empty_list) = True, bool(non_empty_dict) = True
15.1.2 Short circuit
Short circuit is general optimization strategy used by many languages. Main idea is to avoid evaluating unnecessary condition in an boolean combination.
For example in and
combination if first condition is false then there is no point checking the second condition. Therefore, second condition of the combination is not evaluated.
Similarly for or
combination if the first condition is true
then the expression is true
in both possible cases, therefore second condition is not evaluated.
15.1.3 Execution context
Python treats conditional expressions differently based on where they are used.
Boolean combinations used in conditional expression, can contain objects directly rather than comparisons. Object’s truth value (bool(<object>)
) is used rather than object itself.
15.1.3.1 if
/elif
conditions
Conditional expression used in if
/elif
statement’s condition return booleans values (True
/False
).
Condition can contain object’s as well, bool(object)
is used rather than object’s value.
In below code, since x
is an empty list it is falsy, bool(x) = False
, therefore else
block is executed.
= []
x if x:
print("x is truthy")
else:
print("x is falsy")
>>> x is falsy
In below code, since x
is a string, it is truthy (bool(x) = True
), therefore if
block is executed.
= "abcd"
x if x:
print("x is truthy")
else:
print("x is falsy")
>>> x is truthy
In below example, since bool(x) = False
, else
block is executed without evaluating bool(y)
.
= []; y = None
x if x and y:
print("x and y returned True")
else:
print("x was false, therefore y was not evaluated")
>>> x was false, therefore y was not evaluated
15.1.3.2 Outside if
/elif
condition
If a boolean combination is used outside if
statement and contains objects rather than comparison, then the underlying object’s value is returned, it may or may not be boolean data type.
Below regular comparisons are used outside if
block and they are treated as usual, returning boolean values (True
/False
).
= []; y = 2
x < 5; y == 2 y
>>> True
>>> True
Since boolean combination is used outside if
/elif
statement, bool(x)
is checked and found to be falsy, using short circuit for and
, x is returned and y is not evaluated.
= []; y = 2
x and y x
>>> []
Since boolean combination is used outside if
/elif
statement, bool(x)
is checked and found to be falsy, using short circuit for or
, y is evaluated and returned.
= []; y = 2
x or y x
>>> 2
15.1.4 Summary
Tables below summarize the scenarios, where 0
and 1
signify boolean True
and False
.
x |
y |
x and y |
x and y (outside if /elif ) |
---|---|---|---|
1 | 1 | 1 | y |
1 | 0 | 0 | y |
0 | 1 | 0 | x |
0 | 0 | 0 | x |
x |
y |
x or y |
x or y (outside if /elif ) |
---|---|---|---|
1 | 1 | 1 | x |
1 | 0 | 1 | x |
0 | 1 | 1 | y |
0 | 0 | 0 | y |
For x and y
, where x
and y
can be variables, objects or conditions:
- evaluate
bool(x)
- if
bool(x)
isTrue
- if
x and y
is part of a condition inif
/elif
block- evaluate and return
bool(y)
- evaluate and return
- if
x and y
is not part of a condition inif
/elif
block- return
y
- return
- if
- if
bool(x)
isFalse
- if
x and y
is part of a condition inif
/elif
block- return
False
- return
- if
x and y
is not part of a condition inif
/elif
block- return
x
- return
- if
- if
Similarly, for x or y
, where x
and y
can be variables, objects or conditions:
- evaluate
bool(x)
- if
bool(x)
isTrue
- if
x and y
is part of a condition inif
/elif
block- return
True
- return
- if
x and y
is not part of a condition inif
/elif
block- return
x
- return
- if
- if
bool(x)
isFalse
- if
x and y
is part of a condition inif
/elif
block- evaluate and return
bool(y)
- evaluate and return
- if
x and y
is not part of a condition inif
/elif
block- return
y
- return
- if
- if
15.1.5 Use cases
15.1.5.1 Iterables
It is common situation where state of an iterable is not known in advance.
For example a list generated by some operation which can result in None
or empty list or a list with values. There are some operations to be done only if the list is truthy.
Using the 3 concepts the code can be simplified as below. Both the condition are to ensure that the object is truthy.
Note that len(some_itr) > 0
is used as second condition in and
combination. Due to short circuit it is not evaluated if first condition is False
, i.e. some_itr
is None
. If it is used as first condition, it is evaluated always, therefore will give error when the iterable is None
.
= # some iterable object
some_itr if some_itr is not None and len(some_itr) > 0:
# code block
else:
# code block
= # some iterable object
some_itr if some_itr:
# code block
else:
# code block
15.1.5.2 Assign default
Another common situation is to assign a default value in case the expected value is falsy.
Below form of assignment will work. If expected_var
is truthy, because of short circuit or
, it will be returned and assigned to new_var
. If it is falsy, default object will be evaluated and returned.
= expected_var or default_object new_var
15.2 Comprehensions
Comprehensions are a newer feature in Python which combine the fundamentals of map and filter in a more concise syntax.
They are relevant for iterables, i.e. tuples, lists, sets and dictionaries. Mostly they are used with lists and dictionaries.
Generic idea is
- transformation iteration filter or
- expression loop condition
i.e. map expression to each item in iterable which satisfy the filter.
- Filter/condition is optional
- Comprehensions can be nested
15.2.1 List comprehensions
For list comprehension generic syntax is
[expression using item for item in list if condition on item]
example without filter: list of squares from a list of integers
= list(range(10))
some_itr = [item**2 for item in some_itr]
squared_itr print(some_itr, squared_itr, sep="\n")
>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
- example with filter: list of squares from a list of integers if integer is even
= tuple(range(10))
some_itr = [item**2 for item in some_itr if item % 2 == 0]
evens_squared_itr print(some_itr, evens_squared_itr, sep="\n")
>>> (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
>>> [0, 4, 16, 36, 64]
- example with multiple iterables: list of sum of squares of 2 iterables of numbers
= (*range(1,4),); t_2 = (*range(4,7),)
t_1 = [x**2 + y**2 for x, y in zip(t_1, t_2)]
sum_of_sqrs t_1, t_2, sum_of_sqrs
>>> ((1, 2, 3), (4, 5, 6), [17, 29, 45])
- example of nested comprehensions: make all possible combinations of letters of 2 strings if they are not equal
= 'abc'; string_2 = 'axy'
string_1 = [l1 + l2 for l1 in string_1 for l2 in string_2 if l1 != l2]
combinations print(combinations)
>>> ['ax', 'ay', 'ba', 'bx', 'by', 'ca', 'cx', 'cy']
For better code readability, this can be written as
= 'abc'; string_2 = 'axy'
string_1 = [l1 + l2
combinations for l1 in string_1
for l2 in string_2
if l1 != l2]
print(combinations)
>>> ['ax', 'ay', 'ba', 'bx', 'by', 'ca', 'cx', 'cy']
This is same as
= 'abc'; string_2 = 'axy'
string_1 = []
combinations for l1 in string_1:
for l2 in string_2:
if l1 != l2:
+ l2)
combinations.append(l1 print(combinations)
>>> ['ax', 'ay', 'ba', 'bx', 'by', 'ca', 'cx', 'cy']
15.2.2 Tuple, set, dict
To return a tuple instead of a list from a comprehension use tuple constructor instead of square brackets.
For sets and dictionary just use curly braces instead of square brackets.
- example with multiple iterables: tuple of sum of squares of 2 iterables of numbers
= (*range(1,4),); t_2 = (*range(4,7),)
t_1 = tuple(x**2 + y**2 for x, y in zip(t_1, t_2))
sum_of_sqrs t_1, t_2, sum_of_sqrs
>>> ((1, 2, 3), (4, 5, 6), (17, 29, 45))
- example with set
= set((1, 2, 3))
some_set print({e**2 for e in some_set if e > 1})
>>> {9, 4}
- example with dictionary
import string
= dict(zip(string.ascii_letters[0:10], list(range(10))))
dict_1 print(dict_1)
>>> {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9}
= {k: v**2 for k, v in dict_1.items() if k in {'a', 'e', 'i'}}
dict_2 print(type(dict_2))
print(dict_2)
>>> <class 'dict'>
>>> {'a': 0, 'e': 16, 'i': 64}