Python Arena
Step 3 of 3

Combining with reduce

map and filter turn a series into another series. reduce turns a whole series into a single value, such as a total. It isn't a built-in function: it lives in the module functools, so you import it first.

reduce(function, series, start) works like a loop with a running result. The result begins as start. For each item, reduce calls the function with two arguments, the result so far and the item, and what the function returns becomes the new result. After the last item, reduce returns that result. Here the result is a string that collects the first letter of each color:

from functools import reduce

colors = ["red", "green", "blue"]
code = reduce(lambda letters, color: letters + color[0], colors, "")
print(code)
Output
rgb

Round by round, the lambda gets the result so far and the next color, and what it returns is the result for the next round:

Round letters color The lambda returns
1 "" (the start) "red" "r"
2 "r" "green" "rg"
3 "rg" "blue" "rgb"

There are no more colors after round 3, so reduce returns "rgb".

The function can combine the result and the item in any way you write, and the result doesn't have to be the same kind of value as the items. Both of these go through a list of words. The first keeps the longest word, and the second counts the letters of all the words:

from functools import reduce

words = ["map", "filter", "reduce", "lambda"]
print(reduce(lambda longest, word: word if len(word) > len(longest) else longest, words, ""))
print(reduce(lambda total, word: total + len(word), words, 0))
Output
filter
21

The start value is where the result begins, so choose one that fits the job. The longest word starts from the empty string, which any word is longer than, and a count of letters starts from 0. The start value is also what reduce returns for an empty list.

Without a start value

You can leave the start value out. Then reduce takes the first item as the starting result and goes on from the second one, so reduce(lambda a, b: a + b, [4, 8, 3]) starts from 4 and gives 15. But with an empty list there's nothing to start from, and reduce stops with TypeError: reduce() of empty iterable with no initial value.

reduce and your own objects

With objects, give the start value. The function adds something taken from each object to the result:

from functools import reduce


class Parcel:
    def __init__(self, label, weight):
        self.label = label
        self.weight = weight


parcels = [Parcel("books", 4.5), Parcel("lamp", 2.0), Parcel("plates", 6.5)]
total = reduce(lambda result, parcel: result + parcel.weight, parcels, 0)
print("Total weight:", total)
Output
Total weight: 13.0

Without the 0, reduce would take the first parcel itself as the starting result, and the first round would try to add a weight to a Parcel object:

from functools import reduce


class Parcel:
    def __init__(self, label, weight):
        self.label = label
        self.weight = weight


parcels = [Parcel("books", 4.5), Parcel("lamp", 2.0), Parcel("plates", 6.5)]
total = reduce(lambda result, parcel: result + parcel.weight, parcels)
print("Total weight:", total)
Python stops with
Traceback (most recent call last):
  File "main.py", line 11, in <module>
    total = reduce(lambda result, parcel: result + parcel.weight, parcels)
  File "main.py", line 11, in <lambda>
    total = reduce(lambda result, parcel: result + parcel.weight, parcels)
                                          ~~~~~~~^~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'Parcel' and 'float'

So when the items are objects and the result is a number, always give a start value of the right type.

Your task

Challenge 1 of 3

The program should add up the playing times of the songs with reduce, but it stops with an error. Fix it, so that it prints:

Total: 11 min

Challenge 2

What does this program print? Type each line it prints.

Challenge 3

The class CartItem holds the name, the unit price and the quantity of an item in a shopping cart. Write a function cart_total(items) that uses reduce to return the total price of the cart: the sum of price times quantity of every item. An empty cart costs 0. For example,

cart = [CartItem("pen", 1.5, 4), CartItem("notebook", 3.25, 2), CartItem("eraser", 0.75, 1)]
print(cart_total(cart))

should print:

13.25