{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "l10-c000",
   "metadata": {},
   "source": [
    "# Lecture 12 - Collections 2: Tuples and collection operations\n",
    "\n",
    "**Authors:** Fiona McNeill, Matteo Degiacomi, Rayo Verweij, and Nigel Topham\n",
    "\n",
    "## Introduction\n",
    "\n",
    "Lecture 5 introduced lists, sets, and dictionaries, with some basic methods for using them. In this lecture, we'll explore some more advanced collection operations, as well as introduce the final collection type in Python: tuples.\n",
    "\n",
    "### Learning outcomes\n",
    "\n",
    "In this notebook, we'll discuss:\n",
    "\n",
    "- explaining which values can be dictionary keys or set members;\n",
    "- creating and unpacking tuples;\n",
    "- accessing and updating two-dimensional lists;\n",
    "- selecting parts of sequences with slicing;\n",
    "- looping with `enumerate()` and `zip()`; and\n",
    "- using collection methods and comprehensions to process data.\n",
    "\n",
    "### Using the notebook\n",
    "\n",
    "Remember, this notebook is _yours_! You can edit each cell, for example by adding new examples or changing values to see what happens. Feel free to experiment!\n",
    "\n",
    "<details>\n",
    "<summary>Click to expand</summary>\n",
    "\n",
    "A common approach for learning how to read and write code in a structured way is called **PRIMM**:\n",
    "1. **Predict**: before running the code, look at it - do you understand each line? What do you think will happen?\n",
    "2. **Run**: now run the code and see what output you get.\n",
    "3. **Investigate**: did it go as you thought it would? If not, can you figure out why?\n",
    "4. **Modify**: now change the code and once again try to predict the output. If you got it wrong the first time, did you get it right now?\n",
    "5. **Make**: finally, try to write your own program from scratch using the skills you learned.\n",
    "\n",
    "You can apply steps 1-4 to each code cell in the notebook and many will have prediction prompts to help you get started. In addition, there will be challenges that ask you to put everything together yourself and execute step 5. There will often be multiple ways to solve these, but each challenge has a potential answer listed.\n",
    "\n",
    "</details>\n",
    "\n",
    "### Navigating the notebook\n",
    "\n",
    "A Jupyter notebook can be operated with your keyboard. Here is a **cheat sheet**:\n",
    "\n",
    "<details>\n",
    "<summary>Click to expand</summary>\n",
    "\n",
    "- To run the selected cell and move to the next cell, press <kbd>&#x21E7; Shift</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To run the selected cell and keep focus there, press <kbd>⌃ Ctrl</kbd> + <kbd>&#x23ce; Enter</kbd>.\n",
    "- To create a cell under the one currently highlighted, press <kbd>B</kbd>;\n",
    "- To delete the currently highlighted cell, press <kbd>D</kbd> twice (be careful with this one!) or press <kbd>X</kbd> to cut it;\n",
    "- To get help for a specific function, place the cursor within the function's brackets, hold <kbd>&#x21E7; Shift</kbd>, and press <kbd>&#x21E5; Tab</kbd>.\n",
    "\n",
    "</details>\n",
    "\n",
    "Watch out: **code cells remember what happened in cells before**. So, especially in the more complicated notebooks later down the line, make sure to always run **every** cell from top to bottom, as one might rely on a piece of code that came before it! If your results become confusing, use **Kernel > Restart Kernel** and then rerun all the completed cells in order.\n",
    "\n",
    "### Further reading and practice\n",
    "\n",
    "- Official Python tutorial: [Data structures](https://docs.python.org/3/tutorial/datastructures.html), in particular 5.1.3 List comprehensions; 5.3 Tuples and sequences; and 5.6 Looping techniques.\n",
    "- W3Schools: [Tuples](https://www.w3schools.com/python/python_tuples.asp), [Slicing strings](https://www.w3schools.com/python/python_strings_slicing.asp), and [List comprehensions](https://www.w3schools.com/python/python_lists_comprehension.asp).\n",
    "- Helsinki Python MOOC: [More lists](https://programming-26.mooc.fi/part-5/1-more-lists) and [Tuple](https://programming-26.mooc.fi/part-5/4-tuple).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "645a13ae",
   "metadata": {},
   "source": [
    "## 1. Hashability"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "878dc25b-c651-4201-a7ce-e317fba81472",
   "metadata": {},
   "source": [
    "We have seen how to check whether a specific item exists in a list, a set, or a dictionary."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "361a43a9-fdbc-467a-b9d3-fdd288bd604f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# check if an item is in a list\n",
    "a = [1, 2 , 3]\n",
    "print(2 in a)\n",
    "\n",
    "# check if an item is in a set\n",
    "a = (1, 2, 3)\n",
    "print(2 in a)\n",
    "\n",
    "# check if an item is in a dictionary\n",
    "a = {\"one\": 1, \"two\": 2 , \"three\": 3}\n",
    "print(\"two\" in a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49676422-4584-4394-8712-58a29938e613",
   "metadata": {},
   "source": [
    "The answer appears very fast, right? However, if these collections were extremely large, finding an item in a set or a dictionary would be substantially faster. For collections containing a million elements, this operation can be **290'000 times faster** on a set or a dictionary than on an list! Why is that? This is connected to a key property of some values, called **hashability**."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2354f70",
   "metadata": {},
   "source": [
    "In computer science, a **hash** is an identifier for objects. They are integers or strings, created through running the underlying encoding of the object through a _hash function_ that creates an identifier for that object. There are multiple algorithms for creating hashes, such as [MD5](https://en.wikipedia.org/wiki/MD5) or [SHA-2](https://en.wikipedia.org/wiki/SHA-2).\n",
    "\n",
    "Hashes are:\n",
    "- _Deterministic_: hash functions always give the same output for a particular object. Therefore, equal objects have equal hashes.\n",
    "- _Irreversible_: you cannot convert the hash back to the object it was made of.\n",
    "\n",
    "Python has a built-in `hash()` function that can make hashes for you:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "54877fae",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(hash(\"Hello world!\"))\n",
    "print(hash(True))\n",
    "print(hash(3.1415926535))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c7d35eb",
   "metadata": {},
   "source": [
    "Not all types of objects are hashable. Lists, sets, and dictionaries are not:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30d45904",
   "metadata": {
    "tags": [
     "raises-exception"
    ]
   },
   "outputs": [],
   "source": [
    "print(hash([1, 2, 3]))  # Will throw a TypeError"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c79d94f",
   "metadata": {},
   "source": [
    "Note that different values can have the same hash, so while hashes are identifiers, they are not _unique_ identifiers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b093dafd",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(hash(-1))\n",
    "print(hash(-2))\n",
    "print(hash(-1) == hash(-2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec99ef0c",
   "metadata": {},
   "source": [
    "Why do we care about hashability? Two reasons:\n",
    "\n",
    "- Sets can only contain hashable objects; and\n",
    "- Dictionary keys can only be hashable objects.\n",
    "\n",
    "When creating either type of collection, Python automatically calls `hash()` under the hood to create hashes for set items and dictionary keys. While hashes are not unique to objects, having these identifiers _greatly speeds up_ looking up items. Essentially, when accessing an item, Python can use the hash to immediately retrieve it without much processing. Only in the event of a **hash collision**, when several objects have the same hash, does Python need to use the relatively slower equality check to ensure that the right item is retrieved.\n",
    "\n",
    "**Note:** In the built-in types we've seen so far, strings, integers, floats, and Booleans are both immutable and hashable; whereas lists, sets, and dictionaries are both mutable and not hashable. While these neatly correspond, mutability and hashability are very different things, and we'll soon see a type where this is not always the case!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35b3582b",
   "metadata": {
    "tags": [
     "raises-exception"
    ]
   },
   "outputs": [],
   "source": [
    "# These each raise a TypeError:\n",
    "invalid_dictionary = {\n",
    "    [1, 2]: \"this cannot be!\"\n",
    "}\n",
    "\n",
    "invalid_set = {[1, 2]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b8e374e",
   "metadata": {},
   "source": [
    "The exact hash of a string may change when running Python files at different times, so do not use the output of `hash()` as a permanent value in your code.\n",
    "\n",
    "As you've seen, dictionary *values* do not need to be hashable. For example, a dictionary can map a room name to a list of bookings just fine. The restriction applies only to its keys."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f670dbd",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: evaluating hashability</b>\n",
    "\n",
    "For each value, note down whether it is hashable or not.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9bdd4a7b",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "'''\n",
    "\"Monday\"            :\n",
    "{\"room\": \"G.07\"}    :\n",
    "12                  :\n",
    "[1, 2]              :\n",
    "{\"tea\", \"soup\"}     :\n",
    "'''\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd6e03b6",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for the solution</b></summary>\n",
    "\n",
    "```python\n",
    "'''\n",
    "\"Monday\"            : Hashable\n",
    "{\"room\": \"G.07\"}    : Not hashable (though both the individual key and value are)\n",
    "12                  : Hashable\n",
    "[1, 2]              : Not hashable (though both individual items are)\n",
    "{\"tea\", \"soup\"}     : Not hashable (though both individual items are)\n",
    "'''\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc5a972b",
   "metadata": {},
   "source": [
    "## 2. Tuples"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c001",
   "metadata": {},
   "source": [
    "In addition to lists, sets, and dictionaries, Python has one more collection type: tuples.\n",
    "\n",
    "A **tuple** is an ordered sequence, like a list, but it is *immutable*: unlike lists, items cannot be replaced, added, or removed after creation. Tuples are useful for fixed groups of related values, like coordinates.\n",
    "\n",
    "Whereas lists use square brackets `[]`, tuples use parentheses `()`, with items separated by commas. Indexing, negative indexing, and iteration work as they do for lists.\n",
    "\n",
    "**Predict:** What does each `print()` call display?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c002",
   "metadata": {
    "tags": [
     "raises-exception"
    ]
   },
   "outputs": [],
   "source": [
    "location = (55.9444, -3.1872)\n",
    "print(location[0])\n",
    "print(location[-1])\n",
    "\n",
    "for coordinate in location:\n",
    "    print(coordinate)\n",
    "\n",
    "location[0] = 56    # Throws a TypeError because tuples are immutable"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c003",
   "metadata": {},
   "source": [
    "A one-item tuple needs a comma, as in `(5,)`, because `(5)` would just evaluate to the number `5`. An empty tuple can be written as `()`.\n",
    "\n",
    "**Predict:** Which types will the next cell print?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c004",
   "metadata": {},
   "outputs": [],
   "source": [
    "empty = ()\n",
    "one_item = (5,)\n",
    "not_a_tuple = (5)\n",
    "print(type(empty), type(one_item), type(not_a_tuple))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c005",
   "metadata": {},
   "source": [
    "**Unpacking** a tuple assigns the items in a sequence to several names at once. The number of names must match the number of items, or Python raises a `ValueError`.\n",
    "\n",
    "**Predict:** Which value will be assigned to `credits`?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c006",
   "metadata": {},
   "outputs": [],
   "source": [
    "course = (\"IP\", \"Introduction to Programming\", 20)\n",
    "\n",
    "code, title, credits = course\n",
    "print(f\"{code}: {title} ({credits} credits)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02d78f81",
   "metadata": {},
   "source": [
    "...and guess what, unpacking also works for lists!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b04086c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "fibs = [1, 1, 2, 3, 5]\n",
    "\n",
    "one, two, three, four, five = fibs\n",
    "print(one, two, three, four, five)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a521516e",
   "metadata": {},
   "source": [
    "### Tuples and hashability\n",
    "\n",
    "A tuple is hashable **only if every item it contains is hashable**. Therefore, a tuple that only contains numbers can be a dictionary key or set item, but a tuple that contains a list cannot.\n",
    "\n",
    "As an overview, this is the mutability and hashability of the built-in Python types we've discussed:\n",
    "\n",
    "| Type | Mutable? | Hashable? |\n",
    "| ---- | -------- | --------- |\n",
    "| String | No | Yes |\n",
    "| Integer | No | Yes |\n",
    "| Float | No | Yes |\n",
    "| Boolean | No | Yes |\n",
    "| NoneType | No | Yes |\n",
    "| List | Yes | No |\n",
    "| Set | Yes | No |\n",
    "| Dictionary | Yes | No |\n",
    "| Tuple | No | Only when all its contents are |\n",
    "\n",
    "**Predict:** Will changing the list inside `booking` change what is printed?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2d17c236",
   "metadata": {},
   "outputs": [],
   "source": [
    "room_at_position = {\n",
    "    (0, 0): \"Entrance\",\n",
    "    (0, 1): \"Lecture room\"\n",
    "}\n",
    "print(room_at_position[(0, 1)])\n",
    "\n",
    "booking = (\"Monday\", [\"lecture\"])\n",
    "booking[1].append(\"practice\")\n",
    "print(booking)\n",
    "\n",
    "# The tuple contains a list, so it is not hashable:\n",
    "# hash(booking)  # would raise TypeError"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c007",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: represent a fixed record</b>\n",
    "\n",
    "Create an event tuple containing a name, room, and capacity. Unpack it and print a labelled sentence. Create a one-item tuple too, and explain whether each tuple is hashable.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c008",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "# Your code here!\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c009",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "Both tuples written here are hashable, as every item is a string or an integer.\n",
    "\n",
    "```python\n",
    "event = (\"Programming lecture\", \"F.21\", 150)\n",
    "name, room, capacity = event\n",
    "print(f\"{name} is in {room} with capacity {capacity}\")\n",
    "\n",
    "one_item = (\"cancelled\",)\n",
    "print(one_item)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e787134",
   "metadata": {},
   "source": [
    "## 3. Two-dimensional lists"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c010",
   "metadata": {},
   "source": [
    "A list can contain other lists. A list of rows is often called a **two-dimensional list**, and can represent a grid or table.\n",
    "\n",
    "In `temperatures[1][2]` below, the first index chooses a row, and the second chooses an item within that row. Both indices start at zero.\n",
    "\n",
    "**Predict:** What will the first two calls to `print()` display? Which value changes in the final list?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c011",
   "metadata": {},
   "outputs": [],
   "source": [
    "temperatures = [\n",
    "    [12, 14, 13],\n",
    "    [11, 15, 16],\n",
    "    [10, 12, 14],\n",
    "]\n",
    "print(temperatures[0])\n",
    "print(temperatures[1][2])\n",
    "\n",
    "temperatures[2][0] = 9\n",
    "print(temperatures)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c012",
   "metadata": {},
   "source": [
    "We can use a nested loop to visit each row and then each value in that row:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c013",
   "metadata": {},
   "outputs": [],
   "source": [
    "for daily_readings in temperatures:\n",
    "    for reading in daily_readings:\n",
    "        print(reading)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c016",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: summarise rows</b>\n",
    "\n",
    "For the 3×3 list below, print the sum of each row. Then change the centre value to `30` and print the grid. Which two indices identify the centre?\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c017",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "grid = [[2, 4, 6], [1, 3, 5], [8, 7, 9]]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c018",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "grid = [[2, 4, 6], [1, 3, 5], [8, 7, 9]]\n",
    "for row in grid:\n",
    "    print(sum(row))\n",
    "\n",
    "grid[1][1] = 30\n",
    "print(grid)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc090fa2",
   "metadata": {},
   "source": [
    "## 4. Slicing sequences"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c019",
   "metadata": {},
   "source": [
    "A **slice** allows us to select a part of a sequence. `sequence[start:stop]` includes the item at `start` and stops *before* `stop`, just like the upper boundary of `range()`. Omit `start` to begin at the start, or `stop` to continue to the end.\n",
    "\n",
    "Just like with `range()`, the third part of the slice indicates step size: for example, `[::2]` selects every second item. Negative values, such as `[::-1]`, reverse the order.\n",
    "\n",
    "**Predict:** Write each resulting list as a comment next to it before running the cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c020",
   "metadata": {},
   "outputs": [],
   "source": [
    "values = [10, 20, 30, 40, 50, 60]\n",
    "print(values[1:4])\n",
    "print(values[:3])\n",
    "print(values[3:])\n",
    "print(values[2:5:2])\n",
    "print(values[::3])\n",
    "print(values[::-1])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c021",
   "metadata": {},
   "source": [
    "Slicing works with tuples and strings too.\n",
    "\n",
    "Note that slice boundaries that are out of range are allowed: Python simply stops at the available boundary.\n",
    "\n",
    "A slice of a list creates a _new_ outer list. Like `.copy()`, this is a shallow copy, so slicing a nested list does not copy its rows."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c022",
   "metadata": {},
   "outputs": [],
   "source": [
    "text = \"programming\"\n",
    "coordinate = (55.9, -3.2, 40.0)\n",
    "\n",
    "print(text[:7])\n",
    "print(text[::2])\n",
    "print(coordinate[:2])\n",
    "print(text[:100])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c023",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: select subsequences</b>\n",
    "\n",
    "From `days` below, create weekday, weekend, every-second-day, and reversed lists using slices. Finally, print `days` to check that creating the slices did not change the list itself.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c024",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c025",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n",
    "print(days[:5])\n",
    "print(days[5:])\n",
    "print(days[::2])\n",
    "print(days[::-1])\n",
    "print(days)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1da8a496",
   "metadata": {},
   "source": [
    "## 5. Looping with positions and pairs"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c026",
   "metadata": {},
   "source": [
    "### Enumerating list and tuple items\n",
    "\n",
    "Looping over a list or tuple gives us access to the values in that collection, but sometimes we also want easy access to an item's index. One way to do this is to loop over `range(len(items))`, which provides the indices from `0` to `len(items) - 1` that we can then use to access individual items:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c027",
   "metadata": {},
   "outputs": [],
   "source": [
    "rooms = [\"G.07\", \"2.12\", \"3.01\"]\n",
    "\n",
    "for index in range(len(rooms)):\n",
    "    print(index, rooms[index])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd2dec97",
   "metadata": {},
   "source": [
    "However, Python gives us an even easier way to do this: the `enumerate()` function. This function supplies `(index, item)` tuples, which we can unpack directly in the loop header:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0baf9754",
   "metadata": {},
   "outputs": [],
   "source": [
    "for index, room in enumerate(rooms):\n",
    "    print(index, room)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56605c29",
   "metadata": {},
   "source": [
    "`enumerate()` takes an optional `start` argument, which allows you to specify at what number to start the counter. This is useful for labels shown to a user, and does not change the indices of the original list."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3abbc82e",
   "metadata": {},
   "outputs": [],
   "source": [
    "for position, room in enumerate(rooms, start=1):\n",
    "    print(f\"Choice {position}: {room}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c028",
   "metadata": {},
   "source": [
    "### Combining multiple iterables\n",
    "\n",
    "Another useful built-in function, `zip()`, supplies tuples of corresponding items from two or more iterables. This is useful when several lists describe the same records and you want to combine them.\n",
    "\n",
    "By default, `zip()` stops when the shortest input ends. If one list is shorter by mistake, some data will be left unused.\n",
    "\n",
    "**Predict:** How many lines will this print? Remove the last mark and run it again to check what happens."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c029",
   "metadata": {},
   "outputs": [],
   "source": [
    "course_codes = [\"IP\", \"CS\", \"SE\"]\n",
    "marks = [68, 74, 71]\n",
    "\n",
    "for code, mark in zip(course_codes, marks):\n",
    "    print(f\"{code}: {mark}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f75e90d8",
   "metadata": {},
   "source": [
    "We can even combine them - though to keep unpacking clear, only unpack the outer tuple (from `enumerate()`) in the loop header, and then unpack the nested tuple (from `zip()`) inside the loop itself:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fd4bef48",
   "metadata": {},
   "outputs": [],
   "source": [
    "for index, record in enumerate(zip(course_codes, marks)):\n",
    "    code, mark = record\n",
    "    print(f\"{index}. {code}: {mark}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c030",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: combine aligned data</b>\n",
    "\n",
    "Use `zip()` to combine the three lists below, and `enumerate()` to number each result. Print the line number, product name, price, quantity, and line total.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c031",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "names = [\"tea\", \"soup\", \"cake\"]\n",
    "prices = [2.4, 3.5, 2.8]\n",
    "quantities = [2, 1, 3]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c032",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "names = [\"tea\", \"soup\", \"cake\"]\n",
    "prices = [2.4, 3.5, 2.8]\n",
    "quantities = [2, 1, 3]\n",
    "\n",
    "for line, record in enumerate(zip(names, prices, quantities), start=1):\n",
    "    name, price, quantity = record\n",
    "    print(f\"{line}. {name}: £{price:.2f} × {quantity} = £{price * quantity:.2f}\")\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e347310",
   "metadata": {},
   "source": [
    "## 6. Generating collections"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33257c05",
   "metadata": {},
   "source": [
    "### Moving between text and lists\n",
    "\n",
    "`text.split(separator)` splits a string into a list of strings. With no separator argument, `.split()` splits on whitespace, treating several consecutive whitespace characters as a single separator, and ignoring whitespace at the edges.\n",
    "\n",
    "The reverse operation is `separator.join(strings)`, combining strings with that separator between them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "568f68c0",
   "metadata": {},
   "outputs": [],
   "source": [
    "text = \"  tea   soup cake  \"\n",
    "words = text.split()\n",
    "print(words)\n",
    "\n",
    "print(\" | \".join(words))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40f42e5f",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: clean up a list of words</b>\n",
    "\n",
    "Split `\" pear,apple,pear,banana \"` at its commas. Strip each word, remove duplicates using a set, and join them with `\", \"`.\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2a87658d",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "text = \" pear,apple,pear,banana \"\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98d6e164",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "```python\n",
    "text = \" pear,apple,pear,banana \"\n",
    "cleaned = []\n",
    "\n",
    "for word in text.split(\",\"):\n",
    "    cleaned.append(word.strip())\n",
    "\n",
    "unique_words = set(cleaned)\n",
    "clean_text = \", \".join(unique_words)\n",
    "print(clean_text)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c035",
   "metadata": {},
   "source": [
    "### Comprehensions\n",
    "\n",
    "A **comprehension** automatically builds a collection from an iterable, allowing you to write a `for` loop _directly inside_ of a new list. Take the following piece of code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "168a05ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "squares = []\n",
    "for number in range(1, 6):\n",
    "    squares.append(number ** 2)\n",
    "print(squares)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "115824e0",
   "metadata": {},
   "source": [
    "This can be written in one line, as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff665526",
   "metadata": {},
   "outputs": [],
   "source": [
    "squares = [number ** 2 for number in range(1, 6)]\n",
    "print(squares)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cddc852",
   "metadata": {},
   "source": [
    "We can even extend this comprehension with an `if` clause to filter items:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c036",
   "metadata": {},
   "outputs": [],
   "source": [
    "even_squares = [number ** 2 for number in range(1, 11) if number % 2 == 0]\n",
    "print(even_squares)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e7de95d",
   "metadata": {},
   "source": [
    "Using the same syntax with curly brackets makes for a set or dictionary comprehension. Dictionary comprehensions need `key: value`, whereas a set comprehension contains just one expression.\n",
    "\n",
    "**Predict:** Why are there only three values in `remainders`? If you're unsure, write out the comprehension as a full loop and trace each value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8736970",
   "metadata": {},
   "outputs": [],
   "source": [
    "lengths = {word: len(word) for word in [\"list\", \"tuple\", \"set\"]}\n",
    "remainders = {number % 3 for number in range(10)}\n",
    "\n",
    "print(lengths)\n",
    "print(remainders)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c038",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "**Note:** do not overuse comprehensions. Ordinary loops are often clearer when an operation has several steps, changes other objects, or needs error handling. Comprehensions are mostly useful for performing a short calculation whose result belongs in a new collection.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: build three comprehensions</b>\n",
    "\n",
    "Create (1) cubes of the integers from 1 to 5, (2) uppercase versions of the non-empty strings in `words`, and (3) a dictionary mapping each non-empty word to its first character.\n",
    "\n",
    "Why must the third comprehension skip empty strings?\n",
    "</div>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-c039",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "words = [\"python\", \"\", \"loops\", \"data\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-c040",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "An empty string has no item at index `0`, so accessing it would raise `IndexError`.\n",
    "\n",
    "```python\n",
    "words = [\"python\", \"\", \"loops\", \"data\"]\n",
    "\n",
    "cubes = [number ** 3 for number in range(1, 6)]\n",
    "uppercase_words = [word.upper() for word in words if word != \"\"]\n",
    "first_characters = {word: word[0] for word in words if word != \"\"}\n",
    "\n",
    "print(cubes)\n",
    "print(uppercase_words)\n",
    "print(first_characters)\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-consolidation-prompt",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "source": [
    "## Consolidation challenge\n",
    "\n",
    "Let's wrap up by practising what we've seen in this lecture one more time.\n",
    "\n",
    "<div class=\"alert alert-success\">\n",
    "<b>Try it yourself: summarise a temperature record</b>\n",
    "\n",
    "The code below records two weeks of temperatures, with each row running from Monday to Sunday. Unfortunately, the very first reading was entered incorrectly: change it to `10` directly in `weeks`. Then turn the string of day names into a list.\n",
    "\n",
    "For each week:\n",
    "\n",
    "1. Give it a week number;\n",
    "2. Print its Monday-to-Friday readings using a slice, followed by its mean temperature for all seven days;\n",
    "3. Use `zip()` and unpacking to print each day name alongside its reading; and\n",
    "4. Use a list comprehension to collect the names of days with readings of at least `13`, then display those names on one line, separated by commas.\n",
    "\n",
    "Before running your code, predict which days will meet that threshold in each week.\n",
    "\n",
    "What would happen to the `zip()`-based results if the list of day names contained only the first six names?\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "l10-consolidation-work",
   "metadata": {
    "tags": [
     "exercise"
    ]
   },
   "outputs": [],
   "source": [
    "day_names = \"Mon Tue Wed Thu Fri Sat Sun\"\n",
    "weeks = [[11, 12, 10, 13, 14, 12, 11], [9, 10, 12, 12, 13, 14, 13]]\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-consolidation-solution",
   "metadata": {
    "tags": [
     "solution"
    ]
   },
   "source": [
    "<details><summary><b>Click for a possible solution</b></summary>\n",
    "\n",
    "The days reaching at least 13°C are Thursday and Friday in week 1, and Friday, Saturday, and Sunday in week 2. With only the first six day names, `zip()` would stop after Saturday, leaving Sunday's reading out of both the daily output and the filtered day names. The weekday slices and weekly means would still use the readings in `weeks`.\n",
    "\n",
    "```python\n",
    "day_names = \"Mon Tue Wed Thu Fri Sat Sun\"\n",
    "weeks = [[11, 12, 10, 13, 14, 12, 11], [9, 10, 12, 12, 13, 14, 13]]\n",
    "\n",
    "weeks[0][0] = 10\n",
    "days = day_names.split()\n",
    "\n",
    "for week_number, readings in enumerate(weeks, start=1):\n",
    "    print(f\"Week {week_number}: weekdays = {readings[:5]}\")\n",
    "\n",
    "    print(f\"Mean = {sum(readings) / len(readings):.1f}°C\")\n",
    "\n",
    "    for day, reading in zip(days, readings):\n",
    "        print(f\"{day}: {reading}°C\")\n",
    "    \n",
    "    warm_days = [day for day, reading in zip(days, readings) if reading >= 13]\n",
    "    print(\"Days at or above 13°C:\", \", \".join(warm_days))\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "l10-consolidation-summary",
   "metadata": {
    "tags": [
     "summary"
    ]
   },
   "source": [
    "## Summary\n",
    "\n",
    "There was a lot in this notebook! You've seen both some new fundamental topics, such as hashability and tuples, and a goodie bag of convenient Python functionality like `enumerate()`, `zip()`, and comprehensions. Onwards!\n",
    "\n",
    "### Key terms\n",
    "\n",
    "<dl>\n",
    "\t<dt><b>Hash</b></dt>\n",
    "\t<dd>An integer or string that identifies an object, remaining stable over an object's lifetime, with equal objects having equal hashes.</dd>\n",
    "\t<dt><b>Hashability</b></dt>\n",
    "\t<dd>The property of a type that determines whether it can be hashed or not. Required for dictionary keys and set members.</dd>\n",
    "\t<dt><b>Hash collision</b></dt>\n",
    "\t<dd>When a hash function has assigned two objects the same hash; resolved by checking the objects themselves to see whether they're equal or not.</dd>\n",
    "\t<dt><b>Tuple</b></dt>\n",
    "\t<dd>An immutable ordered sequence. It is hashable only if all of its items are hashable.</dd>\n",
    "\t<dt><b>Unpacking</b></dt>\n",
    "\t<dd>Assigning the items of a sequence to several names in one statement.</dd>\n",
    "\t<dt><b>Two-dimensional list</b></dt>\n",
    "\t<dd>A list whose items are lists, often used as rows of a grid or table.</dd>\n",
    "\t<dt><b>Slice</b></dt>\n",
    "\t<dd>A selection from a sequence, specified by start, stop, and optionally step.</dd>\n",
    "\t<dt><b>Comprehension</b></dt>\n",
    "\t<dd>An expression that builds a collection by transforming, and optionally filtering, items from an iterable.</dd>\n",
    "</dl>\n",
    "\n",
    "### New syntax\n",
    "\n",
    "<dl>\n",
    "\t<dt><code>hash(value)</code></dt>\n",
    "\t<dd>Returns the hash of a hashable value; raises <code>TypeError</code> for an unhashable value.</dd>\n",
    "\t<dt><code>(a, b), (a,), ()</code></dt>\n",
    "\t<dd>A two-item tuple, a one-item tuple, and an empty tuple.</dd>\n",
    "\t<dt><code>first, second = pair</code></dt>\n",
    "\t<dd>Unpacks a two-item sequence into two names.</dd>\n",
    "\t<dt><code>grid[row][column]</code></dt>\n",
    "\t<dd>Selects an item in a nested list using two indices.</dd>\n",
    "\t<dt><code>sequence[start:stop:step]</code></dt>\n",
    "\t<dd>Selects items with an exclusive stop boundary and an optional step.</dd>\n",
    "\t<dt><code>enumerate(items, start=0)</code></dt>\n",
    "\t<dd>Supplies counter-and-item tuples; the counter starts at zero unless specified.</dd>\n",
    "\t<dt><code>zip(first, second)</code></dt>\n",
    "\t<dd>Supplies tuples of corresponding items, stopping at the shortest iterable by default.</dd>\n",
    "\t<dt><code>text.split(separator), separator.join(strings)</code></dt>\n",
    "\t<dd>Splits text into strings, or combines strings with a separator.</dd>\n",
    "\t<dt><code>[expression for item in source if condition]</code></dt>\n",
    "\t<dd>Builds a list with an optional filter. Use curly brackets for a set, or <code>{key: value for ...}</code> for a dictionary.</dd>\n",
    "</dl>\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
