In this post we’ll see how to define and use functions. We’ll start with an example of a simple program and then see how to improve it using functions in Python. We’ll conclude with an analysis of the advantages we get by using functions in general.
Motivation
Let’s say we’re working on a program that prints all of the divisors for each number from 1 to 5. We could do it with the following code:
for n in range(1, 5 + 1): divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div) print('Number {} divisors are: {}'.format(n, divisors))
The output will be:
Number 1 divisors are: [1] Number 2 divisors are: [1, 2] Number 3 divisors are: [1, 3] Number 4 divisors are: [1, 2, 4] Number 5 divisors are: [1, 5]
Now, let’s say we want to do the same but for numbers in the range 11 – 15 as well. Since the task is very similar, we could just copy the code we had before, changing the input range:
for n in range(11, 15 + 1): divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div) print('Number {} divisors are: {}'.format(n, divisors))
But what if we need to do the same for dozens of different ranges of numbers? While we could just copy the initial code and change the ranges at each time, this task becomes increasingly tedious and error prone. What if we modify the code by mistake while copying it? Or what if we notice we had a bug on the original code?
Now imagine that we want to do the same but only for pair numbers of the input range. While it’s a minor modification, we will need to address the changes at each part of the copied code. Not to mention the possibility of creating new bugs and errors as we modify it.
We’ll see how functions will allow us to address all of these problems.
Creating functions: divide and conquer
One of the most important skills for programming is the ability to decompose one big problem into several smaller ones. It also has to do with being able to recognize patterns in the task at hand.
One sub-task of our initial problem is getting the divisors of a number. Given a value n
, the portion of code that took care of it was:
divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div)
Now that we have identified this small sub-task, we can encapsulate it with a function definition:
def get_divisors(n): divisors = [] for div in range(1, n + 1): if n % div == 0: divisors.append(div) return divisors
See that we have added a line with the keyword def
. This is done to define a function, which is given the name get_divisors
. The original code that we’ve encapsulated has been indented (following Python’s syntax). It forms the function’s body . Next to the function name, between parentheses, we have the function parameters. The parameters are variables (or data in general) that’s passed to the function in order to perform its operations. In this case the only parameter is n
.
The final line uses the return
keyword. A function is like a sealed box that works inside its own environment that is not accessible from outside the function. If we want to get some sort of output from it, we specify it with the return statement. It’s like specifying what result would we like to get out of the function. In this case, we’re returning the divisors
list that was defined inside the function’s body.
We could define a function in simple terms as a bunch of code to which we give a name.
Using functions
Once we have defined our function as we did above, we can call it as follows:
get_divisors(5) >>> [1, 5]
We’re calling our function get_divisors
with 5
as its argument. The function will take this argument, 5
, and substitute it into its body in place of its parameter n
. Basically, we’re asking Python to evaluate the get_divisors
body with n = 5
.
When the function reaches the return
statement, it will “give back” the stated variable. get_divisors
was made to return its variable divisors
, which corresponds to the list [1, 5]
when it’s called with argument 5
.
At this point we could include our function into our initial program:
for n in range(1, 5 + 1): divisors = get_divisors(n) print('Number {} divisors are: {}'.format(n, divisors))
This is already an improvement. As an extra step, let’s create another function that automates the whole process:
def print_range_divisors(start, end): for n in range(start, end + 1): divisors = get_divisors(n) print('Number {} divisors are: {}'.format(n, divisors))
See that print_range_divisors
doesn’t have any return
statement. The function will just reach the end of its body and quit. The only intended effect was to print
our results. This is known as a side-effects function. Another interesting feature is that we called the get_divisors
function inside the print_range_divisors
body.
We call the resulting function with:
print_range_divisors(1, 5)
Getting the same output as we had initially. Only this time using a single line!
Advantages
If the last example didn’t convince you of the advantages of using functions, we’ll review some of them now.
Re-usability
With functions, we create a bunch of code that can be easily reutilized. On the example above, we used it to print the divisors for numbers in the range 1 to 5, but we can use it for any other range of numbers.
Maintainability
If there’s a bug or if any modification is necessary, there are only a few lines of code to change.
Abstraction
The idea of abstraction is to “hide” unnecessary details. Take the get_divisors
function. Using the notion of abstraction, we could say that we don’t care about how it’s implemented. It gets the job done and that’s all. Just like we did when we called it from the print_range_divisors
function. Knowing the implementation details does not change it.
In this sense, you can consider a function as a “black box”. It’s a machine that takes an input and gives back some result. The details of how it does it, what’s inside this machine, are abstracted.
Modularity
The idea of modularity is to divide your program into pieces. And, as much as possible, to keep those pieces as separate independent parts. This means that a change in one piece will not need to change the other parts as well. This is modular design.
Readability
By separating the code into functions that perform specific and separate operations, our whole program is much more understandable. Without having to look at any of the function body’s, we can tell what they do by their names: get_divisors
, print_range_divisors
.
Always name your functions and variables with meaningful names!
In a future post, we will cover higher-order functions. Functions that include functions as parameters or that return functions.