## CodingBison

A program may need to execute different sets of statements depending upon a condition. For example, a program can execute a specific set of statements to invoke a particular action, when a user provides a specific input. Thus, running these statements is conditional upon a specific user input. Alternatively, if the user does not provide the specified input, then the program can simply skip running these statements.

Python provides several conditional constructs for executing tasks that rely upon the value of a condition (a variable or an expression). It supports three types of conditional constructs: (a) if, (b) if-else, and (c) if-elif-else, We can consider the first two forms as simplification of the if-elif-else form. Note that Python does not provide a (C-like) switch statement.

We begin by providing (below) generic formats of the first three constructs. We show two conditions ("condition2" and "condition3") with the elif clause; however, a program can have as many number of such clauses as it requires.

``` #Format of a single if expression
if condition:
statement
...

#Format of an if-else expression
if condition:
statement
...
else:
statement
...

#Format of an if-elif-else expression
if condition1:
statement
...
elif condition2:
statement
...
elif condition3:
statement
...
else:
statement
...
```

Now, for each of these cases, Python statements that follow after the if, else, or elif clauses form a block. Since Python uses indentation to form a block, all the statements within one block should have identical indentation (N white-spaces or N-tabs). In fact, with Python 3, indentation should be identical even in terms of white-spaces and tabs. Thus, if statement has 1 tab and statement has same number of white-spaces as a tab, then Python3 considers them as unequal indentation and will throw an error. It is a good practice to keep uniform indentation (e.g. 4 white-spaces).

Next, let us write a simple program that implements these conditional expressions. The program (provided below) uses these expressions to find specific animals from a list of animals. As we can see, we provide print statements only when the specified condition becomes true.

``` #List of cats.
listCats = ["tiger", "mountain lion", "lion", "bobcat"]

#An if clause: find lion.
for element in listCats:
if (element == "lion"):
print("[if]Found Lion")

#An if-else clause: find lion.
print("")
for element in listCats:
if (element == "lion"):
print("[if-else]Found Lion")
else:
print("[if-else]Searching..")

#An if-elif-else clause: find lion. Even mountain-lion would do!!
print("")
for element in listCats:
if (element == "lion"):
print("[if-elif-else]Found Lion")
elif (element == "mountain lion"):
print("[if-elif-else]Found Mountain Lion")
else:
print("[if-elif-else]Searching..")
```

And, here is the output of the above program; to run the above program (save in a file, let us say, "ifelse.py"), and run it from the command prompt. As expected, both "if" and "if-else" cases allow us to look for one condition. But, with "if-elif-else", we can look for multiple conditions.

``` [user@codingbison]\$ python3 ifelse.py
[if]Found Lion

[if-else]Searching..
[if-else]Searching..
[if-else]Found Lion
[if-else]Searching..

[if-elif-else]Searching..
[if-elif-else]Found Mountain Lion
[if-elif-else]Found Lion
[if-elif-else]Searching..
[user@codingbison]\$
```

For certain cases, we may not want to do anything. For such cases, Python provides "pass" keyword; as the name suggests this statement does nothing. Here is a trivial example. When we run this example, it prints "Found Lion".

``` #List of cats.
listCats = ["tiger", "mountain lion", "lion", "bobcat"]

#An if clause: find lion.
for element in listCats:
if (element == "lion"):
print("Found Lion")
else:
#Do nothing.
pass
```

Python also provides a handy form of the if-else conditional expression in a ternary form: "doThis1 if condition else doThis2". This is equivalent to

``` if condition:
doThis1
else:
doThis2
```

This means that if the "condition" is true, then we execute "doThis1", else, we execute "doThis2". A simple example could be to find the maximum of the two numbers; we provide the example below. The output for this example is "The maximum of these two numbers is 1000".

``` numA = 500
numB = 1000

numMax = numA if (numA > numB) else numB
print("The maximum of these two numbers is " + str(numMax));
```

### Logical Operators: "or" and "and"

In the earlier examples, we saw that for conditions, we used simpler expressions like 'if (element == "lion"):', where we check if the value of the variable "element" equals the string "lion".

More often than not, we need to have conditions that are combinations of such simple checks. Python provides two logical operators to help us accomplish this task: "or" and "and". The check "condition1 or condition2" returns True if either of these conditions are True. On the other hand, the check "condition1 and condition2" returns True only if both of these conditions are True.

With these logical operators, we can combine simpler conditional expressions into one larger and more sophisticated check. For example, if we want to check if the element is a "lion" or a "mountain lion", then we can use the "or" operator to do so in one shot: 'if (element == "lion") or (element == "mountain lion"): ' On the other hand, let us say that we have two variables, element1 and element2 and if we want to verify that one equals "lion" and the other equals "mountain lion", then we can use the "and" operator as: 'if (element1 == "lion") and (element2 == "mountain lion"): '

With that background, let us make the earlier example more specific using these two logical operators. For the "or" case, we use it to check if the "element" equals either "lion" or "mountain lion". For the "and" case, we make the example slightly non-trivial. We first taken into account the number of tokens (words) for an "element". We use the split() method to split a string into tokens and we use "white-space" as a basis for creating tokens from a string -- we will revisit this method in our coverage for strings. If there is only word, then we check if it is "lion". And, if there are two words, then we check if the first word (tokens[0]) equals "mountain" and the second word (tokens[1]) equals "lion".

``` #List of cats.
listCats = ["tiger", "mountain lion", "lion", "bobcat"]

#An if clause with "or" identifier.
for element in listCats:
if (element == "lion") or (element == "mountain lion"):
print("[or]Found a " + element)

#An if clause with "and" identifier.
for element in listCats:
tokens = element.split()
print("[and]Tokens are: " + str(tokens))
if (len(tokens) == 1) and (tokens[0] == "lion"):
print("[and]Found a lion")
if (len(tokens) == 2) and (tokens[0] == "mountain") and (tokens[1] == "lion"):
print("[and]Found a mountain lion")
```

Here is the output when we run the above example.

``` [user@codingbison]\$ python3 and-or.py
[or]Found a mountain lion
[or]Found a lion

[and]Tokens are: ['tiger']
[and]Tokens are: ['mountain', 'lion']
[and]Found a mountain lion
[and]Tokens are: ['lion']
[and]Found a lion
[and]Tokens are: ['bobcat']
[user@codingbison]\$
```

Given a chain of conditions, these logical operators are checked as long as the outcome is not established. The moment we find we have evaluated a set of conditions and established the outcome, we ignore the rest and proceed! This is also called short-circuiting. Let us understand it for both "or" and "and" cases.

For "or" cases, the goal of the short-circuit is to establish the outcome as true. For example, with the check "condition1 or condition2 or condition3", we will return as soon as any of these becomes true. Thus, if "condition1" is true, then we return immediately without bothering to check "condition2" and "condition3". On the other hand, if "condition1" is false, then we will proceed and check if "condition2" is true. If so, then we will return without checking "condition3".

For "and" cases, the goal of the short-circuit is to establish the outcome as false. Thus, with the check "condition1 and condition2 and condition3", if "condition1" is false, then we know that the entire set of check would return false and we return immediately. We continue as long as the conditions continue to be true. In the end, if all conditions are true, then we return true.

This case of short-circuit is often kept in mind when considering the performance. As an example, if for check "condition1 or condition2", "condition1" is more unique and is likely to establish true or false in majority of cases, then it is a good idea to put the check "condition1" first -- this way, we can skip another computation of checking "condition2" for majority of the cases!