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.
1 2 3 4 5 6 7
configurations = [["name","Bob"], ["email","firstname.lastname@example.org"], ["template","dark"]] def get_config_value(desired_key, configs): for key_value in configs: if key_value == desired_key: return key_value 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:
1 2 3 4 5
def get_config_value(desired_key, configs): for value in (config for config in configs if config == 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
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.
1 2 3 4 5 6 7 8
import itertools def get_config_value(desired_key, configs): for value in itertools.imap(lambda item:item, itertools.ifilter(lambda item:item==desired_key, configs)): return value print get_config_value("name", configurations)
Functional Alternative #3 – IterHelper
To address this, let’s create the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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.
1 2 3 4 5 6 7
def get_config_value(desired_key, configs): return IterHelper(configs)\ .filter(lambda item:item == desired_key)\ .map(lambda item:item)\ .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.