Functional Python Programming
上QQ阅读APP看书,第一时间看更新

Immutable data

Since we're not using variables to track the state of a computation, our focus needs to stay on immutable objects. We can make extensive use of tuples and namedtuples to provide more complex data structures that are immutable.

The idea of immutable objects is not foreign to Python. There can be a performance advantage to using immutable tuples instead of more complex mutable objects. In some cases, the benefits come from rethinking the algorithm to avoid the costs of object mutation.

We will avoid class definitions almost entirely. It can seem like anathema to avoid objects in an Object-Oriented Programming (OOP) language. Functional programming simply doesn't need stateful objects. We'll see this throughout this book. There are reasons for defining callable objects; it is a tidy way to provide namespaces for closely related functions, and it supports a pleasant level of configurability. Also, it's easy to create a cache with a callable object, leading to important performance optimizations.

As an example, here's a common design pattern that works well with immutable objects: the wrapper() function. A list of tuples is a fairly common data structure. We will often process this list of tuples in one of the two following ways:

  • Using higher-order functions: As shown earlier, we provided lambda as an argument to the max() function: max(year_cheese, key=lambda yc: yc[1])
  • Using the wrap-process-unwrap pattern: In a functional context, we should call this and the unwrap(process(wrap(structure))) pattern

For example, look at the following command snippet:

>>> max(map(lambda yc: (yc[1], yc), year_cheese))[1]
(2007, 33.5)  

This fits the three-part pattern of wrapping a data structure, finding the maximum of the wrapped structures, and then unwrapping the structure.

map(lambda yc: (yc[1], yc), year_cheese) will transform each item into a two-tuple with a key followed by the original item. In this example, the comparison key is merely yc[1].

The processing is done using the max() function. Since each piece of data has been simplified to a two-tuple with position zero used for comparison, the higher-order function features of the max() function aren't required. The default behavior of the max() function uses the first item in each two-tuple to locate the largest value.

Finally, we unwrap using the subscript [1]. This will pick the second element of the two-tuple selected by the max() function.

This kind of wrap and unwrap is so common that some languages have special functions with names like fst() and snd() that we can use as function prefixes instead of a syntactic suffix of [0] or [1]. We can use this idea to modify our wrap-process-unwrap example, as follows:

>>> snd = lambda x: x[1]
>>> snd(max(map(lambda yc: (yc[1], yc), year_cheese)))
(2007, 33.5)

Here, a lambda is used to define the snd() function to pick the second item from a tuple. This provides an easier-to-read version of unwrap(process(wrap())). As with the previous example, the map(lambda... , year_cheese) expression is used to wrap our raw data items, and the max() function does the processing. Finally, the snd() function extracts the second item from the tuple.

In Chapter 13, Conditional Expressions and the Operator Module, we'll look at some alternatives to lambda functions, such as fst() and snd().