Functional Programming in Python

Introduction

Functional Programming is a popular programming paradigm closely linked to computer science's mathematical foundations. While there is no strict definition of what constitutes a functional language, we consider them to be languages that use functions to transform data.

Python is not a functional programming language but it does incorporate some of its concepts alongside other programming paradigms. With Python, it's easy to write code in a functional style, which may provide the best solution for the task at hand.

Functional Programming Concepts

Functional languages are declarative languages, they tell the computer what result they want. This is usually contrasted with imperative languages that tell the computer what steps to take to solve a problem. Python is usually coded in an imperative way but can use the declarative style if necessary.

Some of Python's features were influenced by Haskell, a purely functional programming language. To get a better appreciation of what a functional language is, let's look at features in Haskell that can be seen as desirable, functional traits:

  • Pure Functions - do not have side effects, that is, they do not change the state of the program. Given the same input, a pure function will always produce the same output.
  • Immutability - data cannot be changed after it is created. Take for example creating a List with 3 items and storing it in a variable my_list. If my_list is immutable, you wouldn't be able to change the individual items. You would have to set my_list to a new List if you'd like to use different values.
  • Higher Order Functions - functions can accept other functions as parameters and functions can return new functions as output. This allows us to abstract over actions, giving us flexibility in our code's behavior.

Haskell has also influenced iterators and generators in Python through its lazy loading, but that feature isn't necessary for a functional language.

Functional Programming in Python

Without any special Python features or libraries, we can start coding in a more functional way.

Pure Functions

If you'd like functions to be pure, then do not change the value of the input or any data that exists outside the function's scope.

This makes the function we write much easier to test. As it does not change the state of any variable, we are guaranteed to get the same output every time we run the function with the same input.

Let's create a pure function to multiply numbers by 2:

def multiply_2_pure(numbers):  
    new_numbers = []
    for n in numbers:
        new_numbers.append(n * 2)
    return new_numbers

original_numbers = [1, 3, 5, 10]  
changed_numbers = multiply_2_pure(original_numbers)  
print(original_numbers) # [1, 3, 5, 10]  
print(changed_numbers)  # [2, 6, 10, 20]  

The original list of numbers are unchanged, and we don't reference any other variables outside of the function, so it is pure.

Immutability

Ever had a bug where you wondered how a variable you set to 25 became None? If that variable was immutable, the error would have been thrown where the variable was being changed, not where the changed value already affected the software - the root cause of the bug can be found earlier.

Python offers some immutable data types, a popular one being the Tuple. Let's contrast the Tuple to a List, which is mutable:

mutable_collection = ['Tim', 10, [4, 5]]  
immutable_collection = ('Tim', 10, [4, 5])

# Reading from data types are essentially the same:
print(mutable_collection[2])    # [4, 5]  
print(immutable_collection[2])  # [4, 5]

# Let's change the 2nd value from 10 to 15
mutable_collection[1] = 15

# This fails with the tuple
immutable_collection[1] = 15  

The error you would see is: TypeError: 'tuple' object does not support item assignment.

Now, there's an interesting scenario where a Tuple may appear to be a mutable object. For instance, if we wanted to change the list in immutable_collection from [4, 5] to [4, 5, 6], you can do the following:

immutable_collection[2].append(6)  
print(immutable_collection[2])  # [4, 5, 6]  

This works because a List is a mutable object. Let's try to change the list back to [4, 5].

immutable_collection[2] = [4, 5]  
# This throws a familiar error:
# TypeError: 'tuple' object does not support item assignment

It fails just as we expected it to. While we can change the contents of a mutable object in a Tuple, we cannot change the reference to the mutable object that's stored in memory.

Higher Order Functions

Recall that Higher Order Functions either accept a function as an argument or return a function for further processing. Let's illustrate how simple both can be created in Python.

Consider a function that prints a line multiple times:

def write_repeat(message, n):  
    for i in range(n):
        print(message)

write_repeat('Hello', 5)  

What if we wanted to write to a file 5 times, or log the message 5 times? Instead of writing 3 different functions that all loop, we can write 1 Higher Order Function that accepts those functions as an argument:

def hof_write_repeat(message, n, action):  
    for i in range(n):
        action(message)

hof_write_repeat('Hello', 5, print)

# Import the logging library
import logging  
# Log the output as an error instead
hof_write_repeat('Hello', 5, logging.error)  

Now imagine that we're tasked with creating functions that increment numbers in a list by 2, 5, and 10. Let's start with the first case:

def add2(numbers):  
    new_numbers = []
    for n in numbers:
        new_numbers.append(n + 2)
    return new_numbers

print(add2([23, 88])) # [25, 90]  

While it's trivial to write add5 and add10 functions, it's obvious that they would operate in the same: looping through the list and adding the incrementer. So instead of creating many different increment functions, we create 1 Higher Order Function:

def hof_add(increment):  
    # Create a function that loops and adds the increment
    def add_increment(numbers):
        new_numbers = []
        for n in numbers:
            new_numbers.append(n + increment)
        return new_numbers
    # We return the function as we do any other value
    return add_increment

add5 = hof_add(5)  
print(add5([23, 88]))   # [28, 93]  
add10 = hof_add(10)  
print(add10([23, 88]))  # [33, 98]  

Higher Order Functions give our code flexibility. By abstracting what functions are applied or returned, we gain more control of our program's behavior.

Python provides some useful built-in Higher Order Functions, which makes working with sequences much easier. We'll first look at lambda expressions to better utilize these built-in functions.

Lambda Expressions

A lambda expression is an anonymous function. When we create functions in Python, we use the def keyword and give it a name. Lambda expressions allow us to define a function much more quickly.

Let's create a Higher Order Function hof_product that returns a function that multiplies a number by a predefined value:

def hof_product(multiplier):  
    return lambda x: x * multiplier

mult6 = hof_product(6)  
print(mult6(6)) # 36  

The lambda expression begins with the keyword lambda followed by the function arguments. After the colon is the code returned by the lambda. This ability to create functions "on the go" is heavily used when working with Higher Order Functions.

There's a lot more to lambda expressions that we cover in our article Lambda Functions in Python if you want more info.

Built-in Higher Order Functions

Python has implemented some commonly used Higher Order Functions from Functional Programming Languages that makes processing iterable objects like lists and iterators much easier. For space/memory efficiency reasons, these functions return an iterator instead of a list.

Map

The map function allows us to apply a function to every element in an iterable object. For example, if we had a list of names and wanted to append a greeting to the Strings, we can do the following:

names = ['Shivani', 'Jason', 'Yusef', 'Sakura']  
greeted_names = map(lambda x: 'Hi ' + x, names)

# This prints something similar to: <map object at 0x10ed93cc0>
print(greeted_names)  
# Recall, that map returns an iterator 

# We can print all names in a for loop
for name in greeted_names:  
    print(name)

Filter

The filter function tests every element in an iterable object with a function that returns either True or False, only keeping those which evaluates to True. If we had a list of numbers and wanted to keep those that are divisible by 5 we can do the following:

numbers = [13, 4, 18, 35]  
div_by_5 = filter(lambda num: num % 5 == 0, numbers)

# We can convert the iterator into a list
print(list(div_by_5)) # [35]  

Combining map and filter

As each function returns an iterator, and they both accept iterable objects, we can use them together for some really expressive data manipulations!

# Let's arbitrarily get the all numbers divisible by 3 between 1 and 20 and cube them
arbitrary_numbers = map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))

print(list(arbitrary_numbers)) # [27, 216, 729, 1728, 3375, 5832]  

The expression in arbitrary_numbers can be broken down to 3 parts:

  • range(1, 21) is an iterable object representing numbers from 1, 2, 3, 4... 19, 20.
  • filter(lambda num: num % 3 == 0, range(1, 21)) is an iterator for the number sequence 3, 6, 9, 12, 15 and 18.
  • When they're cubed by the map expression we can get an iterator for the number sequence 27, 216, 729, 1728, 3375 and 5832.

List Comprehensions

A popular Python feature that appears prominently in Functional Programming Languages is list comprehensions. Like the map and filter functions, list comprehensions allow us to modify data in a concise, expressive way.

Let's try our previous examples with map and filter with list comprehensions instead:

# Recall
names = ['Shivani', 'Jan', 'Yusef', 'Sakura']  
# Instead of: map(lambda x: 'Hi ' + x, names), we can do
greeted_names = ['Hi ' + name for name in names]

print(greeted_names) # ['Hi Shivani', 'Hi Jason', 'Hi Yusef', 'Hi Sakura']  

A basic list comprehensions follows this format: [result for singular-element in list-name].

If we'd like to filter objects, then we need to use the if keyword:

# Recall
numbers = [13, 4, 18, 35]  
# Instead of: filter(lambda num: num % 5 == 0, numbers), we can do
div_by_5 = [num for num in numbers if num % 5 == 0]

print(div_by_5) # [35]

# We can manage the combined case as well:
# Instead of: 
# map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))
arbitrary_numbers = [num ** 3 for num in range(1, 21) if num % 3 == 0]  
print(arbitrary_numbers) # [27, 216, 729, 1728, 3375, 5832]  

Every map and filter expression can be expressed as a list comprehension.

Some Things to Consider

It's well known that the creator of Python, Guido van Rossum, did not intend for Python to have functional features but did appreciate some of the benefits its introduction has brought to the language. He discussed the history of Functional Programming language features in one of his blog posts. As a result, the language implementations have not been optimized for Functional Programming features.

Furthermore, the Python developer community does not encourage using the vast array of Functional Programming features. If you were writing code for the global Python community to review, you would write list comprehensions instead of using map or filter. Lambdas would be used minimally as you would name your functions.

In your Python interpreter, enter import this and you will see "The Zen of Python". Python generally encourages code to be written in the most obvious way possible. Ideally, all code should be written in one way - the community doesn't think it should be in a Functional style.

Conclusion

Functional Programming is a programming paradigm with software primarily composed of functions processing data throughout its execution. Although there's not one singular definition of what is Functional Programming, we were able to examine some prominent features in Functional Languages: Pure Functions, Immutability, and Higher Order Functions.

Python allows us to code in a functional, declarative style. It even has support for many common functional features like Lambda Expressions and the map and filter functions.

However, the Python community does not consider the use of Functional Programming techniques best practice at all times. Even so, we've learned new ways to solve problems and if needed we can solve problems leveraging the expressivity of Functional Programming.

Author image
Trinidad and Tobago Twitter Github
Web Dev|Games|Music|Art|Fun|Caribbean I love many things and coding is one of them!