Functional Programming Paradigm in Python

An Overview of Python's Higher Order Functions, Anonymous Lambda Functions and the Best Packages for Functional Programming Style in Python

Functional Programming Paradigm in Python
Photo by Chris Ried / Unsplash

Python is a multi-paradigm programming language, with functional programming being one option, and functions in Python are treated as first-class objects.

In this article, we will explore the applications of treating functions as first-class objects and we will start by defining what is a first class object.

Functions as First-Class Objects

We can define a first class object as a program entity that can be:

  • Created at Runtime.
  • Passed as an argument to a method or a function.
  • Returned as a result of a function or a method.
  • Assigned to a variable.

This is the description of any object in Object Oriented Programming, and Python treats functions as objects out of the box.

def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

print(type(factorial))
# <class 'function'>

As we can see, Python identifies the function as an Object, which means we can do this:

def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

fact = factorial
print(fact(5))
# 123

print(list(map(factorial, range(11))))
# [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

Python’s Higher-Order Functions

A high order function is a Function that takes another Function as an argument or returns a Function, like Python’s Built in function sorted.

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=len))
# ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

Here, we pass the len function as an argument to the sorted function.

Some of the best higher order functions are the famous map, filter and reduce functions, but list comprehension and generator expression have replaced them since they can do the job with easier and more compact syntax.

print(list(map(factorial, range(6))))
# [1, 1, 2, 6, 24, 120]

print([factorial(n) for n in range(6)])
# [1, 1, 2, 6, 24, 120]

print(list(map(factorial, filter(lambda n: n % 2, range(6)))))
# [1, 6, 120]

print([factorial(n) for n in range(6) if n % 2])
# [1, 6, 120]

How to be anonymous? Introducing the Lambda function

The Lambda Function is an anonymous function within a Python expression created by a Lambda expression.

Lambda Functions cannot be too complicated, since they usually are one liners and used in an argument to another function. If there ever is a need to use a while loop or a try-catch block, then we're better off using a regular function.

# Sorting a list of words by their reversed spelling using lambda
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']

print((fruits, key=lambda word: word[::-1]))
# ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

Generally, if a piece of code is hard to understand because of a lambda, the best solution is to refactor following these steps:

  • Document the lambda function to explain what it does.
  • If the comment does not explain enough, think of a name for the lambda function.
  • Convert the lambda to a regular function with a def statement

Positional and Keyword Parameters

Python is extremely flexible when handling parameters, it offers a mechanism similar to the unpacking of different types of iterables and mappings, from the lists with * operator to dictionaries with ** operator.

Positional Parameters

Positional arguments or parameters are values that are passed into a function and recognized by the order in which the parameters were listed during the function definition.

To define a function requiring positional-only parameters, we use “/” in the parameter list. For example divmod is built-in function that can only be called by positional arguments such as divmod(a, b) and not as divmod(a=10, b=4).

All the arguments to the left of “/” are positional, any arguments on the right are not and can be defined as keyword arguments or simply non keyworded/positional arguments.

def team(name, /, project=None):
    print(name, "is working on project", project)
   
team("Neo", project="Matrix")
# Neo is working on project Matrix


team(project="Matrix")
# TypeError: team() missing 1 required positional argument: 'name'

Keyword Parameters

Keyword parameters or arguments are values that are passed into a function and recognized by their names, that is why they are also called Named arguments.

To define a function requiring Keyword-only arguments, we use the “*” in the parameters list, the reason for this is allow for keyword-only arguments after the “*” parameter.

All the arguments to the right of “*” are keyword only, any arguments on the left are not and can be defined as keyword arguments or simply non keyworded/positional arguments.

def team(name, *, project=None):
    print(name, "is working on project", project)
   
team("Neo", project="Matrix")
# Neo is working on project Matrix


team("Neo")
# TypeError: team() missing 1 required keyword-only argument: 'project'

Here’s another example taken from the seventh chapter of the book Fluent Python and altered by me to showcase what we just discussed.

def tag(name, /, *content, class_=None, **attrs):
    if class_ is not None:
        attrs['class'] = class_
    attr_pairs = (f' {attr}="{value}"' for attr, value in
                 sorted(attrs.items()))
    attr_str = ''.join(attr_pairs)
    if content:
        elements = (f'<{name}{attr_str}>{c}</{name}>' for c in content)
        return '\n'.join(elements)
    else:
        return f'<{name}{attr_str} />'

#A single positional argument produces an empty tag with that name
print(tag('br'))
# '<br />'

# Any number of arguments after the first are captured by *content as a #tuple
print(tag('p', 'hello'))
# '<p>hello</p>'

print(tag('p', 'hello', 'world'))
# <p>hello</p>
# <p>world</p>

#Keyword arguments not explicitly named in the tag signature are captured #by **attrs as a dict.

print(tag('p', 'hello', id=33))
#'<p id="33">hello</p>

#The class_ parameter can only be passed as a keyword argument
print(tag('p', 'hello', 'world', class_='sidebar'))
# <p class="sidebar">hello</p>
# <p class="sidebar">world</p>

my_tag = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'class': 'framed'}

# Prefixing the my_tag dict with ** passes all its items as separate  #arguments which are then bound to the named parameters, with the remaining #caught by **attrs
print(tag('img', **my_tag))
# '<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'

Best Packages for Functional Programming Style in Python

Taking advantage of Python’s first class Functions, pattern matching and the support of certain packages, we can write code functionally and benefit from its simplicity.

Operator and functools Packages

These two packages allow us to manipulate sequences concisely by making use of functions like reduce, mul, itemgetter and attrgetter.

from functools import reduce
def factorial(n):
    return reduce(lambda a, b: a*b, range(1, n+1))

Now the Operator package replaces a lot of Lambda operator functions by providing their equivalents, so we can write this:

from functools import reduce
from operator import mul

def factorial(n):
    return reduce(mul, range(1, n+1)

Another common use for the operator package is to read items from sequences and objects.

from operator import itemgetter

metro_data = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ]

cc_name = itemgetter(1, 0)
for city in metro_data:
    print(cc_name(city))

# ('JP', 'Tokyo')
# ('US', 'New York-Newark')

Another useful feature provided by the functools package is freezing arguments in a function call with functools.partial.

Given a callable, partial will produce a new callable with the arguments you choose, some of them are bound to the original arguments and some are predetermined values.

from operator import mul
from functools import partial

triple = partial(mul, 3)
print(triple(7))
# 21

print(list(map(triple, range(1, 10))))
# [3, 6, 9, 12, 15, 18, 21, 24, 27]

Conclusion

We saw here how can Python be efficient when coding in functional style, this has been made possible by the higher order functions, callables and the operator and functools packages.

Further Reading