Lecture 1: Subtleties of Functional Programming

In this lecture, we will familiarize ourselves with elements of functional programming in Python. We will discuss what constitutes an object, examine how functions operate as first-class objects, and explore higher-order functions.

To conclude, we will delve into important aspects of Python, including the use of functions like map, filter, and reduce, as well as partial and lambda functions.

How Functions Work

Of course, you're already familiar with what functions are, aren't you? However, there are certain aspects of functions in Python that we don't often deal with, and as a result, they can be forgotten.

Let's clarify what functions are and how they are represented in Python.

Functions as Procedures

This is the aspect of functions we are most familiar with. A procedure is a named sequence of computational steps. Any procedure can be called from anywhere in the program, including from within another procedure or even from itself. There’s not much more to say about this part, so let's move on to the next aspect of functions in Python.

Functions as First-Class Objects

In Python, everything is an object, not just the objects you create from classes. When you create a variable in Python, you're not just assigning a value to the variable; you're establishing a binding between the variable name and an object. Each type of variable has its own methods. For example, an int variable has a special method called add.

All these methods can be listed using dir().

As we can see, the variable num has a vast array of methods. This raises the question of how variables can have methods at all. The answer lies in the nature of Python. Variables have methods because they are, in fact, objects in Python.

This applies not only to simple types like int, float, and bool, but also to modules, functions, and classes. All of these are objects with their own methods and attributes. Therefore, in Python, any object can be assigned to a variable or passed as an argument to a function.

This means that in Python, everything is an object:

📌 Numbers
📌 Strings
📌 Containers
📌 Classes (yes, even classes!)
📌 Functions (which is what interests us)
📌 And everything else

The fact that everything is an object opens up many possibilities for us. Functions can be passed to other functions, returned from functions, created on the fly – meaning they are first-class objects. And this is where functional programming comes into play, along with decorators (which we will cover a little later).

 

Elements of Functional Programming

Python incorporates some concepts from functional languages like Haskell and OCaml. We'll skip the formal definition of a functional language and go straight to two of its characteristics that are also inherent to Python:

  1. Functions are first-class objects;
  2. Consequently, the language supports higher-order functions.

If you're familiar with the basics of higher mathematics, then you already know some mathematical higher-order functions, such as the differential operator d/dxd/dxd/dx. It takes a function as input and returns another function, the derivative of the original function.

Higher-order functions in programming work exactly the same way – they either take function(s) as input and/or return function(s).

So, functions can be passed into other functions. They can also be created within other functions.

Since we returned a different function, the variable multiplier now holds the inner function.

Let's try to define the inner function, which will take one argument and always multiply it by the same number that we passed to get_multiplier. For example, if we pass 2 to get_multiplier, we get a function that always multiplies the argument passed to it by 2.

This concept is called "closure".

Map

Sometimes it is necessary to apply a function to a set of elements. For these purposes, there are several standard functions. One such function is map, which takes a function and an iterable object (such as a list) and applies the given function to all elements of the object.

Note the use of the list function around map, because map by default returns a <map object> (an iterable object).

The same can be done without the map function, though it will be a bit longer using generator expressions.

This technique might already be familiar to you, but we'll discuss it a bit later nonetheless.

Filter

Another function that is often used in the context of functional programming is the filter function. The filter function allows you to filter an iterable based on a condition. It takes a function (the condition) and the iterable itself as input.

Since filter, like map, returns a <filter object> (an iterable object), it's a good idea to wrap it in a list.

It’s worth noting that although map and filter are very powerful, you should avoid overusing them, as this can often make the code less readable.

Anonymous Functions

Anonymous functions are an extremely useful feature that are either overlooked or overused. You'll learn where they shouldn't be used in the lecture on clean code. For now, let's talk about implementing anonymous functions.

Overall, the structure is very simple, as we've illustrated in the picture.

You start by writing the keyword lambda, followed by the arguments, a colon, and then some expression that will be automatically returned. There's no need to write return.

Now let's show a few examples of how to use them. We believe there shouldn't be any difficulties here.

A lambda function (anonymous function) is like a regular function but without an identifier.

If we want to pass a small function to `map` that we won’t need again, we can use lambda functions. Lambda allows you to define a function "in place," meaning without the `def` keyword. Let's do the same thing as in the example with `map`, but using `lambda`.

Lambda functions are also convenient to use with filter.

Reduce

The functools module allows you to leverage Python's functional features even more effectively.

For example, in recent versions of the language, functools includes the reduce function, which allows you to reduce data by applying a function sequentially and remembering the result:

Thus, reduce multiplies 1 by 2, then multiplies the result by 3, and so on.

And of course, you can achieve the same thing using a lambda function:

Partial

The `partial` method from `functools` allows you to create a new function with some of the arguments of the original function fixed to certain default values. For better understanding, see the example below.

Zip

Another important function is zip, which allows you to combine two or more iterables. In the following example, we sequentially pair items from num_list and squared_list into tuples.

The `zip` function returns a `<zip object>`, so we wrap it in a `list` to see the result.

Additionally, `zip` can be used to create a dictionary from two lists.

Collection Generators Instead of map, filter

Up to this point, we've defined lists in the standard way, but Python offers a more elegant and concise construction for creating lists and other collections, which we've briefly touched on.

For creating a list of squares of numbers, you can use a list comprehension instead of map.

Let’s explain the structure of this expression:

In square brackets, you essentially write a standard for loop, but the expression you want to apply is placed before the for keyword.

List comprehensions (or list generators) allow you to create nested lists with for loops and other complex expressions. They are generally preferred over traditional loops for performance reasons, as they can be slightly faster. However, overusing them can reduce code readability.

Similarly, you can write a list comprehension with a condition (replacing the filter function). The syntax is the same, just add the condition at the end.

In a similar way, you can define dictionaries. This can be useful if you need to set default values.

When using list comprehensions with curly braces, if you omit the colon, you'll get a set instead.

Without brackets, a list comprehension returns a generator—a type of object that can be iterated over (generators will be discussed in more detail later).

Comments (0)

Leave a comment