Functional Programming with Python¶
Python allows you to use the functional programming paradigm. This is a programming style where only functions are used.
Overview¶
What is a function?
Lambda expression
Built-in Functional Functions
functools module
What is a Function?¶
A function takes zero or more parameters, runs some code, returning a result.
Functions in Python can interact with the global scope (really, a file or module scope), the local scope of the function it was created in (see Nested Functions). The parameters may also be mutable, and so they may be changed by the function.
A pure function, which we aim to use all the time, doesn’t rely on global variables. Its result depends entirely on the parameters passed in. Additionally, it does not change global state nor does it modify the things passed into it, or anything those things reference. We call this sort of behavior “idempotent”, meaning, whether you do it once or a million times, the effect is the same.
Pure functions are preferable because they are simple to think about.
Pure functions can do quite a lot!
Functional Programming¶
Python, at its core, like many languages, follows the Procedural Programming Paradigm. In the Procedural Programming Paradigm, you specify what actions to take, and the program will follow your instructions exactly.
Procedural programs rely on global state and functions that can read from and write to that global state. Although procedural programming is ugly from a theoretical perspective, and it’s quite easy to create programs that are almost impossible to understand, it’s still closest to how we speak to each other.
Functional Programming is another programming paradigm – a way of doing things. It started with the mathematical idea of Lambda Calculus, which was a way of describing programs completely different from the procedural paradigm. Lambda Calculus was thought at first to be a fantasy of our imagination, but this was quickly proven wrong when languages like Lisp and others appeared and were successful.
In Functional Programming, functions are considered “first class values”, in that they are freely passed around and referenced. Indeed, you can create a successful language entirely with functions – no integers, no floats, no strings, no tuples. Just functions.
Recursion is a preferable option to looping. As I pointed out earlier, tail recursion means you can have infinite recursion with no negative side effects to memory or the stack. This is an example of optimizations that are possible with functional programming.
Other optimizations can be introduced:
If a function is pure, and the result is not used, then it need not be called at all.
If a pure function doesn’t depend on the results of another pure function, then the two functions can run in parallel, isolated from each other.
If a pure function is called twice with the same parameters, then you needn’t actually execute the function a second time. You can just return the same result that was returned earlier. This is called “memoizing”.
Theoretically, a pure function is just the function body applied to the parameters passed in. Thus, you can “inline” the function body and bypass calling the function altogether. IE, if I had
add(a,b)
, and then I called it asadd(1,3)
, the compiler can simply replace it with1+3
or even4
and thus skip calling the function (or even the operation!) at all.
Allow me to introduce a new concept you should be aware of: Lazy evaluation. In lazy evaluation, when you call a function, it isn’t called, but the fact that you are relying on the result of the function is remembered. As long as you don’t look at the result, or don’t ask for it, the functions are not called. In fact, you can pass the result that hasn’t been rendered yet into a function, and you will get a new result that still didn’t require actually calling any functions.
When you do actually need the value, then you can call the functions that were necessary to render the result, but only those functions that were actually used.
This is hinted at with iterators. Iterators won’t necessarily do all the work
to calculate each value in the sequence until you call next()
!
Python definitely doesn’t support all the advanced features you might want for Functional Programming, but that doesn’t mean you can’t use this paradigm for your code. Sticking to pure functions will dramatically decrease the complexity of your code and may help you identify patterns that can further reduce complexity.
Lambda Expression¶
We often have functions that are a simple expression, and don’t require any statements except to return that expression. For this, we can use a lambda expression:
lambda param, param, param: expr
The parameter list is exactly the same as it is for functions.
The expression is evaluated and then returned when the lambda is called.
The lambda function returns the expression applied to the parameters.
Starred parameters are ok:
>>> (lambda *x: len(x))(1,2,3,4,5)
5
Typically, we don’t store lambdas in variables. We pass them to functions or call them immediately.
The fact that you can’t specify statements in the body of a lambda expression is somewhat unfortunate, but it usually isn’t a problem. Defining new functions is easy enough!
It is rather easy to define pure lambdas. Just be sure not to call any functions from the body that are not pure.
Built-in Functional Functions¶
filter(fn, iterable)
: Appliesfn
to each item initerable
, returning only the elements that are True.map(fn, iterable, ...)
: Returns an iterator offn
applied to each element initerable
. If multiple iterators are specified, then the next value of each iterable is called and passed as a list to fn.zip(*iterables)
: Returns an iterator with each item being the next item from each iterable. Stops when shortest sequence is exhausted
Examples:
# Return all the odd numbers between 0-99, reversed.
filter(lambda a: a%2==1, reversed(range(100)))
def count():
n = 0
while True:
yield n
n = n + 1
# The cube series.
i = map(lambda x: x**3, count())
# Tuples with powers of x.
i = zip(
map(lambda x: x**0, count()),
map(lambda x: x**1, count()),
map(lambda x: x**2, count()),
map(lambda x: x**3, count()))
i = map(lambda x: (x**0, x**1, x**2, x**3), count())
functools Module¶
You need to import the functools module to use it:
import functools
functools.partial(func, *args, **kwargs)
: Returns a new function that when called, will combine the new call’sargs
andkwargs
to the already passed in parameters, and then call the func.functools.reduce(func, iterable, initializer=None)
: We’ve written this already.