Functions as parameters

In this article we take a look at functional abstractions using filter and sorted as examples.

In the previous article, we saw how Python has first class functions. In this article we will see some examples of how this property can be used.

Let us say that we have a list of numbers and we want to find all the even numbers in the list. We might write that code like this

a = [1, 2, 3, 4, 5]
out = []
for num in a:
    if num % 2 == 0:
        out.append(num)

or if you prefer list comprehensions,

out = [num for num in a if num % 2 == 0]

Now this works pretty well, and later on you find that you need to filter out all the numbers divisible by five, so we get this code

out = []
for num in a:
    if num % 5 == 0:
        out.append(num)

At this point you look at both pieces of code, and see that they look similar. Why not write a function that can take a list and a number and give all the values divisible by that number? So we get this function

def divisible_by(numbers, divisor):
    return [num for num in numbers if num % divisor == 0]

Great! We have generalised the code and we can now pass in any divisor and do the operation.

But there is a problem. Later on you want to find all the odd numbers. The condition for that is num % divisor == 1. You look at the divisble_by function and see that there is no way to use it. And then you want positive numbers, and negative numbers and you realise we need to create separate functions for each of these. We cannot just change the data value in the function, instead the actual code for the condition is itself different.

Building a filtering abstraction

At this point we are understanding our abstraction a little deeper. What we really want is something that can select numbers from the list which match any given condition: even numbers, positive numbers, prime numbers, whatever. We might even want to select certain names from a list of strings, or filter a list of objects.

To do this, we need to separate the concept of "filtering" and the "condition". We want to create a filtering function that can implement the concept of filtering, but it should be able to take the condition as a parameter, so that we can pass in whatever condition we want.

The condition is actually a piece of code, so we will put that code into a function, and pass in the function as a parameter.

When we do that, we get something like this

def is_even(num):
    return num % 2 == 0
    
def filter(sequence, condition):
	return [item for item in sequence if condition(item)]
    
filter([1, 2, 3, 4, 5], is_even) # [2, 4]

Here, we pass the condition function as a parameter to filter. filter will call that function for every item in the sequence and if the condition function returns True for the item then the item will be included in the output, otherwise excluded. (Such a function that returns True or False is called a predicate)

You will see here that by putting the condition in a function and passing it as a parameter, it enables us to create any kind of condition that we want. And the filter has also become generic, it abstracts the logic of looping the sequence, testing each item, creating the output list.

Having function as a first class citizen allows us to create abstractions for the structure of the code. In the case of filter the outer looping and filtering structure can be made into a separate function, even though some code in the middle (the condition) needs to be different.

You don't even need to create a separate named function as such. If the condition is just a single expression then you can simply use a lambda to create the function in memory without a name and pass that object into filter.

filter([1, 2, 3, 4, 5], lambda num: num > 2) # [3, 4, 5]

Once you start looking, you see this in many places in python. Some related functions to talk about here are map and reduce, but I will touch upon these two in a later article in this series.

Instead, I want to turn to the sorted function. sorted is used to sort a sequence. Here I have a list of three names, and sorted will sort them alphabetically.

names = ["Siddharta", "Anjali", "Latha"]
sorted(names) # ['Anjali', 'Latha', 'Siddharta']

What if I want to sort them some other way? Perhaps I want to sort based on the length of the name, so Latha will come first, followed by Anjali and then Siddharta.

How can we do this? Here sorted provides a key parameter where we can pass a function that calculates how the sort should be done. This is how we can sort by length

sorted(names, key=lambda name: len(name)) # ['Latha', 'Anjali', 'Siddharta']

sorted implements the abstraction of sorting. It has all the steps required to do the Timsort algorithm. By taking in a key function, the actual condition based on which the items are ordered can be customised. (PS: In case you have never heard of Timsort, thats the beauty of abstractions. You don't need to know how the abstraction is implemented in order to use it)

Summary

In this article, we discussed some of the things that we can do when the language allows us to pass in a function as a parameter to another function. It allows us to create an abstraction for a pattern or structure of code, even when a some part of the code will be different. In the next article, we will talk about functions that create and return new functions.