Writing Functional Python

Functional programming has a lot of advantages that have been enumerated over the years. Being a bit of a Pythonista, myself, I can’t help but introduce functional concepts into my Python code. Though Python is not a functional language, it doesn’t mean that your Python code can’t benefit from some of these concepts.

Perhaps one of my favorite illustrations of functional Python comes in the treatment of loops — or more accurately, their removal from my code. Before I demonstrate how to do that, allow me to introduce a very contrived example.

Let’s say you’ve parsed a configuration file into a list of lists, and you need to be able to access a particular settings by keyword. The following snippet does exactly that in a traditional format.

configurations = [["name","Bob"], ["email","[email protected]"], ["template","dark"]]

def get_config_value(desired_key, configs):
    for key_value in configs:
        if key_value[0] == desired_key:
            return key_value[1]
print get_config_value("name", configurations)

This code works well, but I find the nested statements less than ideal.

Functional Alternative #1 – Generators

Generators simply create iterators (i.e., something you can use in a loop) without constructing a complete list ahead of time. Generators are lazy loaded and are more efficient than their eager-loading cousin, the list comprehension, when working with large data sets.

The following returns the same result as the previous example with one less layer of nesting:

def get_config_value(desired_key, configs):
    for value in (config[1] for config in configs if config[0] == desired_key):
        return value

print get_config_value("name", configurations)

I often use generator expressions when I’m only working with a single, simple list. They’re built-in, simple to create, and, more importantly, Pythonic.

Functional Alternative #2 – Map and Filter

But when my code is a bit more involved and I find I’m working with a number of lists with more complexity, generator expressions can become somewhat unreadable. To combat this, I can turn to the imap and ifilter functions in the itertools module (in Python 3, the built in map and filter functions are equivalent).

Though without using an abstraction, as the next example illustrates, they really don’t improve readability.

import itertools

def get_config_value(desired_key, configs):
    for value in itertools.imap(lambda item:item[1],
        itertools.ifilter(lambda item:item[0]==desired_key, configs)):
        return value

print get_config_value("name", configurations)

Functional Alternative #3 – IterHelper

To address this, let’s create the IterHelper class.

import itertools

class IterHelper(object):
    def __init__(self, iterable = None):
        self.iterable = iterable

    def map(self, func):
        return IterHelper(itertools.imap(func, self.iterable))

    def filter(self, predicate):
        return IterHelper(itertools.ifilter(predicate, self.iterable))

    def first(self):
        for item in self.iterable:
            return item

That abstraction allows me to write the following cleaner code.

def get_config_value(desired_key, configs):
    return IterHelper(configs)\
        .filter(lambda item:item[0] == desired_key)\
        .map(lambda item:item[1])\
        .first()

print get_config_value("name", configurations)

Of course, if you don’t want to write all of that extra code, you could simply use the ASQ module, a port of Microsoft’s LINQ. However, it’s useful to be able to create you own version since you can both understand what is happening in the background and define a grammar tailored to your specific needs.

However you choose to handle iterating, you should keep in mind that there are options when your code is no longer looking quite as Pythonic as you would like to be.

Happy coding!

Conversation
  • chris czub says:

    great post!

    looks like there’s an indentation error in the first example — i assume the ‘return’ statement was intended to be indented.

  • Ben Rousch says:

    You should hang out with the Grand Rapids Python Users Group some time! http://www.meetup.com/grpython/

  • Ben Rousch says:

    Ah yeah, I forgot that Atoms are not all in GR these days.

  • Comments are closed.