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.
Did you like this article?
If you liked this article, consider subscribing to this site. Subscribing is free.
Why subscribe? Here are three reasons:
- You will get every new article as an email in your inbox, so you never miss an article
- You will be able to comment on all the posts, ask questions, etc
- Once in a while, I will be posting conference talk slides, longer form articles (such as this one), and other content as subscriber-only