{"id":"functional-reduce","slug":"reduce","title":"Combining with reduce","text":"`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.\n\n`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:\n\n```python\nfrom functools import reduce\n\ncolors = [\"red\", \"green\", \"blue\"]\ncode = reduce(lambda letters, color: letters + color[0], colors, \"\")\nprint(code)\n```\n\n```output\nrgb\n```\n\nRound by round, the lambda gets the result so far and the next color, and what it returns is the result for the next round:\n\n| Round | `letters` | `color` | The lambda returns |\n|:-:|---|---|---|\n| 1 | `\"\"` (the start) | `\"red\"` | `\"r\"` |\n| 2 | `\"r\"` | `\"green\"` | `\"rg\"` |\n| 3 | `\"rg\"` | `\"blue\"` | `\"rgb\"` |\n\nThere are no more colors after round 3, so `reduce` returns `\"rgb\"`.\n\nThe 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:\n\n```python\nfrom functools import reduce\n\nwords = [\"map\", \"filter\", \"reduce\", \"lambda\"]\nprint(reduce(lambda longest, word: word if len(word) > len(longest) else longest, words, \"\"))\nprint(reduce(lambda total, word: total + len(word), words, 0))\n```\n\n```output\nfilter\n21\n```\n\nThe 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.\n\n### Without a start value\n\nYou 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`.\n\n### reduce and your own objects\n\nWith objects, give the start value. The function adds something taken from each object to the result:\n\n```python\nfrom functools import reduce\n\n\nclass Parcel:\n    def __init__(self, label, weight):\n        self.label = label\n        self.weight = weight\n\n\nparcels = [Parcel(\"books\", 4.5), Parcel(\"lamp\", 2.0), Parcel(\"plates\", 6.5)]\ntotal = reduce(lambda result, parcel: result + parcel.weight, parcels, 0)\nprint(\"Total weight:\", total)\n```\n\n```output\nTotal weight: 13.0\n```\n\nWithout 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:\n\n```python\nfrom functools import reduce\n\n\nclass Parcel:\n    def __init__(self, label, weight):\n        self.label = label\n        self.weight = weight\n\n\nparcels = [Parcel(\"books\", 4.5), Parcel(\"lamp\", 2.0), Parcel(\"plates\", 6.5)]\ntotal = reduce(lambda result, parcel: result + parcel.weight, parcels)\nprint(\"Total weight:\", total)\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 11, in <module>\n    total = reduce(lambda result, parcel: result + parcel.weight, parcels)\n  File \"main.py\", line 11, in <lambda>\n    total = reduce(lambda result, parcel: result + parcel.weight, parcels)\n                                          ~~~~~~~^~~~~~~~~~~~~~~\nTypeError: unsupported operand type(s) for +: 'Parcel' and 'float'\n```\n\nSo when the items are objects and the result is a number, always give a start value of the right type.\n","task":"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:\n\n```\nTotal: 11 min\n```\n","kind":"code","seed":"from functools import reduce\n\n\nclass Song:\n    def __init__(self, title, minutes):\n        self.title = title\n        self.minutes = minutes\n\n\nsongs = [Song(\"Intro\", 2), Song(\"Theme\", 5), Song(\"Outro\", 4)]\ntotal = reduce(lambda result, song: result + song.minutes, songs)\nprint(\"Total:\", total, \"min\")\n","solution":"from functools import reduce\n\n\nclass Song:\n    def __init__(self, title, minutes):\n        self.title = title\n        self.minutes = minutes\n\n\nsongs = [Song(\"Intro\", 2), Song(\"Theme\", 5), Song(\"Outro\", 4)]\ntotal = reduce(lambda result, song: result + song.minutes, songs, 0)\nprint(\"Total:\", total, \"min\")\n","hints":["Read the error message. What is `reduce` trying to add a number to in the first round?","Without a start value, the first `Song` object is the starting result. Give `reduce` a third argument, the start value `0`."],"tests":[{"name":"Output","stdin":"","expect":"Total: 11 min","hidden":false}],"require":[{"pattern":"\\breduce\\s*\\(","message":"Keep adding up the minutes with `reduce`."}],"forbid":[{"pattern":"\\b11\\b","message":"Let `reduce` calculate the total instead of typing 11."}],"more":[{"task":"What does this program print? Type each line it prints.\n","kind":"predict","seed":"from functools import reduce\n\nnumbers = [3, 9, 2]\nprint(reduce(lambda a, b: a * 10 + b, numbers, 0))\nprint(reduce(lambda a, b: a if a > b else b, numbers))\nprint(reduce(lambda text, n: text + str(n) + \"-\", numbers, \">\"))\n","solution":"from functools import reduce\n\nnumbers = [3, 9, 2]\nprint(reduce(lambda a, b: a * 10 + b, numbers, 0))\nprint(reduce(lambda a, b: a if a > b else b, numbers))\nprint(reduce(lambda text, n: text + str(n) + \"-\", numbers, \">\"))\n","hints":["Go round by round. On the first line the result is 0, then 0 * 10 + 3, then 3 * 10 + 9, and so on.","The second line has no start value, so it starts from 3 and keeps the larger of the result and the next number.","The last line starts from the string `\">\"` and adds each number and a dash."],"tests":[{"name":"Output","stdin":"","expect":"392\n9\n>3-9-2-","hidden":false}],"require":[],"forbid":[],"lines":["392","9",">3-9-2-"]},{"task":"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,\n\n```python\ncart = [CartItem(\"pen\", 1.5, 4), CartItem(\"notebook\", 3.25, 2), CartItem(\"eraser\", 0.75, 1)]\nprint(cart_total(cart))\n```\n\nshould print:\n\n```\n13.25\n```\n","kind":"code","seed":"class CartItem:\n    def __init__(self, name, price, quantity):\n        self.name = name\n        self.price = price\n        self.quantity = quantity\n\n\ndef cart_total(items):\n    pass\n\n\nif __name__ == \"__main__\":\n    cart = [CartItem(\"pen\", 1.5, 4), CartItem(\"notebook\", 3.25, 2), CartItem(\"eraser\", 0.75, 1)]\n    print(cart_total(cart))\n","solution":"from functools import reduce\n\n\nclass CartItem:\n    def __init__(self, name, price, quantity):\n        self.name = name\n        self.price = price\n        self.quantity = quantity\n\n\ndef cart_total(items):\n    return reduce(lambda total, item: total + item.price * item.quantity, items, 0)\n\n\nif __name__ == \"__main__\":\n    cart = [CartItem(\"pen\", 1.5, 4), CartItem(\"notebook\", 3.25, 2), CartItem(\"eraser\", 0.75, 1)]\n    print(cart_total(cart))\n","hints":["Import it first: `from functools import reduce`.","The function given to `reduce` gets the total so far and one item, and returns the total plus that item's price times its quantity.","Give `0` as the start value. Without it, the first round would add to a `CartItem` object, and an empty cart would be an error."],"tests":[{"name":"Test 1","stdin":"","call":"cart = [CartItem(\"pen\", 1.5, 4), CartItem(\"notebook\", 3.25, 2), CartItem(\"eraser\", 0.75, 1)]\nprint(cart_total(cart))\n","expect":"13.25","hidden":false},{"name":"cart_total([CartItem(\"melon\", 2.5, 2)])","stdin":"","call":"cart_total([CartItem(\"melon\", 2.5, 2)])\n","expect":"5.0","hidden":true,"returns":true},{"name":"print(cart_total([]) == 0)","stdin":"","call":"print(cart_total([]) == 0)\n","expect":"True","hidden":true},{"name":"print(cart_total([CartItem(\"cup\", 4, 3), CartItem(\"plate\", 6, 2), CartItem(\"fork\", 1, 10)]) == 34)","stdin":"","call":"print(cart_total([CartItem(\"cup\", 4, 3), CartItem(\"plate\", 6, 2), CartItem(\"fork\", 1, 10)]) == 34)\n","expect":"True","hidden":true}],"require":[{"pattern":"\\breduce\\s*\\(","message":"Calculate the total with `reduce`."}],"forbid":[]}]}