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!
great post!
looks like there’s an indentation error in the first example — i assume the ‘return’ statement was intended to be indented.
Thanks for the catch, Chris!
You should hang out with the Grand Rapids Python Users Group some time! http://www.meetup.com/grpython/
Thank you for the invite, Ben. I’ll try to make sure my next trip to GR coincides with GRPython.
Ah yeah, I forgot that Atoms are not all in GR these days.