Help support the author by donating or purchasing a copy of the book (not available yet)



Chapter 12 - Functions & Modules

12.1 - What are functions?

In Python, a function is some code that takes input, performs some computation and produces an output. In programming, a function is a reusable block of code which performs a specific task.

We can call a function multiple times during a programs execution. We have done this many times throughout this book already.

Some of the common functions we have met are len() and print(). When we want to know the length of something we pass that something to the len() function for example.

When we want to print something to the terminal window we call the print() function.

We have also met methods. Methods are functions that are associated with an object. For example the format() method operates on strings. Methods are implemented the same way as functions and are essentially functions that operate on associated objects but we'll get back to those later.

12.2 - Defining our own functions

Most of the time, the Python built-in functions are not enough. Let's take our linear search algorithm for example. We may want to search a string multiple times throughout our program. Until now we would have had to write out the linear search algorithm over and over again. Programmers don't like to repeat themselves so if they find themselves repeating the same piece of code, they'll put that code into a function. This means, whenever we want to use that piece of code we can just call it like we do with print() for example.

We define our functions using the def keyword. Let's take a look at a function that prints "Hello, World!" to the screen.

def hello():
    print("Hello, World!")

Above we have defined, using def, a new function called hello(). When we call our function, the functions code block will execute, in this case we just print "Hello, World!" to the screen. This function can be used as follows within our code:

def hello():
    print("Hello, World!")

hello()

The last line above calls our function. This should be familiar as you've called other functions many times now!

The syntax for defining functions is:

def func_name(arg1, arg2, ...., argn):
    # function code
    return result

I'll explain what arg1, arg2, ..., argn is in the next section but let's take a look at the return statement.

In our first function we didn't get a result back, it simply printed something to the screen. Let's write a function that will return the text "Hello, world!" to us.

def hello():
    return "Hello, World!"

We can use this function as follows:

def hello():
    return "Hello, World!"

x = hello()
print(x)
# ALTERNATIVELY
print(hello())

In this function, the string Hello, World! is handed back to the function caller. Since the function returns a value, it's caller is expected to collect that value. Above we see the caller collects the returned value and stores it in x. You can see that the print() function also collects the value returned by the hello() function.

12.3 - Arguments & parameters

We've seen functions such as len() that take arguments such as a string or dictionary and return its length.

In the previous section we looked at the syntax for a function. In that, we had something like:

def myFunc(arg1, arg2, arg3):
#
#

The arg1, arg2, arg3 are called the functions parameters. A function parameter is like a variable that is local to the function thus cannot be referenced outside of that function. Another name of a parameter is a formal parameter. If our function has one formal parameter then we must pass a value to the function when we call it. This value is called an argument. Another name for an argument is an actual parameter.

Let's look at an example of a function that adds two numbers:

def add(x, y):
    result = x + y
    return result

print(add(5, 7))

In the above function, x and y in the function definition are called the parameters.

When we call the function, we pass 5 and 7 to the function. These are the arguments that we supplied to the function.

The variable result is also local to the function and cannot be referenced from outside the function.

Arguments and parameters are, by default, matched based on position. In this example, the first argument (5) is copied into the first parameter (x).... and so on.

Earlier on we looked at a function that printed hello world. Functions that do not return any values are called procedures.

A procedure changes the state of our program. For example, they may update values of variables, print something to the screen or update a file.

Functions that return a value inspect the state of our program. They return a value based on their inspection.

Remember the split() method? I said if we don't pass any arguments to this method then it will split a string based on whitespace. This is called a default argument.

A default argument is a parameter that assumes a default value if one is not provided when the caller invokes the function.

Below is an example of a function with default arguments:

def print_circle_info(r, x=0, y=0):
    print("(x, y) coordinates: {}, {}\nRadius: {}".format(x, y, r))

In the above function, we must pass a radius as an argument when calling it. We don't however have to pass an x or y coordinate. If we don't pass an x or y coordinate, our function defaults these values to 0.

If we do pass x and y coordinates then the values we pass will be assigned to the parameters x and y.

We can also define functions that have a variable number of arguments.

def my_function(*argv):
    for arg in argv:
        print(arg)

my_function("first", "second", "even a third")

The output from this calling this function would be:

first
second
even a third

In the above example, argv is some arbitrary name we chose as the parameter name. The * before hand indicates that it is a variable length parameter.

We also have keyword variable length parameters.

def my_function(**kwargs):
    for k, v in kwargs:
        print("{} : {}".format(k, v))
        
my_function(first="Hello", second="World")

The output would be as follows:

second : World
first : Hello

As you can probably tell, kwargs is a dictionary. We indicate that it is a keyword variable length parameter by the ** before the variable name.

Both normal and keyword variable length parameters must be put at the end of the parameter list when defining your functions parameters. There is good reason for this. If we had def func(x, y, *argv, z) we wouldn't know where *argv ended.

I'm going to say, be cautious when using variable length parameters. They should only be used when you have a good reason to use them (and that isn't very often).

12.4 - Variable Scope

Not all variables are accessible from all parts of your program. The part of a program where a variable is accessible is called it's scope. A variable which is defined in the main body of a file is called a global variable. It will be visible throughout the file and any file which imports that file.

Global variables can have unintended consequences because of their "range" (they can be accessed from anywhere). There are only very special circumstances in which we should use global variables in software we make in real life.

Variables which are defined inside code blocks are local to that block. A block is a construct that delimits the scope of any declaration within it. In Python, a variable defined inside a function is local to that function. It is accessible from when it is defined until the end of that function.

The formal parameters of a function act like local variables. However, assignments to a parameter can never affect the associated argument unless they are a mutable type, which you've seen in the previous section.

12. 5 - Default mutable argument trap

Consider this code and it's output:

def add_to_list(word, word_list=[]):
    word_list.append(word)
    return word_list

def main():
    word = "apple"
    tlist = add_to_list(word)
    print(tlist)
    word = "orange"
    tlist = add_to_list(word, ['pear'])
    print(tlist)
    word = "banana"
    tlist = add_to_list(word)
    print(tlist)
    
main()
$ py trap.py
['apple']
['pear', 'orange']
['apple', 'banana']

Let's look at why this is strange behaviour. We see that the second parameter to add_to_list() is optional (indicated by the default value []). If no argument for this parameter is supplied when calling the function then that parameter takes on the value of [], the empty list.

The first time we call the function and pass it apple, we get back ['apple']. This is fine and it's what we expected. The second time we call the function and pass it orange and a list of ['pear'], we get back the list ['pear', 'orange']. This is also fine and what we expected. The third time however, we get some unexpected behaviour.

Why is apple in the list if we called the function and passed banana and no list. Surely we should just get back ['banana']? This is called the default mutable argument trap and no, it's not a bug.

The empty list is initialised only once by Python. It is initialised when the def for that function is first encountered. This means the list has memory and anything added to it will stay there.

The takeaway from this is to not use mutable types as default arguments. Instead, work around it as follows:

def add_to_list(word, word_list=None):
    if word_list is None:
        word_list = []
    word_list.append(word)
    return word_list

def main():
    word = "apple"
    tlist = add_to_list(word)
    print(tlist)
    word = "orange"
    tlist = add_to_list(word, ['pear'])
    print(tlist)
    word = "banana"
    tlist = add_to_list(word)
    print(tlist)
    
main()

This will give:

['apple']
['pear', 'orange']
['banana']

And this is what we expected the first time.

None is a special type. It is an object that indicates no value and it is an immutable type. In fact, None is returned from a function if there is no return statement in that function i.e a procedure.

12.6 - What are modules?

I've briefly mentioned modules before and I said, a module is simply a file consisting of Python code. A module can define functions, classes and variables. A module can also contain runnable code.

Modules allow you to logically organize your Python code. Grouping code into modules makes it easier to understand and use your code. It is not uncommon for a software project to span hundreds of files.

You can import modules using the import statement which we have seen when importing sys and string.

We can import specific attributes from modules using the from keyword e.g. from string import punctuation. We can also import all attributes from a module using * e.g. from string import *.

When we import a module, the Python interpreter searches for the module in the following manner:

12.7 - Creating our own modules

We can create our own modules to logically organize our code. Let's say we want to create a module that contains some maths functions. We do this as follows:

# maths.py

def add(x, y):
    return x + y

def multiply(x, y)
    return x * y

def main():
    print(add(x, y))
    print(multiply(x, y))

if __name__=="__main__":
    main()

The add() and multiply() functions are fine and we have come across similar functions before. The main() function we have also come across in a previous section but the if __name__=="__main__":, we have not come across before.

When you execute a program directly from the command line (as you've been doing), the Python interpreter sets a special variable for that script. That variable is __name__. When a script is executed directly, that variable is set to "__main__", otherwise, if it is being imported for example, then it is set to the name of the module, "math" in this case.

We include the if statement to check this variable as sometimes we want to test our code or have some way of demoing our module. If we run it directly then the main() function is executed. Otherwise, if we import this module, then the main() function is not executed.

It is good practice to include this check in your scripts. This check is also usually located at the end of the script.

The way Python handles the __name__ variable has changed in Python 3.7. It is now under PEP 567. On the surface nothing has drastically changed.

12.8 - Exercises

When completing these exercises, use the following pattern when writing your scripts:

# functions and constants

def main():
    # Test code
    # Anything else that shouldn't happen when your code is imported

if __name__ == "__main__":
    main()

If you are asked to write module, then you should save the module in the same directory as your testing script.

Question 1

Write a module named sorting.py that contains functions that implement both selection and insertion sort i.e your module (sorting.py) should contain a selection_sort(a) function and an insertion_sort(a) function. Your function should return the values, not print them.

Your module should be imported and run as follows in another script called test.py:

import sorting

a = [5, 6, 3, 8, 7, 2]
print(sorting.selection_sort(a))
a = [5, 6, 3, 8, 7, 2]
print(sorting.insertion_sort(a))
$ py test.py
[2, 3, 5, 6, 7, 8]
[2, 3, 5, 6, 7, 8]

Question 2

Write a module called arithmetic.py. This module should include the following functions:

The add(), multiply() and subtract() functions should be able to handle an arbitrary number of arguments. divide() should only take 2 arguments. Your function should return the calculated values, not print them.

Your module should be imported and run as follows in another script called test.py

from arithmetic import *

print(add(4, 6, 7, 2, 3))
print(add(3, 2))

print(subtract(5, 6, 8, 9))
print(subtract(3, 2))

print(divide(6, 3))

print(multiply(2, 3))
print(multiply(2, 3, 3))
$ py test.py
22
5
-18
1
2
6
18

Question 3

Write a function called length() that mimics the len() function. Your function should work for strings, dictionaries and lists. length() should only take 1 argument and return the length of the data-structure passed to it.

Your function should be tested with the following:

a = [5, 3, 4, 1, 2, 3]
print(length(a))

a = []
print(length(a))

d = {}
print(length(d))

d = {"one": True, "two": True}
print(length(d))

s = "This is a string"
print(length(s))

s = ""
print(length(s))
$ py test.py
6
0
04+6+7+2+3
2
16
0

Question 4

Write a function called fib() that calculates and returns the n-th Fibonacci number. Your function should take 1 argument, the Fibonacci number you want to calculate.

Your function should be tested with the following:

print(fib(3))
print(fib(0))
print(fib(1))
print(fib(10))
print(fib(13))
$ py test.py
3
1
1
89
377

Remember:
fib(n)=fib(n − 1)+fib(n − 2)


fib(0)=1


fib(1)=1

Question 5

Write a function called read_file() which takes a single argument, a filename (as a string), and returns the contents of the file in list form with each element in the list being a single line from the file.

Your function should be called as follows:

lines = read_file("input.txt")

Question 6

Write a function procedure called rep_all() that takes 3 arguments, a list of integers and 2 numbers. Your procedure should replace all occurrences of the first number with the second.

Calling your function as follows and printing the list afterwards should produce the following:

a = [4, 2, 3, 3, 7, 8]
rep_all(a, 3, 10)
print(a)
$ py test.py
[4, 2, 10, 10, 7, 8]


Help support the author by donating or purchasing a copy of the book (not available yet)



Previous Chapter - Next Chapter