I mostly use Python using object-oriented and procedural styles. Python is actually not particularly well-suited to functional programming.
A lot of people think they are writing functional Python code by using lots of lambda
, map
, filter
, and reduce
, but this is a bit over-simplified. The hallmark feature of functional programming is a lack of state or side effects. Important elements of a functional style are pure functions, recursive algorithms, and first class functions.
Here are my thoughts on functional programming and Python:
-
Pure functions are great. I do my best to make my module-level functions pure.
- Pure functions can be tested. Since they do not depend on outside state, they are much easier to test.
- Pure functions are able to support other optimizations, such as memoization and trivial parallelization.
-
Class-based programming can be pure. If you want an equivalent to pure functions using Python classes (which is sometimes but not always what you want),
- Make your instances immutable. In particular, this mainly means to make your methods always return new instances of your class rather than changing the current one.
- Use dependency injection rather than getting stuff (like imported module) from global scope.
- This might not always be exactly what you want.
-
Don’t try to avoid state all together. This isn’t a reasonable strategy in Python. For example, use
some_list.append(foo)
rather thannew_list = some_list + [foo]
, the former of which is more idiomatic and efficient. (Indeed, a ton of the “functional” solutions I see people use in Python are algorithmically suboptimal compared to just-as-simple or simpler solutions that are not functional or are just as functional but don’t use the functional-looking tools.) -
Learn the best lessons from functional programming, for example mutable state is dangerous. Ask yourself, Do I really want to change this X or do I want a new X?
-
One really common place this comes up is when processing a list. I would use
foo = [bar(item.baz()) for item in foo]
rather than
for index, _ in enumerate(foo): foo[index] = bar(foo[index].baz())
and stuff like it. This avoids confusing bugs where the same list object is stored elsewhere and shouldn’t be changed. (If it should be changed, then there is a decent chance you have a design error. Mutating some list you have referenced multiple places isn’t a great way to share state.)
-
-
Don’t use
map
and friends gratuitously. There is nothing more functional about doing this.map
/filter
are not more functional than list comprehensions. List comprehensions were borrowed from Haskell, a pure functional language.map
and especiallyfilter
can be harder to understand than a list comprehension. I would never usemap
orfilter
with a lambda but might if I had a function that already existed; I usemap
a decent bit.- The same goes for
itertools.imap
/ifilter
compared to generator expressions. (These things are somewhat lazy, which is something great we can borrow from the functional world.) - Don’t use
map
andfilter
for side effects. I see this withmap
a lot, which both makes hard-to-understand code, unneeded lists, and is decidedly not functional (despite people thinking it must be because ofmap
.) Just use a for loop. reduce
is confusing except for very simple cases. Python has for loops and there is no hurt in using them.
-
Don’t use recursive algorithms. This is one part of functional programming Python just does not support well. CPython (and I think all other Pythons) do not support tail call optimization. Use iteration instead.
-
Only use
lambda
when you are defining functions on the fly. Anonymous functions aren’t better than named functions, the latter of which are often more robust, maintainable, and documented.