Functional programming tips to power up your Python code
Hi! In this post, I'd like to present my favorite functional programming techniques (FP) for Python. I'm a big fan of FP as I've found that by following FP principles I can write code that is more readable and easier to debug. Python is not a functional programming language (and it never will be), but I think there are still many things we can learn from languages such as Haskell that are beneficial also in Python.
Use list comprehensions
The first technique is to use list comprehensions to create lists and to perform operations such as
filter on lists.
In Haskell, we would use list comprehensions as follows:
Here we're traversing the list
[1..10] by filtering values by
x*2 >= 12 and mapping with
In Python, we would achieve the same with
The more "imperative" version of the above would be:
One can also perform
flatmap-like operations with nested list comprehensions:
One problem I often face with list comprehensions in Python is that there's no
let construct. This construct would be useful for improving code readability and to avoid re-computing intermediate values by binding them to variables. For example, in Haskell you could do:
Here we avoid computing
x*x twice for every element by binding the value to variable
In Python, we could write
but this would compute
x*x twice for every element. We can circumvent this by using an auxiliary one-element tuple as follows:
It's not as readable as in Haskell, but sometimes it feels like the best choice. We can make the construct more readable by splitting it into multiple lines:
Use generator expressions and
In Haskell, expressions are evaluted lazily so it's completely natural to work with e.g. lists containing all integers. Such infinite lists are never supposed to be evaluated in full, but the values are created only when they are actually needed.
As an example, let's consider the first problem in Project Euler. Sharing solutions of Project Euler problems publicly is strongly discouraged, but since the internet already is full of answers for the first problem, I'll make an exception here. Here's the problem:
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000.
We could solve this with iterators as follows:
>>> from itertools import count, takewhile >>> ints = count(1) # Iterator of all integers >>> multiples = (val for val in ints if val % 3 == 0 or val % 5 == 0) # All integers that are multiples of 3 or five >>> multiples_below_1000 = takewhile(lambda val: val < 1000, multiples) # All such integers below 1000 >>> sum(multiples_below_1000) # Sum of all such values, this is the evaluation step 233168
This isn't the most efficient solution to the problem in Python, but I find it very easy to read and reason about.
Use frozen dataclasses
In pure functional languages such as Haskell, one cannot mutate objects in-place. Instead, one must compose a program out of pure functions that do not mutate their inputs. Code becomes essentially a pipeline, where immutable data structures flow from one transformation to the next. This kind of thinking is also encouraged in the wonderful Pragmatic Programmer book:
"Thinking of code as a series of (nested) transformations can be a liberating approach to programming. It takes a while to get used to, but once you've developed the habit you'll find your code becomes cleaner, your functions shorter, and your designs flatter. Give it a try."
Now, it does not make much sense to try to write purely functional programs with Python, but I think the idea of favoring "transformative" code with immutable objects is a good idea. Using immutable objects also eliminates one whole class of potential bugs.
For plain datastructures, Python's built-in dataclasses are a good choice for immutable containers. With
frozen=True, we can ensure the properties in our dataclasses cannot be mutated after their creation:
If we want to change a
Doggie's name, we can use
Creating a new object every time will incur a performance penalty, so it's up to you to decide if the overhead is too large for your program.
It's important to remember that frozen dataclasses aren't truly mutable, as we can still mutate all mutable data structures within our class:
We can avoid this by using frozen dataclasses as properties:
Changing a doggie's name can then be achieved by chaining
With deeply nested structures, using
replace can become tedious. In functional programming languages, this problem of "mutating" deeply nested structures is elegantly handled by optics libraries such as
lens in Haskell. Python has its own
lenses package, but I haven't found it to be that useful out there in the real world: without the power of static type checking, overuse of lenses can result in very cryptic code.
With that said, most data structures I've encountered are at most two to three levels deep and using
replace isn't an issue, especially when using helper functions such as
Use immutable lists or dictionaries
Continuing on the topic of immutability, we still need to work with mutable lists and dictionaries in Python code. Just because they are mutable, that doesn't mean we need to mutate them. Whenever I find myself trying to mutate a list in-place with
l.append(value) or a dictionary with
d["key"] = value, I stop and think if it's really necessary. Such an operation would not be available in pure functional languages such as Haskell, so it's definitely possible to write code without mutating objects in-place. Could I avoid it here?
If I'm creating a new list or dictionary, maybe I could use list or dictionary comprehensions instead. If I need to add a value to an existing list, it may be better to simply create a whole new new object with the help of positional expansion:
Similarly, instead of adding a key to an existing dictionary, maybe I can just create a new dictionary with keyword expansion:
One advantage in treating our lists as read-only is that if we use Python's
typing system (we should!) for static type checking, we can use the read-only
typing.Sequence type for our lists instead of the mutable
typing.List type. Because
typing.Sequence[T] is a read-only collection for values of type
T, it's covariant in
typing.Sequence[A] <: typing.Sequence[B] if A <: B
This means that we can use
typing.Sequence[B] is expected, as long as
A is a subtype of
B. For similar reasons, it's better to use the read-only
typing.Mapping for dictionaries instead of
It's up to you to decide if creating new objects instead of mutating existing ones is the right solution for your program. In performance-critical cases, it's most likely better to simply mutate lists in-place instead of suffering the overhead of creating new objects. Similarly, if creating the list includes side-effects such as writing to a database, it's better to avoid list comprehensions for readability's sake.
Use type union instead of inheritance
The Pragmatic Programmer book has an interesting paragraph on inheritance:
"Do you program in an object-oriented language? Do you use inheritance? If so, stop! It probably isn't what you want to do."
How to avoid inheritance in Python? Let's assume you'd like to work with objects of type
Cat have a
say() method and you want to put such animals in containers such as (immutable) lists. The classic Programming 101 solution would be to create a super-class
Animal and inherit
Now we get classic polymorphism:
However, we can achieve the same without any of the problems of inheritance by using a type union:
If any class within the
BetterAnimal doesn't have the
say() method of proper type, the type-checker will complain.
Type unions only cover one class of use cases for inheritance, so don't expect you can always get rid of inheritance via type unions. The above example also wasn't a good example of "bad inheritance", as we could have made
Animal an "interface" by turning it into an abstract class with abstract methods and without any hard-coded behaviour. The point I'm trying to make is: always stop to think before using inheritance.
This concludes my list of functional programming tips for Python. If you have any other tips, comments or questions, please leave a comment! Thanks for reading!