Lesson 14 - Introduction to Functions

The following topics are discussed in this notebook:

  • Defining functions.
  • Parameters and arguments.
  • Return values.

Function Basics

A function is a sequence of instructions that has been given a name. Functions usually (but not always) take some number of inputs, called parameters, and usually (but not always) give an output, called the return value. When we use a function in our code, we say that we are calling the function.

Once a function has been defined, it can be called from anywhere within your program. If you find that there is a task that you are repeating often and that requires several lines of codes, it might be a good idea to group those lines of code together into a function.

Most functions we write will accept one or more inputs and will generate an output. However, We will start by considering functions that do not accept inputs, and that do not provide outputs. The syntax for defining such a function is as follows:

def function_name():
    Code to be executed by the function.

We start by defining a (very) simple function below.

In [1]:
def greeting():
    print("Hello, world!")

Notice that nothing was printed when we defined the function. This cell defined the function, but it did not call it. Thus, the code inside of the function was not executed.

In [2]:
greeting()
Hello, world!
In [3]:
for i in range(0,5):
    greeting()
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!

Parameters and Arguments

The function greeting() defined above is not vary useful, for a few reasons. In particular, it doesn't accept any inputs. There are occassions when you will want to write functions that take no inputs, but functions are generally more useful when we are able to supply them information that affects how they behave.

When defining a function, we can specify that a function expects one or more inputs by listing variable names between the parentheses to serve as placeholders for the inputs. The placeholders themselves are called parameters, and specific values that are supplied as inputs are referred to as arguments.

In the example below, we create a function square() that has a single parameter, denoted by n. When the function is called, an argument must be supplied as a value for this parameter. The function will then print the square of that argument.

In [4]:
def square(n):
    print(n**2)

In the cell below, we call the square function with several different arguments. Each one of these arguments is substituted in for the parameter n for that particular function call.

In [5]:
square(2)
square(3.6)
square(-3)
4
12.96
9

If we would like, we can specify the name of the parameter when we are calling a function. This is usually not necessary, however.

In [6]:
square(n=4)
16

Note that (unlike many languages) Python does not ask us to specifiy what data type the arguments should be. However, if we supply an argument for which the function would perform an illegal operation, we will get an error.

In [7]:
square('circle')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-aafa7e540230> in <module>
----> 1 square('circle')

<ipython-input-4-3e7f8c3e48a6> in square(n)
      1 def square(n):
----> 2     print(n**2)

TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

Additionally, if a function has one or more parameters, but we don't supply arguments for those parameters when we call the function, we will get an error.

In [8]:
square()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-67e8141f4182> in <module>
----> 1 square()

TypeError: square() missing 1 required positional argument: 'n'

Example: Reporting the Square of a Number

In this example, we will write a function called square_message(). The function should take one argument, n, and should print the message:

The square of [n] is [n**2].
In [9]:
def square_message(n):
    print("The square of ", n, " is ", n**2, ".", sep='')

In the cell below, we have a loop that applies the function square_message() to every element of the list A.

In [10]:
A = [12, 34, 89, 17, 23, 49, 87, 34, 89, 71]

for i in range(0, len(A)):
    square_message(A[i])
The square of 12 is 144.
The square of 34 is 1156.
The square of 89 is 7921.
The square of 17 is 289.
The square of 23 is 529.
The square of 49 is 2401.
The square of 87 is 7569.
The square of 34 is 1156.
The square of 89 is 7921.
The square of 71 is 5041.

Return Values

All of the function we have written so far have printed some sort of message. However, we will not always desire this sort of behavior. Most of the pre-defined functions we have seen in this course print nothing at all, but instead provide some sort of output that can be printed, or stored into a variable.

For example, the sum() function takes a list as an input, and provides the sum of the values in the list as the output. When a function provides an output, that output is called a return value.

The syntax for defining a function with a return value is as follows:

def my_function(parameter1, parameter 2, ...):
    Code to be executed.
    return value_to_be_returned

As a simple example, the cell below defines a function called half() that accepts a single input, and returns the input divided by 2.

In [11]:
def half(x):
    return x/2
In [12]:
y = half(36)
print(y)
18.0
In [13]:
half(9) + half(14)
Out[13]:
11.5
In [14]:
print(half(100))
print(half(35))
print(half(3.14159))
50.0
17.5
1.570795

We can nest function calls inside of each other. In this way, the return value for one function call becomes the argument for another.

In [15]:
one_eighth = half(half(half(20)))
print(one_eighth)
2.5

Example: Summing the First n Positive Integers

In the following cell, we write a function called sum_first() that takes a single parameter n, and returns the sum of the first n positive integers.

In [16]:
def sum_first(n):
    
    total = 0
    
    for i in range(1,n+1):
        total = total + i
        
    return total

We now call sum_first() on 100, and also on 237, printing the return value for each function call.

In [17]:
print(sum_first(100))
print(sum_first(237))
5050
28203

Example: Factorials

We will now write a function called fact() that takes in one argument, assumed to be an integer, and returns the factorial of that integer.

In [18]:
def fact(n):
    
    product = 1
    
    for i in range(1,n+1):
        product = product * i
        
    return product

We will use our function calculate the factorials of 3, 5, 10, and 20.

In [19]:
print(fact(3))
print(fact(5))
print(fact(10))
print(fact(20))
6
120
3628800
2432902008176640000

Functions with Multiple Parameters

It is possible for functions to have two or more parameters. Consider the following example.

In [20]:
def power(a, b):
    return a**b
In [21]:
print( power(2, 5) )
print( power(3, 4) )
print( power(10, 2) )
print( power(2, 10) )
32
81
100
1024

Notice that when we call a function with multiple arguments, the arguments are assigned to the parameters in the same order as they appear. If we specify names for the parameters when we call a function, then we can supply the arguments in any order that we wish.

In [22]:
print(power(a=4, b=3))
print(power(b=3, a=4))
64
64

Supplying names for our parameters can make our code easier to read when we are using functions with many parameters. It can also be useful when we can't recall the exact order in which the parameters appeared in the function definition.

Default Parameter Values

It is sometimes useful to assign a default value to a parameter. This can be done by setting the parameter equal to the desired default value in the function definition. When the function is called, if an argument is supplied for this parameter, it will override the default value. If no argument is supplied for a parameter with a default value, then the default will be used.

In [23]:
def sum_first(arg_list, n=5):
    total = 0
    for i in range(n):
        total += arg_list[i]
    return total
In [24]:
my_list = [4, 8, 5, 7, 4, 9, 1, 6, 3, 2]

print(sum_first(my_list, 3))
print(sum_first(my_list, 5))
print(sum_first(my_list))
17
28
28

If the argument supplied for n in the sum_first() function is greater than the length of the list, then the function will result in an error. We will now rewrite this function so that it instead retuns the sum of all of the elements in the list in this situation.

In [25]:
def sum_first(arg_list, n=5):
    
    total = 0
    m = min(n, len(arg_list))
    
    for i in range(m):
        total += arg_list[i]
        
    return total
In [26]:
print(sum_first(my_list, 15))
49

Example: sum_power Function

In the cell below, we create a function called sum_power. The function has two parameters, x and n. The parameter x is intended to be a list. The parameter n should be an int with a default value of 1. The function should raise each element to the power of n, sum the resulting values, and then return this sum.

In [27]:
def sum_power(x, n=1):
    total = 0
    
    for i in range(0, len(x)):
        total += x[i]**n
        
    return total 

We test our function in the cell below.

In [28]:
A = [4,-3,1]
print(sum_power(A))
print(sum_power(A, 2))
print(sum_power(A, 3))
print(sum_power(A, 7))
2
26
38
14198

Example: find_item Function

In the cell below, we define a function called find_item that has three parameters, x, item, and first. The parameter x is expected to be a list. The parameter first should have a default value of True. The function should behave as follows:

  • If first == True, then the function should search for the first time that item appears in x, and should return the index of that occurrence.
  • If first == False, then the function should search for the last time that item appears in x, and should return the index of that occurrence.
  • If item does not appear in x, then the function should return None.
In [29]:
def find_item(x, item, first=True):
    idx = None
    for i in range(0, len(x)):
        if x[i] == item:
            idx = i
            if first:
                return idx
    return idx

After defining your function, use the following lines of code to test it.

In [30]:
letter_list = ['B', 'C', 'A', 'D', 'A', 'C', 'B', 'A', 'D']

print(find_item(letter_list, 'A'))
print(find_item(letter_list, 'A', first=False))
print(find_item(letter_list, 'D'))
print(find_item(letter_list, 'D', first=False))
print(find_item(letter_list, 'E'))
print(find_item(letter_list, 'E', first=False))
2
7
3
8
None
None

Returning Multiple Values

It is possible for a Python function to return multiple values. To To accomplish this, we can combine the desired return values together into a list or a tuple, which we then return. Alternately, we can simply list the return values in the return statement, separated by commas.

When storing the values returned by such a function in variables, we can list the target variable names on the left of the assignment operator, separated by commas.

In [31]:
def power(a,b):
    return (a**b, b**a)

a_to_b, b_to_a = power(2,5)
print(a_to_b)
print(b_to_a)
32
25

Example: Division with Remainder

Write a function div_w_remainder() that takes two arguments, num and div, and returns the number of times that div evenly divides num (i.e. the quotient), as well as the remainder of num after division by div. Coerce the remainder into an integer.

In [32]:
def div_w_remainder(num, div):
    
    quotient = int(num/div)
    remainder = num % div
    
    return (quotient, remainder)

We will now consider several examples to test our function.

In [44]:
q, r = div_w_remainder(17, 3)
print('Quotient: ', q)
print('Remainder:', r)
Quotient:  5
Remainder: 2
In [43]:
q, r = div_w_remainder(459, 17)
print('Quotient: ', q)
print('Remainder:', r)
Quotient:  27
Remainder: 0
In [42]:
q, r = div_w_remainder(6237, 13)
print('Quotient: ', q)
print('Remainder:', r)
Quotient:  479
Remainder: 10

Example: Locating Elements in a List

Write a function called locate. The function should take two arguments: a list called x, and another variable called item.

The function should return two values: A list of indices at which the element in x is equal to item, and a count of the number of times that item appears in x.

In [36]:
def locate(x, item):
   
    index_list = []

    for i in range(0, len(x)):
        if x[i] == item:
            index_list.append(i)

    return (index_list, len(index_list))

A list of student grades is provided in the cell below. Call locate() five times. In each function call, pass in grades for x. For item, use each of the following values: 'A', 'B', 'C', 'D', and 'F'.

For each function call, print out a message of the following form:

A: indices = [......], count = ##
In [37]:
grades = ['A', 'D', 'A', 'C', 'B', 'F', 'A', 'D', 'C', 'B', 'F', 'A', 'C', 
          'B', 'A', 'B', 'B', 'C', 'B', 'F', 'D', 'D', 'A', 'C', 'B']

for letter in ['A', 'B', 'C', 'D', 'F']:
    idx, c = locate(grades, letter)
    print(str(letter) + ': indices = ' + str(idx) + ', count = ' + str(c) )
A: indices = [0, 2, 6, 11, 14, 22], count = 6
B: indices = [4, 9, 13, 15, 16, 18, 24], count = 7
C: indices = [3, 8, 12, 17, 23], count = 5
D: indices = [1, 7, 20, 21], count = 4
F: indices = [5, 10, 19], count = 3

Multiple Return Statements

It is possible for a function to have multiple return statements. As soon as a return statement is hit, however, the function returns that value and then exits. As an illustration of this idea, compare the following two functions, both of which return the absolute value of a number.

In [38]:
def abs_val_1(x):
    
    if (x < 0):
        absVal = -x
    else:
        absVal = x
        
    return absVal
In [39]:
print(abs_val_1(9))
print(abs_val_1(-3.8))
9
3.8
In [40]:
def abs_val_2(x):
    if(x < 0):
        return -x
    return x
In [41]:
print(abs_val_2(9))
print(abs_val_2(-3.8))
9
3.8