Basic control statements

Complex programs usually consist of the execution of a sequence of commands. To control which command will be executed at a certain point, we use control statements. In all programming languages, we can find two basic control statemets: one that creates loops and one that enables conditional choices.

The if statemet

The if statement is responsible for branching in most programming languages. With the help of if, we can determine for our program which branch to follow if one or more conditions are fulfilled. Let us see an example:

In [1]:
if 2+2==4:
    print('Mathematics still works.')
Mathematics still works.

Attention, we've indented the second line! Indentation is Python's notation for grouping commands.

A common convention is to use four spaces as indentation for code that is at a lower syntactical level. It means that using two subsequent if statements results in eight spaces as indentation for the code block under the second if. This indentation is automatically done in the code cells of a Jupyter Notebook. Pressing Enter after the : mark, the next line will be indented by 4 spaces, pressing further Enter keys keeps the level of this hierarchy. I we want to end the indented block, and go up one level, we have to delete the indentation.

In the C, C++, Java languages, we use curly brackets for grouping code blocks. An example:

if (i==10) {
print i
}

In Fortrain, the word END marks the end of a code block.

If we copy code from another code editor, we have to pay attention, because it may use TABs instead of four spaces for indentation. TABs are sometimes allowed, but are to avoid. Modern Python style guidelines suggest that every indentation should consist of 4 spaces.

In [2]:
today='Monday';
time='12:00';
if today=='Monday':
    if time=='12:00':
        print('Let\'s do Python!')
Let's do Python!

We can tell the program what to do in case the condition is False with the else and elif commands.

In [3]:
x = 1
if x < 0:
    x = 0
    print('Negative, I\'ve changed it to zero.')
elif x == 0:
    print('Zero.')
elif x == 1:
    print('One.')
else:
    print('More than one.')
One.

else and elif are optional. There may be only one else branch, and one or more elif branches. elif is an abbreviation for 'else if', and it is very useful for avoiding unnecessary indentation. An if ... elif ... elif block subtitutes the switch and case commands of other programming languages.

The for and while loops

One of the best properties of computers is that they are very fast and 'indefatigable'. They are most eective in solving tasks where the problem is relatively easy, but its execution needs numerous repetition or iteration. Iteration can be or example when our program goes through every element of a list, and performs operations on every element. This is what Python's for statement is used for.

In [4]:
days_of_the_week = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]
In [5]:
for day in days_of_the_week:
    print(day)
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

This piece of code goes through every element of the days_of_the_week list, and assigns the visied elements to the day variable, that is also called the loop variable. After that, it executes every command that is in the indented codeblock, which for now is only the print statement. For these commands, it may use the loop variable. After the end of the indented block, it terminated the loop.

Calling the loop variable day has no significance whatsoever. The program doesn't know anything about human time, or that in a week, there are days, not kitties:

In [6]:
for kitty in days_of_the_week:
    print(kitty)
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday

The codeblock of a loop may consist of more than one commands:

In [7]:
for day in days_of_the_week:
    statement = "Today is " + day
    print(statement)
Today is Sunday
Today is Monday
Today is Tuesday
Today is Wednesday
Today is Thursday
Today is Friday
Today is Saturday

The range() command may be used if we want to execute a given number of operations in a for loop.

In [8]:
for i in range(20):
    print(i," times ",i ,"equals to",i*i)
0  times  0 equals to 0
1  times  1 equals to 1
2  times  2 equals to 4
3  times  3 equals to 9
4  times  4 equals to 16
5  times  5 equals to 25
6  times  6 equals to 36
7  times  7 equals to 49
8  times  8 equals to 64
9  times  9 equals to 81
10  times  10 equals to 100
11  times  11 equals to 121
12  times  12 equals to 144
13  times  13 equals to 169
14  times  14 equals to 196
15  times  15 equals to 225
16  times  16 equals to 256
17  times  17 equals to 289
18  times  18 equals to 324
19  times  19 equals to 361

Our programs are becoming more and more interesting, if we combine loops with conditional branches from above:

In [9]:
for day in days_of_the_week:
    statement = "Today is " + day
    print(statement)
    if day == "Sunday":
        print ("   Sleep in")
    elif day == "Saturday":
        print ("   Do chores")
    else:
        print ("   Go to work")
Today is Sunday
   Sleep in
Today is Monday
   Go to work
Today is Tuesday
   Go to work
Today is Wednesday
   Go to work
Today is Thursday
   Go to work
Today is Friday
   Go to work
Today is Saturday
   Do chores

Note how for and if are embedded into each other!

Similarly to the for statement, we can use the while statement for creating loops. Let us observe an example:

In [10]:
i=0
while i<10:
    print(i)
    i+=1
0
1
2
3
4
5
6
7
8
9

Attention! I we are not careful enough, our while loop might result in an infinite loop! For example, the cell below, if we do not interrupt it with the Interrupt command from the Kernel menu (or by pressing I two times outside o editing mode), never stops!

while 1==1:
    print('Allitsák meg a világot, ki akarok szállni!!')

If we complemet the while loop with an else branch, we can write a piece of code that executes if the condition of the while statement is no longer True.

In [11]:
i=0
while i<10:
    print(i)
    i+=1
else:
    print('THE END')
0
1
2
3
4
5
6
7
8
9
THE END

Similarly to other languages, the break interrupts the deepest for or while loop.

In [12]:
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'is factorizable, e.g.:', x, '*', n/x)
            break
        else:
            print(n, 'is a prime.')
3 is a prime.
4 is factorizable, e.g.: 2 * 2.0
5 is a prime.
5 is a prime.
5 is a prime.
6 is factorizable, e.g.: 2 * 3.0
7 is a prime.
7 is a prime.
7 is a prime.
7 is a prime.
7 is a prime.
8 is factorizable, e.g.: 2 * 4.0
9 is a prime.
9 is factorizable, e.g.: 3 * 3.0

More on the break command and other control statements in English.

An example: Fibonacci series

The first two elements of the Fibonacci series are 0 and 1, and each next element is the sum of the previous two elements: 0,1,1,2,3,5,8,13,21,34,55,89,...

Computing further elements is a perfect task for a computer!

In [13]:
n = 10 # number of elements we want to compute
sequence = [0,1] # the first two elements
for i in range(2,n): # numbers from 2 to n, n msut not be less than two!
    sequence.append(sequence[i-1]+sequence[i-2])
print (sequence)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Let us go through the code step by step. First, we set the value of n to 10, which is the length of the sequence to compute. We call the list that is going to store the series sequence, and initialize it with the first two values. After this job done 'by hand', we automate the process by iteration.

We begin the iteration with 2, which is the 3rd element due to the fact that indexing begins with 0. We are going to go until n, that has been previously given.

In the body of the loop we append the sum of the previous two elemets, that have already been calculated. After the end of the loop, we write the results.

Let us see the same example with a while statement:

In [14]:
sequence=[0,1]
while len(sequence)<10:
    sequence.append(sequence[-1]+sequence[-2])
print (sequence)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Functions

In many programming languages, it is common practice to group several commands into so-called functions. Well written functions make the usage of the programs easier, and the code will be much more readable.

Declaring functions

If we want to create a Fibonacci series with different number of elemets than previously, we could copy our code into a new cell, and we could rewrite n=10 to n=100, for example. But it is a much more effective method to define this piece of code as a new function with the def command:

In [15]:
def fibonacci(sequence_length):
    "The first *sequence_length* elements of the Fibonacci sequence." # this is a 'help', the so-called docstring
    sequence = [0,1]
    if 0 < sequence_length < 3:
        return sequence[:sequence_length]
    for i in range(2,sequence_length): 
        sequence.append(sequence[i-1]+sequence[i-2])
    return sequence

Now we can call the function fibonacci() for different lengths.

In [16]:
fibonacci(5)
Out[16]:
[0, 1, 1, 2, 3]

Let us analyze the above piece of code. The colon mark and the indentation define which commands belong to the function definition. In the second line, there is a string between quotation marks as a comment. This is the so-called docstring, that explains the function briefly, and can be later evoked by the help command.

In [17]:
help(fibonacci)
Help on function fibonacci in module __main__:

fibonacci(sequence_length)
    The first *sequence_length* elements of the Fibonacci sequence.

In Jupyter Notebooks, docstrings can be read by using a question mark:

In [18]:
?fibonacci

We can view docstrings in the Jupyter Notebook by pressing SHIFT+TAB while typing the parentheses of the function call. Try it in the next cell without running the code! If you press TAB several times while holding the SHIFT key, you can put the docstring out to the lower half of your browser.

In [19]:
fibonacci()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-19-9f4a5433493a> in <module>()
----> 1 fibonacci()

TypeError: fibonacci() missing 1 required positional argument: 'sequence_length'

The output of a function is determined by the return keyword. If we don't use a return command, then our function returns with a None value. After running a function that has not executed a return, it also returns None. The function fibonacci() returns with a list.

In [20]:
x=fibonacci(10)
x
Out[20]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

This function returns None.

In [21]:
def empty_function(x):
    print('I am an empty function, though I print stuff!')
    y=x-2;

Therefore, there is no value in the variable z.

In [22]:
z=empty_function(3)
z
I am an empty function, though I print stuff!
In [23]:
print(z)
None

A function might have more input values, so-called arguments.

In [24]:
def summing(a,b):
    return a+b

We can get more than one return values:

In [25]:
def plusminus(a,b):
    return a+b,a-b
In [26]:
p,m=plusminus(2,3)
print (p)
print (m)
5
-1

Parameter list and unpacking

A function may have several input values, and the input values of a function may be arranged into a list by another function. A typical example is a least squares fit, where we can pass function parameters by unpacking a list with an * mark in a very compact way, as we will see later. Let us see an example for fitting a fifth-order polynomial:

$$f(x)=a_0+a_1x+a_2x^2+a_3x^3+a_4x^4+a_5x^5$$

Let us define a function, that receives the variable x, and the six parameters $a_0,\dots,a_5$ if the above polynomial:

In [27]:
# this is the function to fit
def poly5(x,a0,a1,a2,a3,a4,a5):
    return a0+a1*x+a2*x**2+a3*x**3+a4*x**4+a5*x**5

We have six parameters to fit, that are given by the fitting function as a list:

In [28]:
# these are the fitted parameters
# according to the following sequence
# params=[a0,a1,a2,a3,a4,a5]
params=[ 2.27171539, -1.1368942 ,  0.65380304, -0.25005187, -0.1751268 , -0.48828309];

If we want to evaluate our polynomial at 0.3, we can do it in the following way:

In [29]:
poly5(0.3,params[0],params[1],params[2],params[3],params[4],params[5])
Out[29]:
1.9801329481213

or more compactly:

In [30]:
poly5(0.3,*params)
Out[30]:
1.9801329481213

This construction makes it possible to prepare the function for receving a varying number of input values. If we put an * mark before a parameter in the function definition, that parameter can be of any length! Let's see an example:

In [31]:
def i_give_what_i_receive(*argv):                                             # we prepare the function to receive  
                                                                              # any number of parameters
    print("I have ",len(argv),"input arguments.")
    for arg in argv:
        print ("This is an argument:", arg)
    return argv[-1]                                                 # we refer to input arguments as the elements
                                                                    # of a common list

The function in the above code cell can receive any number of parameters. It prints the number of parameters, their values, and returns the last input parameter.

In [32]:
i_give_what_i_receive('Caspar','Melchior','Balthazar')
I have  3 input arguments.
This is an argument: Caspar
This is an argument: Melchior
This is an argument: Balthazar
Out[32]:
'Balthazar'

Of course, we can use the 'unpacking' of an arbitrary list as parameter:

In [33]:
i_give_what_i_receive(*params) # unpacking
I have  6 input arguments.
This is an argument: 2.27171539
This is an argument: -1.1368942
This is an argument: 0.65380304
This is an argument: -0.25005187
This is an argument: -0.1751268
This is an argument: -0.48828309
Out[33]:
-0.48828309

Functions with keywords

Apart from dicionaries being very useful data structures in themselves, we will see later, that it is common to use them as function parameters. These parameters are called keyword arguments. They can make the code much more readable to the human eye, and they give more flexibility while programming.

In [34]:
# setting default values
def students(time, state='paying attention to the teacher', activity='experimenting', lesson='physics'):
    print("These students are "+state+" while "+activity+" during a "+lesson+" lesson!");
    print("The current time is",time,"!");

It is compulsory to give the first parameter of the function. If we don't give any more, then it is going to use the default values for the keyword arguments.

In [35]:
students('5PM') 
These students are paying attention to the teacher while experimenting during a physics lesson!
The current time is 5PM !

If it receives one keyword argument, it uses only that one as different from the default values.

In [36]:
students('8AM',state='pulling faces')
These students are pulling faces while experimenting during a physics lesson!
The current time is 8AM !

We don't have to pay attention to the order of the keyword arguments:

In [37]:
students('8AM',lesson='Latin',state='panicking',activity='writing an essay')
These students are panicking while writing an essay during a Latin lesson!
The current time is 8AM !

If we give a keyword that has not been declared in the function definition, we get an error.

In [38]:
students('5PM',teacher='Miss Smith')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-38-62220882d70c> in <module>()
----> 1 students('5PM',teacher='Miss Smith')

TypeError: students() got an unexpected keyword argument 'teacher'

Similarly, we get a problem if we use a keyword two times:

In [39]:
students('8AM',lesson='Latin',lesson='chemistry')
  File "<ipython-input-39-ab4d06b60fe2>", line 1
    students('8AM',lesson='Latin',lesson='chemistry')
                                 ^
SyntaxError: keyword argument repeated

We can unpack the dictionary of keyword arguments with a double **.

In [40]:
student_dict={'lesson':'music','state':'whining'};
students('12AM',**student_dict)
These students are whining while experimenting during a music lesson!
The current time is 12AM !

Just as in the case of simple arguments, it may happen that we would like to process a dictionary of arbitrary length. or example, if we define a function that calls several other functions, for which we would like to pass some of the incoming parameters.

The function below expects an arbitrary dictionary as an input, inspects its length, and if there is a keyword argument 'hamburger', it returns its value.

In [41]:
def check_hamburger(**dictionary):
    print('The length of the dictionary:',len(dictionary))
    if 'hamburger' in dictionary:
        print('There is hamburger!')
        return dictionary['hamburger']
In [42]:
check_hamburger(macaroni=1,cake='delicious') # we can give whatever keywords, even if not predefined!
The length of the dictionary: 2
In [43]:
eat={'macaroni':1,'cake':'finom','hamburger':137,'salad':None}
check_hamburger(**eat) 
The length of the dictionary: 4
There is hamburger!
Out[43]:
137

Custom practices for function definition

We can give parameters to functions in many ways:

  1. variables (simple, lists, keyword arguments etc.)
  2. list of varying length with *
  3. dict of varying length with **

We may use all types in one single function definition. Use the following order:

In [44]:
def difficult_function(var1,var2,var3='ELZETT',*args,**kwargs):
    if ((len(args)==0 and len(kwargs)==0)):
        return var3+str(var2)+str(var1)
    elif (len(args)!=0 and len(kwargs)==0):
        return 'There is something in args!'
    elif (len(args)==0 and len(kwargs)!=0):
        return 'There is something in kwargs!'
    else:
        return 'We have all possible kinds of arguments: '+str(var1)+str(var2)+var3+str(args)+str(kwargs)

The first two arguments of the above function are 'simple' arguments, the third is a keyword argument with a default value of 'ELZETT', and then we can have an arbitrary number of 'simple' arguments (args) and keyword arguments (kwargs). Let us observe some simple behaviours of this function:

In [45]:
difficult_function(1,2)
Out[45]:
'ELZETT21'
In [46]:
difficult_function(1,2,var3='MULTLOCK')
Out[46]:
'MULTLOCK21'
In [47]:
difficult_function(1,2,*days_of_the_week)
Out[47]:
'There is something in args!'
In [48]:
difficult_function(1,2,**eat)
Out[48]:
'There is something in kwargs!'
In [50]:
difficult_function(1,2,*days_of_the_week,**eat)
Out[50]:
"We have all possible kinds of arguments: 12Sunday('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'){'macaroni': 1, 'cake': 'finom', 'hamburger': 137, 'salad': None}"

The lambda functions

We can give not only variables, but other funtions as inputs for functions. For example, we may think of a function, that plots mathematical functions. In this case, when a function expects a function as an input, it is possible, that it would be a tedious task to define all possible input functions. Then, it is much more compact to use lambda functions.

Let us see an example. Let us define a function that evaluated another function at a given place, outputs that place and returns the value at that place.

In [51]:
def funfun(g,x):
    print('This is the place: ',x)
    return g(x)
In [52]:
def fx(x):
    return x**2-1/x;
funfun(fx,0.1)
This is the place:  0.1
Out[52]:
-9.99

Then, we can omit the definition of fx by using the following abbreviation:

In [53]:
funfun(lambda x:x**2-1/x,0.1)
This is the place:  0.1
Out[53]:
-9.99

In the above expression, this piece of code

lambda x:x**2-1/x

defines an anonymous function, that assigns $x^2-1/x$ to the variable $x$.

We can also use multi-variable functions in lambda functions:

This expression

lambda x,y:x**2-1/y

is equivalent to the following function:

def fxy(x,y):
    return x**2-1/y