{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Chapter 8: Mappings & Sets" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "While [Chapter 7](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/07_sequences_00_lecture.ipynb) focuses on one special kind of *collection* types, namely *sequences*, this chapter introduces two more kinds: **Mappings** and **sets**. We present the data types belonging to these two groups in one chapter as they share the *same* underlying implementation at the C Level, known as **[hash tables](https://en.wikipedia.org/wiki/Hash_table)**.\n", "\n", "The most important mapping type in this chapter is the `dict` type that we have not yet seen before (cf, [documentation](https://docs.python.org/3/library/stdtypes.html#dict)). It is an essential part in a data science practitioner's toolbox for two reasons: First, Python employs `dict` objects basically \"everywhere\" internally. So, we must understand how they work to become better at Python in general. Second, after the many concepts related to *sequential* data, the ideas behind *mappings* enhance our general problem solving skills. As a concrete example, we look at the concept of **memoization** to complete our picture of *recursion*, as depicted in [Chapter 4](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/04_iteration_00_lecture.ipynb#Recursion). We end this chapter with a discussion of *set* types." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## The `dict` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "A *mapping* is a one-to-one correspondence from a set of **keys** to a set of **values**. In other words, a *mapping* is a *collection* of **key-value pairs**, also called **items** for short.\n", "\n", "In the context of mappings, the term *value* has a meaning different from the general *value* that *every* object has: In the \"bag\" analogy from [Chapter 1](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/01_elements_00_lecture.ipynb#Value-/-\"Meaning\"), we descibe an object's value to be the concrete $0$s and $1$s it contains. Here, the terms *key* and *value* mean the *role* an object takes within a mapping. Both, *keys* and *values*, are real *objects* with a distinct *value*. So, the student should always remember the double meaning of the term *value* in this chapter!\n", "\n", "Let's continue with an example. To create a `dict` object, we commonly use the literal notation, `{..: .., ..: .., ...}`, and list all the items. `to_words` below maps the `int` objects `0`, `1`, and `2` to their English word equivalents, `\"zero\"`, `\"one\"`, and `\"two\"`, and `from_words` does the opposite. A stylistic side note: Pythonistas often expand `dict` or `list` definitions by writing each item or element on a line on their own. The commas `,` after the *last* items are *not* a mistake, as well, although they *may* be left out. Besides easier reading, such style has actual technical advantages (cf., [source](https://www.python.org/dev/peps/pep-0008/#when-to-use-trailing-commas)) that we do not go into detail about here." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "to_words = {\n", " 0: \"zero\",\n", " 1: \"one\",\n", " 2: \"two\",\n", "}" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "from_words = {\n", " \"zero\": 0,\n", " \"one\": 1,\n", " \"two\": 2,\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "As before, `dict` objects are objects on their own: They have an identity, a type, and a value. The latter is a *literal* that creates a *new* `dict` object with the *same* value when evaluated." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "139953979690352" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(from_words)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "dict" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(from_words)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [dict()](https://docs.python.org/3/library/functions.html#func-dict) built-in gives us an alternative way to create a `dict` object. It is versatile and can be used in different ways.\n", "\n", "First, we may pass it any *mapping* type, for example, a `dict` object, to obtain a *new* `dict` object. That is the easiest way to convert a more specialized mapping type, such as the `OrderedDict`, `defaultdict`, and `Counter` types introduced further below, into an \"ordinary\" `dict` object." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(from_words)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Second, we may pass it an *iterable* of *iterables* with *two* elements each. So, both of the following two code cells work: A `list` of `tuple` objects, or a `tuple` of `list` objects. More importantly, we could use an *iterator*, for example, a `generator` object, that produces the inner iterables \"on the fly.\"" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict([(\"zero\", 0), (\"one\", 1), (\"two\", 2)])" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(([\"zero\", 0], [\"one\", 1], [\"two\", 2]))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Lastly, [dict()](https://docs.python.org/3/library/functions.html#func-dict) may also be called with *keyword* arguments: The keywords become the keys and the arguments the values." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(zero=0, one=1, two=2)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Keyword arguments may always be added in the first two cases as well. That is sometimes useful to take data as is and ensure that certain keys are *existent* and have a pre-defined value." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2, 'three': 3}" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(from_words, three=3)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Nested Data" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Often, `dict` objects occur in a nested form and combined with other collection types, such as `list` or `tuple` objects, to model more complex \"objects\" from the real world.\n", "\n", "The reason for this popularity is that many modern [ReST APIs](https://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_Web_services) on the internet (e.g., [Google Maps API](https://cloud.google.com/maps-platform/), [Yelp API](https://www.yelp.com/developers/documentation/v3), [Twilio API](https://www.twilio.com/docs/usage/api)) provide their data in the popular [JSON](https://en.wikipedia.org/wiki/JSON) format, which looks almost like a combination of `dict` and `list` objects in Python. \n", "\n", "The `people` example below models three groups of people: Mathematicians, physicists, and programmers. Each person may have an arbitrary number of email addresses (e.g., [Leonhard Euler](https://en.wikipedia.org/wiki/Leonhard_Euler) has not lived long enough to get one, whereas [Guido](https://en.wikipedia.org/wiki/Guido_van_Rossum) has more than one for sure).\n", "\n", "`people` has many (implicit) structural assumptions built in. For example, there are a [one-to-many](https://en.wikipedia.org/wiki/One-to-many_%28data_model%29) relationship between people and their email addresses and a [one-to-one](https://en.wikipedia.org/wiki/One-to-one_%28data_model%29) relationship between each person and their name. It is important to understand that we determine this structure by choosing the data types involved in `people` and that it is impossible to model nested data without any (implicit) assumption about the structure. So, the data science practitioner should have a basic understanding of [database normalization](https://en.wikipedia.org/wiki/Database_normalization)." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "people = {\n", " \"mathematicians\": [\n", " {\n", " \"name\": \"Gilbert Strang\",\n", " \"emails\": [\"gilbert@mit.edu\"],\n", " },\n", " {\n", " \"name\": \"Leonhard Euler\",\n", " \"emails\": [],\n", " },\n", " ],\n", " \"physicists\": [],\n", " \"programmers\": [\n", " {\n", " \"name\": \"Guido\",\n", " \"emails\": [\"guido@python.org\", \"guido@dropbox.com\"],\n", " },\n", " ],\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The literal notation of such a nested `dict` object may be hard to read." ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{'mathematicians': [{'name': 'Gilbert Strang', 'emails': ['gilbert@mit.edu']},\n", " {'name': 'Leonhard Euler', 'emails': []}],\n", " 'physicists': [],\n", " 'programmers': [{'name': 'Guido',\n", " 'emails': ['guido@python.org', 'guido@dropbox.com']}]}" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "people" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Luckily, the [pprint](https://docs.python.org/3/library/pprint.html) module in the [standard library](https://docs.python.org/3/library/index.html) provides a [pprint()](https://docs.python.org/3/library/pprint.html#pprint.pprint) function for \"pretty printing.\"" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "from pprint import pprint" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'mathematicians': [{'emails': ['gilbert@mit.edu'],\n", " 'name': 'Gilbert Strang'},\n", " {'emails': [],\n", " 'name': 'Leonhard Euler'}],\n", " 'physicists': [],\n", " 'programmers': [{'emails': ['guido@python.org',\n", " 'guido@dropbox.com'],\n", " 'name': 'Guido'}]}\n" ] } ], "source": [ "pprint(people, indent=1, width=60)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Hash Tables & Key Hashability" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In [Chapter 0](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/00_intro_00_lecture.ipynb#Isn't-C-a-lot-faster?), we argue that a major advantage of using Python is that it takes care of the memory managment for us. In line with that, we have never talked about the C level implementation thus far in the book. However, the `dict` type, among others, exhibits some behaviors that may seem \"weird\" for a beginner. To built a solid intuition that enables the student to better \"predict\" how `dict` objects behave, we describe the underlying implementation details on a conceptual level (i.e., without C code).\n", "\n", "The first unintuitive behavior is that we may *not* use a *mutable* object as a key. That results in a `TypeError`." ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "TypeError", "evalue": "unhashable type: 'list'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m {\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m\"zero\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"one\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m }\n", "\u001b[0;31mTypeError\u001b[0m: unhashable type: 'list'" ] } ], "source": [ "{\n", " [0, 1]: [\"zero\", \"one\"],\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Similarly surprising is that items with the *same* key get \"merged\" together. The resulting `dict` object seems to keep the position of the *first* mention of a key, while at the same time only the *last* mention of a value survives." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 3, 'one': 1, 'two': 2}" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{\n", " \"zero\": 0,\n", " \"one\": 1,\n", " \"two\": 2,\n", " \"zero\": 3,\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The reason for that is that the main building block behind the `dict` type is a [data structure](https://en.wikipedia.org/wiki/Data_structure) called a [hash table](https://en.wikipedia.org/wiki/Hash_table).\n", "\n", "Conceptually, when we create a *new* `dict` object, Python creates a \"bag\" in memory that takes significantly more space (i.e., $0$s and $1$s) than needed to store the references to all the key and value objects. This bag is a **contiguous** chunk of memory (i.e., all the $0$s and $1$s lie right next to each other) and divided into equally sized **buckets** that have just enough space to store *two* references each. These references go to an item's key and value objects. The buckets are labeled with *index* numbers, or \"integer addresses.\" Because Python knows how wide each bucket is and where the bag begins, it can jump directly into *any* bucket by calculating its **offset** from the start. It does not have to follow a reference to some \"random\" memory location once it has followed the reference to the `dict` object's \"start\" in memory.\n", "\n", "The figure below visualizes how we should think of hash tables. An empty `dict` object, created with the literal `{}`, still takes a lot of memory: It is essentially one big, contiguous, and empty table." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "| Bucket | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |\n", "| :---: |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n", "| **Key** |*...*|*...*|*...*|*...*|*...*|*...*|*...*|*...*|\n", "|**Value**|*...*|*...*|*...*|*...*|*...*|*...*|*...*|*...*|" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To insert a key-value pair, the key must be translated into a bucket's address. As the first step to do so, the built-in [hash()](https://docs.python.org/3/library/functions.html#hash) function maps any **hashable** object to its **hash value**, a long `int` number, similar to the ones returned by the built-in [id()](https://docs.python.org/3/library/functions.html#id) function. This hash value is a summary of all the $0$s and $1$s that make up an object's value, and, according to the official [glossary](https://docs.python.org/3/glossary.html#term-hashable), an object is hashable *only if* \"it has a hash value which *never* changes during its *lifetime*.\" So, hashability implies immutability! This formal requirement is also absolutely useful to have: Without it, an object may have *several* addresses and, thus, we could not sort it into a *predictable* bucket. The exact logic behind [hash()](https://docs.python.org/3/library/functions.html#hash) is beyond the scope of this book.\n", "\n", "Let's calculate the hash value of `\"zero\"`. Because `str` objects are immutable, that works. Hash values have *no* semantic meaning. Also, everytime we re-start Python, we see *different* hash values for the *same* objects. That is a security measure, and we do not go into the technicalities here (cf. [source](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED))." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "-1033934764475250610" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hash(\"zero\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "For numeric objects, we can sometimes predict the hash values. However, we must *never* interpret any meaning into them." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hash(0)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "230584300921369408" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hash(0.1)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [glossary](https://docs.python.org/3/glossary.html#term-hashable) states a second requirement for hashability, namely that \"objects which *compare equal* must have the *same* hash value.\" The purpose of this is to ensure that if we put, for example, `1` as a key in a `dict` object, we can look it up later with `1.0`. In other words, we can look up keys by their object's value (i.e., in the meaning of [Chapter 1](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/01_elements_00_lecture.ipynb#Value-/-\"Meaning\")). The converse statement does *not* hold: Two objects *may* (accidentally) have the *same* hash value and *not* compare equal." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "1 == 1.0" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hash(1) == hash(1.0)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Because `list` objects are not immutable, they are *never* hashable, as indicated by the `TypeError`." ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "TypeError", "evalue": "unhashable type: 'list'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mhash\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: unhashable type: 'list'" ] } ], "source": [ "hash([0, 1])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If we need keys composed of several objects, we can use `tuple` objects instead. In general, the key object must be hashable as a whole. So, we must *never* put a *mutable* object in a `tuple` object used as a key, as well." ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "3713080549409410656" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "hash((0, 1))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "There is no such restiction on objects inserted into `dict` objects as *values*." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{(0, 1): ['zero', 'one']}" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{\n", " (0, 1): [\"zero\", \"one\"],\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "A natural question to ask is how does Python know how much memory it should reserve for a `dict` object's hash table. And, the answer is: Python allocates the memory according to some internal heuristics, and whenever a hash table is roughly 2/3 full, it creates a *new* one with twice the space, and re-inserts all items, one by one, from the *old* one. So, during its lifetime, a `dict` object may have several hash tables.\n", "\n", "Let's see how Python translates the keys' hash values into buckets and what happens if a hash table gets too crowded. Assume the now bigger `from_words` example ..." ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "from_words = {\n", " \"zero\": 0,\n", " \"one\": 1,\n", " \"two\": 2,\n", " \"three\": 3,\n", " \"four\": 4,\n", " \"five\": 5,\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... is to be stored in a hash table with eight buckets. Thus, to label the buckets, we need three bits (i.e., $2^3 = 8$).\n", "\n", "Once Python has obtained an object's hash value with [hash()](https://docs.python.org/3/library/functions.html#hash), that number's *least* significant bits in *binary* representation are used as the address. So, here, we cut off the last three digits. These digits could be converted back into an integer for nicer presentation, but Python itself does not need that.\n", "\n", "In summary, with a potentially infinite number of possible keys being mapped on a limited number of buckets, there is a chance that two or more keys end up in the *same* bucket. That is called a **hash collision** and sounds worse than it is. In such cases, Python uses a perturbation rule to rearrange the bits, and if the corresponding next bucket is empty, places an item there. The main disadvantage of that is that the nice offsetting logic from above breaks down, and Python needs more time on average to place items into a hash table. The remedy is to just use a bigger hash table as then the chance of colliding hashes decreases. Luckily, Python does all that for us in the background. So, the main cost we pay for that convenience is the *high* memory usage of `dict` objects.\n", "\n", "Another effect of this hashing logic is that items that have keys with the *same* value (i.e., $0$s and $1$s) end up in the *same* bucket, as well. The item that gets inserted last *overwrites* all previously inserted items. That is why the two `\"zero\"` keys get \"merged\" above.\n", "\n", "The code below shows how the key objects are transformed into their hash values in integer and binary representation, the least significant bits of the latter are cut off, and the buckets are obtained. Because we use `str` objects as the keys, their buckets are *unpredictable*. So, taking into account the randomization for security purposes from above, we may or may not see redundant buckets in the output. The chance is rather high." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def buckets(mapping, *, bits):\n", " \"\"\"Calculate the bucket indices for a mapping's keys.\"\"\"\n", " for key in mapping: # cf., next sub-section for details on looping\n", " hash_value = hash(key)\n", " binary = bin(hash_value)\n", " address = binary[-bits:]\n", " bucket = int(\"0b\" + address, base=2)\n", " print(key, hash_value, \"0b...\" + binary[-12:], address, bucket, sep=\"\\t\")" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "zero\t-1033934764475250610\t0b...011110110010\t010\t2\n", "one\t-3496683303036773129\t0b...001100001001\t001\t1\n", "two\t-4468022785493412346\t0b...100111111010\t010\t2\n", "three\t1491334499222274950\t0b...001110000110\t110\t6\n", "four\t490235692124377396\t0b...000100110100\t100\t4\n", "five\t-550474145318920636\t0b...000110111100\t100\t4\n" ] } ], "source": [ "buckets(from_words, bits=3)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "With five of the six keys already inserted, the next insertion has a chance of more than 50% to be a hash collision." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "| Bucket | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |\n", "| :---: | :---: |:---:| :---: | :---: |:---:| :---: |:---:| :---: |\n", "| **Key** |`\"one\"`|*...*|`\"two\"`|`\"four\"`|*...*|`\"three\"`|*...*|`\"zero\"`|\n", "|**Value**| `1` |*...*| `2` | `4` |*...*| `3` |*...*| `0` |" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Luckily, in the given case, Python allocates sixteen buckets trading off memory against insertion speed." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "-" } }, "source": [ "| Bucket | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |\n", "| :---: | :---: |:---:|:---:|:---:|:---:|:---:|:---:| :---: |:---:|:---:| :---: | :---: |:---:| :---: |:---:| :---: |\n", "| **Key** |`\"one\"`|*...*|*...*|*...*|*...*|*...*|*...*|`\"five\"`|*...*|*...*|`\"two\"`|`\"four\"`|*...*|`\"three\"`|*...*|`\"zero\"`|\n", "|**Value**| `1` |*...*|*...*|*...*|*...*|*...*|*...*| `5` |*...*|*...*| `2` | `4` |*...*| `3` |*...*| `0` |" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Sixteen buckets imply four bits (i.e., $2^4$) be cut off from the hash value's binary representation and used as the buckets' indices. It is unlikely we see redundant buckets in the code cell below." ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "zero\t-1033934764475250610\t0b...011110110010\t0010\t2\n", "one\t-3496683303036773129\t0b...001100001001\t1001\t9\n", "two\t-4468022785493412346\t0b...100111111010\t1010\t10\n", "three\t1491334499222274950\t0b...001110000110\t0110\t6\n", "four\t490235692124377396\t0b...000100110100\t0100\t4\n", "five\t-550474145318920636\t0b...000110111100\t1100\t12\n" ] } ], "source": [ "buckets(from_words, bits=4)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Although hash tables seem quite complex at first sight, they help us to make certain operations very fast as we see further below." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Mappings are Collections without \"Predictable\" Order" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In [Chapter 7](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/07_sequences_00_lecture.ipynb#Collections-vs.-Sequences), we show how *sequences* are a special kind of *collections*. The latter can be described as iterable containers with a finite number of elements.\n", "\n", "The `dict` type is a special kind of a *collection*, as well, as revealed with the `Collection` ABC from the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module in the [standard library](https://docs.python.org/3/library/index.html)." ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import collections.abc as abc" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(to_words, abc.Collection)" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(from_words, abc.Collection)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "So, we may pass `to_words` or `from_words` to the built-in [len()](https://docs.python.org/3/library/functions.html#len) function to obtain the number of *items* they contain." ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(to_words)" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "6" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(from_words)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In the terminology of the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module, both are `Sized` objects." ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(to_words, abc.Sized)" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(from_words, abc.Sized)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`dict` objects may be looped over, for example, with the `for` statement. For technical reasons, we could *not* rely on the iteration order to be *predictable* in any form until Python 3.7 in 2018. Looping over the *same* `dict` objects multiple times during its lifetime could result in *different* iteration orders every time. That behavior is intentional as `dict` objects are optimized for use cases where order does not matter. Starting with Python 3.7, `dict` objects remember the order in that items are *inserted* (cf., [Python 3.7 release notes](https://www.python.org/downloads/release/python-370/)). A lot of research went into this preservation of order (cf., this [PyCon 2017 talk](https://www.youtube.com/watch?v=npw4s1QTmPg) by core developer [Raymond Hettinger](https://github.com/rhettinger)).\n", "\n", "Because of that, the order in the two `for`-loops below is the *same* as in the *source code* that defines `to_words` and `from_words` above. In that sense, it is \"*predictable*.\" However, if we fill `dict` objects with data from real-world sources, that kind of predictability is not really helpful as such data are not written as source code, and, thus, we consider the order of items in `dict` objects to be *unpredictable*. Further, when an \"insertion\" accidentally *updates* an item, the ordering remains unchanged." ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Python 3.7.4\n" ] } ], "source": [ "!python --version # the order in the for-loops is predictable only for Python 3.7 or higher" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "By convention, iteration goes over the *keys* in the `dict` object only. The \"*Dictionary Methods*\" sub-section below shows how to loop over the *items* or the *values* instead." ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n", "1\n", "2\n" ] } ], "source": [ "for number in to_words:\n", " print(number)" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "zero\n", "one\n", "two\n", "three\n", "four\n", "five\n" ] } ], "source": [ "for word in from_words:\n", " print(word)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Without a predictable *forward* order, `dict` objects are not *reversible* either.\n", "\n", "So, passing a `dict` object to the [reversed()](https://docs.python.org/3/library/functions.html#reversed) built-in raises a `TypeError` ..." ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "TypeError", "evalue": "'dict' object is not reversible", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mnumber\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mreversed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mto_words\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumber\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: 'dict' object is not reversible" ] } ], "source": [ "for number in reversed(to_words):\n", " print(number)" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "TypeError", "evalue": "'dict' object is not reversible", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mword\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mreversed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfrom_words\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mword\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: 'dict' object is not reversible" ] } ], "source": [ "for word in reversed(from_words):\n", " print(word)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... and also `Reversible` ABC in the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module confirms that." ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(to_words, abc.Reversible)" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(from_words, abc.Reversible)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Of course, we could use the built-in [sorted()](https://docs.python.org/3/library/functions.html#sorted) function to loop over, for example, `to_words` in a *predictable* order. However, that *materializes* a temporary `list` object in memory containing references to all the key objects *and* creates a *new* order according to some sorting criterion that has *nothing* to do with how the items are ordered inside the hash table." ] }, { "cell_type": "code", "execution_count": 43, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "five\n", "four\n", "one\n", "three\n", "two\n", "zero\n" ] } ], "source": [ "for word in sorted(from_words):\n", " print(word)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "That approach may be combined with [reversed()](https://docs.python.org/3/library/functions.html#reversed)." ] }, { "cell_type": "code", "execution_count": 44, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "zero\n", "two\n", "three\n", "one\n", "four\n", "five\n" ] } ], "source": [ "for word in reversed(sorted(from_words)):\n", " print(word)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To show the third behavior of *collection* types, we use the boolean `in` operator to check if a given and immutable object evaluates equal to a *key* in `to_words` or `from_words`." ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "1 in to_words" ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ "1.0 in to_words # 1.0 is not contained but compares equal to a key that is" ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "10 in to_words" ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\"one\" in from_words" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\"ten\" in from_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The `Container` ABC in the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module may be used to \"ask\" Python to confirm that `to_words` and `from_words` are indeed *container* types." ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 50, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(to_words, abc.Container)" ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(from_words, abc.Container)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### Membership Testing: Lists vs. Dictionaries" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Because of the [hash table](https://en.wikipedia.org/wiki/Hash_table) implementation, the `in` operator is *extremely* fast: With the hashing/offsetting described above, Python does *not* need to initiate a [linear search](https://en.wikipedia.org/wiki/Linear_search) as in the `list` case but immediately knows the only places in memory where the searched object must be located if present in the hash table at all. Conceptually, that is like comparing the searched object against all key objects with the `==` operator *without* actually doing it.\n", "\n", "To show the difference, we run a little experiment. For that, we create a `haystack`, a `list` object, with `10_000_001` elements in it, *one* of which is the `needle`, namely `42`. Once again, the [randint()](https://docs.python.org/3/library/random.html#random.randint) function in the [random](https://docs.python.org/3/library/random.html) module is helpful." ] }, { "cell_type": "code", "execution_count": 52, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "import random" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "random.seed(87)" ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "needle = 42" ] }, { "cell_type": "code", "execution_count": 55, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "haystack = [random.randint(99, 9999) for _ in range(10_000_000)]\n", "haystack.append(needle)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We put the elements in `haystack` in a *random* order with the [shuffle()](https://docs.python.org/3/library/random.html#random.shuffle) function in the [random](https://docs.python.org/3/library/random.html) module." ] }, { "cell_type": "code", "execution_count": 56, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "random.shuffle(haystack)" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "[8126, 7370, 3735, 213, 7922, 1434, 8557, 9609, 9704, 9564]" ] }, "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "haystack[:10]" ] }, { "cell_type": "code", "execution_count": 58, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "[7237, 886, 5945, 4014, 4998, 2055, 3531, 6919, 7875, 1944]" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "haystack[-10:]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "As modern computers are generally fast, we search the `needle` a total of `10` times." ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4.32 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1 -r 1\n", "for _ in range(10):\n", " needle in haystack" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Now, we convert the elements of `haystack` into the keys of `magic_haystack`, a `dict` object. We use `None` as a dummy value for all items." ] }, { "cell_type": "code", "execution_count": 60, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "magic_haystack = dict((x, None) for x in haystack)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To show the *massive* effect of the hash table implementation, we search the `needle` not `10` but `10_000_000` times. The code cell still runs in only a fraction of the time its counterpart does above." ] }, { "cell_type": "code", "execution_count": 61, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "419 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1 -r 1\n", "for _ in range(10_000_000):\n", " needle in magic_haystack" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "However, there is no fast way to look up the values the keys are mapped to. To achieve that, we have to loop over *all* items and check for each value object if it evaluates equal to searched object. That is, by definition, a linear search, as well, and rather slow. In the context of `dict` objects, we call that a **reverse look-up**." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### \"Indexing\" -> Key Look-up" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The same efficient key look-up executed in the background with the `in` operator is also behind the indexing operator `[]`. Instead of returning either `True` or `False`, it returns the value object the looked up key maps to.\n", "\n", "To show the similarity to indexing into `list` objects, we provide another example with `to_words_list` below." ] }, { "cell_type": "code", "execution_count": 62, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "to_words_list = [\"zero\", \"one\", \"two\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Without the above definitions, we could not tell the difference between `to_words` and `to_words_list`: The usage of the `[]` is the same." ] }, { "cell_type": "code", "execution_count": 63, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "'zero'" ] }, "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words[0]" ] }, { "cell_type": "code", "execution_count": 64, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "'one'" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words_list[1]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Because key objects can be of any immutable type and are, in particular, not constrained to just the `int` type, the word \"*indexing*\" is an understatement here. Therefore, in the context of `dict` objects, we view the `[]` operator as a generalization of the indexing operator and refer to it as the **(key) look-up** operator. " ] }, { "cell_type": "code", "execution_count": 65, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 65, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words[\"two\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If a key is not in a `dict` object, Python raises a `KeyError`. A sequence type would raise an `IndexError` in this situation." ] }, { "cell_type": "code", "execution_count": 66, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "KeyError", "evalue": "'drei'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mfrom_words\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"drei\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mKeyError\u001b[0m: 'drei'" ] } ], "source": [ "from_words[\"drei\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "While `dict` objects support the `[]` operator to look up a *single* key, the more general concept of *slicing* is *not* available. That is in line with the idea that there is *no* predictable *order* associated with a `dict` object's keys, and slicing requires an order conceptually.\n", "\n", "To access \"lower\" levels in nested data, like `people`, we *chain* the look-up operator `[]`. For example, let's view all the mathematicians in `people`." ] }, { "cell_type": "code", "execution_count": 67, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "[{'name': 'Gilbert Strang', 'emails': ['gilbert@mit.edu']},\n", " {'name': 'Leonhard Euler', 'emails': []}]" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "people[\"mathematicians\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Let's take the first mathematician on the list, ..." ] }, { "cell_type": "code", "execution_count": 68, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'name': 'Gilbert Strang', 'emails': ['gilbert@mit.edu']}" ] }, "execution_count": 68, "metadata": {}, "output_type": "execute_result" } ], "source": [ "people[\"mathematicians\"][0]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... and output his name ..." ] }, { "cell_type": "code", "execution_count": 69, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "'Gilbert Strang'" ] }, "execution_count": 69, "metadata": {}, "output_type": "execute_result" } ], "source": [ "people[\"mathematicians\"][0][\"name\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... or all his emails." ] }, { "cell_type": "code", "execution_count": 70, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "['gilbert@mit.edu']" ] }, "execution_count": 70, "metadata": {}, "output_type": "execute_result" } ], "source": [ "people[\"mathematicians\"][0][\"emails\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Mutability" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Analogous to `list` objects, we may mutate `dict` objects *in place*.\n", "\n", "For example, let's translate the English words in `to_words` to their German counterparts. Behind the scenes, Python determines the bucket of the objects passed to the `[]` operator, looks them up in the hash table, and, if present, *updates* their references to the mapped value objects." ] }, { "cell_type": "code", "execution_count": 71, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "to_words[0] = \"null\"\n", "to_words[1] = \"eins\"\n", "to_words[2] = \"zwei\"" ] }, { "cell_type": "code", "execution_count": 72, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{0: 'null', 1: 'eins', 2: 'zwei'}" ] }, "execution_count": 72, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Let's add two more items. Again, Python determines their buckets, but this time finds them to be empty, and *inserts* the references to their key and value objects." ] }, { "cell_type": "code", "execution_count": 73, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "to_words[3] = \"drei\"\n", "to_words[4] = \"vier\"" ] }, { "cell_type": "code", "execution_count": 74, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{0: 'null', 1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" ] }, "execution_count": 74, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "None of these operations change the identity of the `to_words` object." ] }, { "cell_type": "code", "execution_count": 75, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "139953979638320" ] }, "execution_count": 75, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(to_words) # same memory location as before" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The `del` statement removes individual items. Python just removes the *two* references to the key and value objects in the corresponding bucket." ] }, { "cell_type": "code", "execution_count": 76, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "del to_words[0]" ] }, { "cell_type": "code", "execution_count": 77, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" ] }, "execution_count": 77, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We may also change parts of nested data, such as `people`.\n", "\n", "For example, let's add [Albert Einstein](https://en.wikipedia.org/wiki/Albert_Einstein) to the list of physicists, ..." ] }, { "cell_type": "code", "execution_count": 78, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "people[\"physicists\"].append({\"name\": \"Albert Einstein\"})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... complete Guido's name, ..." ] }, { "cell_type": "code", "execution_count": 79, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "people[\"programmers\"][0][\"name\"] = \"Guido van Rossum\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... and remove his work email because he retired." ] }, { "cell_type": "code", "execution_count": 80, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "del people[\"programmers\"][0][\"emails\"][1]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Now, `people` looks like this." ] }, { "cell_type": "code", "execution_count": 81, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'mathematicians': [{'emails': ['gilbert@mit.edu'],\n", " 'name': 'Gilbert Strang'},\n", " {'emails': [],\n", " 'name': 'Leonhard Euler'}],\n", " 'physicists': [{'name': 'Albert Einstein'}],\n", " 'programmers': [{'emails': ['guido@python.org'],\n", " 'name': 'Guido van Rossum'}]}\n" ] } ], "source": [ "pprint(people, indent=1, width=60)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Dictionary Methods" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`dict` objects come with many methods bound on them (cf., [documentation](https://docs.python.org/3/library/stdtypes.html#dict)), many of which are standardized by the `Mapping` and `MutableMapping` ABCs from the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module. While the former requires a data type adhering to it to implement the [keys()](https://docs.python.org/3/library/stdtypes.html#dict.keys), [values()](https://docs.python.org/3/library/stdtypes.html#dict.values), [items()](https://docs.python.org/3/library/stdtypes.html#dict.items), and [get()](https://docs.python.org/3/library/stdtypes.html#dict.get) methods, which *never* mutate an object, the latter formalizes the [update()](https://docs.python.org/3/library/stdtypes.html#dict.update), [pop()](https://docs.python.org/3/library/stdtypes.html#dict.pop), [popitem()](https://docs.python.org/3/library/stdtypes.html#dict.popitem), [clear()](https://docs.python.org/3/library/stdtypes.html#dict.clear), and [setdefault()](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) methods, which *may* do so." ] }, { "cell_type": "code", "execution_count": 82, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(to_words, abc.Mapping)" ] }, { "cell_type": "code", "execution_count": 83, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 83, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(from_words, abc.Mapping)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "While iteration over a mapping type goes over its keys, we may emphasize this explicitly by adding the [keys()](https://docs.python.org/3/library/stdtypes.html#dict.keys) method in the `for`-loop. Again, the iteration order is equivalent to the insertion order but still considered *unpredictable*." ] }, { "cell_type": "code", "execution_count": 84, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "zero\n", "one\n", "two\n", "three\n", "four\n", "five\n" ] } ], "source": [ "for word in from_words.keys():\n", " print(word)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "[keys()](https://docs.python.org/3/library/stdtypes.html#dict.keys) returns an object of type `dict_keys`. That is a dynamic **view** inside the `from_words`'s hash table, which means it does *not* copy the references to the keys, and changes to `from_words` can be seen through it. View objects behave much like `dict` objects themselves." ] }, { "cell_type": "code", "execution_count": 85, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "dict_keys(['zero', 'one', 'two', 'three', 'four', 'five'])" ] }, "execution_count": 85, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words.keys()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "iews can be materialized with the [list()](https://docs.python.org/3/library/functions.html#func-list) built-in. However, that may introduce *semantic* errors into a program an the newly created `list` object has a \"*predictable*\" order (i.e., indexes `0`, `1`, ...) created from an *unpredictable* one." ] }, { "cell_type": "code", "execution_count": 86, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "['zero', 'one', 'two', 'three', 'four', 'five']" ] }, "execution_count": 86, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(from_words.keys())" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To loop over the value objects instead, we use the [values()](https://docs.python.org/3/library/stdtypes.html#dict.values) method. That returns a *view* on the value objects inside `from_words` without copying the references to them." ] }, { "cell_type": "code", "execution_count": 87, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n", "1\n", "2\n", "3\n", "4\n", "5\n" ] } ], "source": [ "for number in from_words.values():\n", " print(number)" ] }, { "cell_type": "code", "execution_count": 88, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "dict_values([0, 1, 2, 3, 4, 5])" ] }, "execution_count": 88, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words.values()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To loop over key-value pairs, we invoke the [items()](https://docs.python.org/3/library/stdtypes.html#dict.items) method. That returns a view on the key-value pairs as `tuple` objects, where the first element is the key and the second the value. Because of that, we use tuple unpacking in the `for`-loop." ] }, { "cell_type": "code", "execution_count": 89, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "zero -> 0\n", "one -> 1\n", "two -> 2\n", "three -> 3\n", "four -> 4\n", "five -> 5\n" ] } ], "source": [ "for word, number in from_words.items():\n", " print(f\"{word} -> {number}\")" ] }, { "cell_type": "code", "execution_count": 90, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "dict_items([('zero', 0), ('one', 1), ('two', 2), ('three', 3), ('four', 4), ('five', 5)])" ] }, "execution_count": 90, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words.items()" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Above, we see how the look-up operator fails *loudly* with a `KeyError` if a key is *not* in a `dict` object. For example, `to_words` does *not* have a key `0` any more." ] }, { "cell_type": "code", "execution_count": 91, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" ] }, "execution_count": 91, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "code", "execution_count": 92, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "KeyError", "evalue": "0", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mto_words\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mKeyError\u001b[0m: 0" ] } ], "source": [ "to_words[0]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "That may be mitigated with the [get()](https://docs.python.org/3/library/stdtypes.html#dict.get) method that takes two arguments: `key` and `default`. It returns the value object `key` maps to if it is in the `dict` object; otherwise, `default` is returned. If not provided, `default` is `None`." ] }, { "cell_type": "code", "execution_count": 93, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "'n/a'" ] }, "execution_count": 93, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words.get(0, \"n/a\")" ] }, { "cell_type": "code", "execution_count": 94, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "'eins'" ] }, "execution_count": 94, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words.get(1, \"n/a\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`dict` objects are *mutable* as can be formally verified with the `MutableMapping` ABC from the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module." ] }, { "cell_type": "code", "execution_count": 95, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 95, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(to_words, abc.MutableMapping)" ] }, { "cell_type": "code", "execution_count": 96, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 96, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(from_words, abc.MutableMapping)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [update()](https://docs.python.org/3/library/stdtypes.html#dict.update) method takes the items of another mapping and either inserts them or overwrites the ones with matching keys already in the `dict` objects. It may be used in the other two ways as the [dict()](https://docs.python.org/3/library/functions.html#func-dict) built-in allows, as well." ] }, { "cell_type": "code", "execution_count": 97, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" ] }, "execution_count": 97, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "code", "execution_count": 98, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "to_spanish = {\n", " 1: \"uno\",\n", " 2: \"dos\",\n", " 3: \"tres\",\n", " 4: \"cuatro\",\n", " 5: \"cinco\", \n", "}" ] }, { "cell_type": "code", "execution_count": 99, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "to_words.update(to_spanish)" ] }, { "cell_type": "code", "execution_count": 100, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1: 'uno', 2: 'dos', 3: 'tres', 4: 'cuatro', 5: 'cinco'}" ] }, "execution_count": 100, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In contrast to the `pop()` method of the `list` type, the [pop()](https://docs.python.org/3/library/stdtypes.html#dict.pop) method of the `dict` type *requires* a `key` argument to be passed. Then, it removes the corresponding key-value pair *and* returns the value object. If the `key` is not in the `dict` object, a `KeyError` is raised. With an optional `default` argument, that loud error may be suppressed and the `default` returned instead, just as with the [get()](https://docs.python.org/3/library/stdtypes.html#dict.get) method above." ] }, { "cell_type": "code", "execution_count": 101, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5}" ] }, "execution_count": 101, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words" ] }, { "cell_type": "code", "execution_count": 102, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "number = from_words.pop(\"zero\")" ] }, { "cell_type": "code", "execution_count": 103, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 103, "metadata": {}, "output_type": "execute_result" } ], "source": [ "number" ] }, { "cell_type": "code", "execution_count": 104, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5}" ] }, "execution_count": 104, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words" ] }, { "cell_type": "code", "execution_count": 105, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "KeyError", "evalue": "'zero'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mfrom_words\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"zero\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mKeyError\u001b[0m: 'zero'" ] } ], "source": [ "from_words.pop(\"zero\")" ] }, { "cell_type": "code", "execution_count": 106, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 106, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words.pop(\"zero\", 0)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Similar to the `pop()` method of the `list` type, the [popitem()](https://docs.python.org/3/library/stdtypes.html#dict.popitem) method of the `dict` type removes *and* returns an \"arbitrary\" key-value pair as a `tuple` object from a `dict` object. With the preservation of the insertion order in Python 3.7 and higher, this effectively becomes a \"last in, first out\" rule, just as with the `list` type. Once a `dict` object is empty, [popitem()](https://docs.python.org/3/library/stdtypes.html#dict.popitem) raises a `KeyError`." ] }, { "cell_type": "code", "execution_count": 107, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5}" ] }, "execution_count": 107, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words" ] }, { "cell_type": "code", "execution_count": 108, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "word, number = from_words.popitem()" ] }, { "cell_type": "code", "execution_count": 109, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "('five', 5)" ] }, "execution_count": 109, "metadata": {}, "output_type": "execute_result" } ], "source": [ "word, number" ] }, { "cell_type": "code", "execution_count": 110, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'one': 1, 'two': 2, 'three': 3, 'four': 4}" ] }, "execution_count": 110, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [clear()](https://docs.python.org/3/library/stdtypes.html#dict.clear) method removes all items but keeps the `dict` object alive in memory." ] }, { "cell_type": "code", "execution_count": 111, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "to_words.clear()" ] }, { "cell_type": "code", "execution_count": 112, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{}" ] }, "execution_count": 112, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words" ] }, { "cell_type": "code", "execution_count": 113, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from_words.clear()" ] }, { "cell_type": "code", "execution_count": 114, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{}" ] }, "execution_count": 114, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [setdefault()](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method may have a bit of an unfortunate name but is useful, in particular, with nested `list` objects. It takes two arguments, `key` and `default`, and returns the value mapped to `key` if `key` is in the `dict` object; otherwise, it inserts the `key`-`default` pair *and* returns a reference to the newly created value object. So, it is similar to the [get()](https://docs.python.org/3/library/stdtypes.html#dict.get) method above, but *mutates* the `dict` object.\n", "\n", "Consider the `people` example again and note hwo the `dict` object modeling \"Albert Einstein\" has *no* `\"emails\"` key in it." ] }, { "cell_type": "code", "execution_count": 115, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'mathematicians': [{'emails': ['gilbert@mit.edu'],\n", " 'name': 'Gilbert Strang'},\n", " {'emails': [],\n", " 'name': 'Leonhard Euler'}],\n", " 'physicists': [{'name': 'Albert Einstein'}],\n", " 'programmers': [{'emails': ['guido@python.org'],\n", " 'name': 'Guido van Rossum'}]}\n" ] } ], "source": [ "pprint(people, indent=1, width=60)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Let's say we want to append the imaginary emails `\"leonhard@math.org\"` and `\"albert@physics.org\"`. With the current \"messy\" structure, we cannot be sure if a `dict` object modeling a person has already a `\"emails\"` key or not. We could first use the `in` operator to check for that and create a new `list` object in a second step if one is missing. Third, we would finally append the new email.\n", "\n", "[setdefault()](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) allows us to do all of the three steps at once. More importantly, behind the scenes Python only needs to make *one* key look-up instead of potentially three. For large nested data that could speed up the computations significantly.\n", "\n", "So, the first code cell below adds the email to the already existing empty `list` object, while the second one creates a new one." ] }, { "cell_type": "code", "execution_count": 116, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "people[\"mathematicians\"][1].setdefault(\"emails\", []).append(\"leonhard@math.org\")" ] }, { "cell_type": "code", "execution_count": 117, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "people[\"physicists\"][0].setdefault(\"emails\", []).append(\"albert@physics.org\")" ] }, { "cell_type": "code", "execution_count": 118, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'mathematicians': [{'emails': ['gilbert@mit.edu'],\n", " 'name': 'Gilbert Strang'},\n", " {'emails': ['leonhard@math.org'],\n", " 'name': 'Leonhard Euler'}],\n", " 'physicists': [{'emails': ['albert@physics.org'],\n", " 'name': 'Albert Einstein'}],\n", " 'programmers': [{'emails': ['guido@python.org'],\n", " 'name': 'Guido van Rossum'}]}\n" ] } ], "source": [ "pprint(people, indent=1, width=60)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "In addition to the standardized methods, `dict` objects come with a [copy()](https://docs.python.org/3/library/stdtypes.html#dict.copy) method on them that creates *shallow* copies." ] }, { "cell_type": "code", "execution_count": 119, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "guido = people[\"programmers\"][0].copy()" ] }, { "cell_type": "code", "execution_count": 120, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'name': 'Guido van Rossum', 'emails': ['guido@python.org']}" ] }, "execution_count": 120, "metadata": {}, "output_type": "execute_result" } ], "source": [ "guido" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If we mutate `guido`, for example, remove all his emails with the `clear()` method on the `list` type, these changes are also visible through `people`." ] }, { "cell_type": "code", "execution_count": 121, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "guido[\"emails\"].clear()" ] }, { "cell_type": "code", "execution_count": 122, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{'name': 'Guido van Rossum', 'emails': []}" ] }, "execution_count": 122, "metadata": {}, "output_type": "execute_result" } ], "source": [ "guido" ] }, { "cell_type": "code", "execution_count": 123, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'mathematicians': [{'emails': ['gilbert@mit.edu'],\n", " 'name': 'Gilbert Strang'},\n", " {'emails': ['leonhard@math.org'],\n", " 'name': 'Leonhard Euler'}],\n", " 'physicists': [{'emails': ['albert@physics.org'],\n", " 'name': 'Albert Einstein'}],\n", " 'programmers': [{'emails': [],\n", " 'name': 'Guido van Rossum'}]}\n" ] } ], "source": [ "pprint(people, indent=1, width=60)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Packing & Unpacking (continued)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Just as a single `*` symbol is used for packing and unpacking iterables in [Chapter 7](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/07_sequences_00_lecture.ipynb#Packing-&-Unpacking), a double `**` symbol implements packing and unpacking for mappings.\n", "\n", "Let's say we have `to_words` and `more_words` as below and want to merge the items together into a *new* `dict` object." ] }, { "cell_type": "code", "execution_count": 124, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "to_words = {\n", " 0: \"zero\",\n", " 1: \"one\",\n", " 2: \"two\",\n", "}" ] }, { "cell_type": "code", "execution_count": 125, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "more_words = {\n", " 2: \"TWO\", # upper case to illustrate a point\n", " 3: \"three\",\n", " 4: \"four\",\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "By *unpacking* the items with `**`, the newly created `dict` object is first filled with the items from `to_words` and then from `more_words`. The item with the key `2` from `more_words` overwrites its counterpart from `to_words` as it is mentioned last." ] }, { "cell_type": "code", "execution_count": 126, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{0: 'zero', 1: 'one', 2: 'TWO', 3: 'three', 4: 'four'}" ] }, "execution_count": 126, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{**to_words, **more_words}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "With the [update()](https://docs.python.org/3/library/stdtypes.html#dict.update) method from above, we can only *mutate* one of the two `dict` objects in place. So, unpacking is *no* syntactic sugar in this context." ] }, { "cell_type": "code", "execution_count": 127, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "to_words.update(more_words)" ] }, { "cell_type": "code", "execution_count": 128, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{0: 'zero', 1: 'one', 2: 'TWO', 3: 'three', 4: 'four'}" ] }, "execution_count": 128, "metadata": {}, "output_type": "execute_result" } ], "source": [ "to_words # we do not want to change an existing object" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "#### Function Definitions & Calls (continued)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Both, `*` and `**` may be used within the header line of a function definition, for example, as in `print_args1()` below. Here, *positional* arguments not captured by positional parameters are *packed* into the `tuple` object `args`, and *keyword* arguments not captured by keyword parameters are *packed* into the `dict` object `kwargs`.\n", "\n", "For `print_args1()`, all arguments are optional, and ..." ] }, { "cell_type": "code", "execution_count": 129, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "def print_args1(*args, **kwargs):\n", " \"\"\"Print out all arguments passed in.\"\"\"\n", " for i, arg in enumerate(args):\n", " print(\"position\", i, arg)\n", " for key, value in kwargs.items():\n", " print(\"keyword\", key, value)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... we may pass whatever we want to it, or nothing at all." ] }, { "cell_type": "code", "execution_count": 130, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "print_args1()" ] }, { "cell_type": "code", "execution_count": 131, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "position 0 a\n", "position 1 b\n", "position 2 c\n" ] } ], "source": [ "print_args1(\"a\", \"b\", \"c\")" ] }, { "cell_type": "code", "execution_count": 132, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "keyword first 1\n", "keyword second 2\n", "keyword third 3\n" ] } ], "source": [ "print_args1(first=1, second=2, third=3)" ] }, { "cell_type": "code", "execution_count": 133, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "position 0 x\n", "position 1 y\n", "keyword flag True\n" ] } ], "source": [ "print_args1(\"x\", \"y\", flag=True)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The next example, `print_args2()`, requires the caller to pass one positional argument, captured in the `pos` parameter, and one keyword argument, captured in `key`." ] }, { "cell_type": "code", "execution_count": 134, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "def print_args2(pos, *args, key, **kwargs):\n", " \"\"\"Print out all arguments passed in.\"\"\"\n", " print(\"required position\", pos)\n", " for i, arg in enumerate(args):\n", " print(\"optional position\", i, arg)\n", " print(\"required keyword\", key)\n", " for key, value in kwargs.items():\n", " print(\"optional keyword\", key, value)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If the caller does not respect that, a `TypeError` is raised." ] }, { "cell_type": "code", "execution_count": 135, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "TypeError", "evalue": "print_args2() missing 1 required positional argument: 'pos'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint_args2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: print_args2() missing 1 required positional argument: 'pos'" ] } ], "source": [ "print_args2()" ] }, { "cell_type": "code", "execution_count": 136, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "TypeError", "evalue": "print_args2() missing 1 required keyword-only argument: 'key'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mprint_args2\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"p\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: print_args2() missing 1 required keyword-only argument: 'key'" ] } ], "source": [ "print_args2(\"p\")" ] }, { "cell_type": "code", "execution_count": 137, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "required position p\n", "required keyword k\n" ] } ], "source": [ "print_args2(\"p\", key=\"k\")" ] }, { "cell_type": "code", "execution_count": 138, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "required position p\n", "optional position 0 x\n", "optional position 1 y\n", "required keyword k\n", "optional keyword flag True\n" ] } ], "source": [ "print_args2(\"p\", \"x\", \"y\", key=\"k\", flag=True)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Just as above when we merge `to_words` and `more_words`, we may use the `**` symbol to unpack the items of a mapping in a function call." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Dictionary Comprehensions" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Analogous to list comprehensions in [Chapter 7](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/07_sequences_00_lecture.ipynb#List-Comprehensions), **dictionary comprehensions**, or **dictcomps** for short, are a concise literal notation to derive new `dict` objects out of existing ones.\n", "\n", "For example, let's derive `from_words` from `to_words` below by swapping the keys and values." ] }, { "cell_type": "code", "execution_count": 139, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "to_words = {\n", " 0: \"zero\",\n", " 1: \"one\",\n", " 2: \"two\",\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Without a dictionary comprehension, we would have to initialize an empty `dict` object, loop over the items of the original one, and insert the key-value pairs one by one in a \"reversed\" fashion as \"value-key\" pairs. That assumes that the values are unique as otherwise some would be merged." ] }, { "cell_type": "code", "execution_count": 140, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 140, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from_words = {}\n", "\n", "for number, word in to_words.items():\n", " from_words[word] = number\n", "\n", "from_words" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "While that code is correct, it is also unnecessarily verbose. The dictionary comprehension below works in the same way as list comprehensions except that entire expression is written within curly braces `{}` instead of brackets `[]`, and a colon `:` added to separate the keys from the values." ] }, { "cell_type": "code", "execution_count": 141, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{'zero': 0, 'one': 1, 'two': 2}" ] }, "execution_count": 141, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{v: k for k, v in to_words.items()}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We may filter out items with an `if`-clause and transform the remaining key and value objects.\n", "\n", "For no good reason, let's filter out all words starting with a `\"z\"` and upper case the remainin words." ] }, { "cell_type": "code", "execution_count": 142, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{'ONE': 1, 'TWO': 2}" ] }, "execution_count": 142, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{v.upper(): k for k, v in to_words.items() if not v.startswith(\"z\")}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Multiple `for`- and/or `if`-clauses are allowed.\n", "\n", "For example, let's find all pairs of two numbers from `1` through `10` whose product is \"close\" to `50` (e.g., within a delta of `5`). The resulting `dict` object maps `tuple` to `int` objects." ] }, { "cell_type": "code", "execution_count": 143, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{(5, 9): 45,\n", " (5, 10): 50,\n", " (6, 8): 48,\n", " (6, 9): 54,\n", " (7, 7): 49,\n", " (8, 6): 48,\n", " (9, 5): 45,\n", " (9, 6): 54,\n", " (10, 5): 50}" ] }, "execution_count": 143, "metadata": {}, "output_type": "execute_result" } ], "source": [ "one_to_ten = range(1, 11)\n", "\n", "{\n", " (x, y): x * y\n", " for x in one_to_ten for y in one_to_ten\n", " if abs(x * y - 50) <= 5\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Memoization" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### \"Easy at second Glance\" Example: [Fibonacci Numbers](https://en.wikipedia.org/wiki/Fibonacci_number) (revisited)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The *recursive* implementation of the [Fibonacci numbers](https://en.wikipedia.org/wiki/Fibonacci_number) in [Chapter 4](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/04_iteration_00_lecture.ipynb#\"Easy-at-first-Glance\"-Example:-Fibonacci-Numbers) takes long to compute for large Fibonacci numbers as the number of function calls grows exponentially.\n", "\n", "The graph below visualizes what the problem is and also suggests a solution: Instead of calculating the return value of the `fibonacci()` function for the *same* argument over and over again, it makes sense to **cache** the result and reuse it. This concept is called **[memoization](https://en.wikipedia.org/wiki/Memoization)** in the computer science literature." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Below is revision of the recursive `fibonacci()` implementation that uses a **globally** defined `dict` object `memo` to store intermediate results and look them up.\n", "\n", "To be precise, called with a valid `i`, the the revised `fibonacci()` function first checks if the `i`th Fibonacci number has already been calculated before. If yes, it is in the `memo` dictionary. That number is then returned immediately *without* any more calculations. If no, there is no corresponding entry in `memo` and a recursive function call must be made. The number obtained by recursion is then put into `memo`.\n", "\n", "When we follow the flow of execution closely, we realize that the intermediate results represented by the left-most path in the graph above are calculated first. `fibonacci(1)` (i.e., the left-most leaf node) is the first base case reached, followed immediately by `fibonacci(0)`. From that moment onwards, the flow of execution moves back up the left-most path while adding together the two corresponding child nodes.\n", "\n", "Effectively, this mirrors the *iterative* implementation in that the order of all computational steps are *identical*.\n", "\n", "We added a keyword-only argument `debug` that allows the caller to print out a message every time a `i` was *not* in the `memo`." ] }, { "cell_type": "code", "execution_count": 144, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "memo = {\n", " 0: 0,\n", " 1: 1,\n", "}" ] }, { "cell_type": "code", "execution_count": 145, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "import numbers # for the goose typing" ] }, { "cell_type": "code", "execution_count": 146, "metadata": { "code_folding": [], "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "def fibonacci(i, *, debug=False):\n", " \"\"\"Calculate the ith Fibonacci number.\n", "\n", " Args:\n", " i (int): index of the Fibonacci number to calculate\n", " debug (bool): show non-cached calls; defaults to False\n", "\n", " Returns:\n", " ith_fibonacci (int)\n", "\n", " Raises:\n", " TypeError: if i is not an integer\n", " ValueError: if i is not positive\n", " \"\"\"\n", " if not isinstance(i, numbers.Integral):\n", " raise TypeError(\"i must be an integer\")\n", " elif i < 0:\n", " raise ValueError(\"i must be non-negative\")\n", "\n", " if i in memo:\n", " return memo[i]\n", "\n", " if debug: # added for didactical purposes\n", " print(f\"fibonacci({i}) is calculated\")\n", "\n", " recurse = fibonacci(i - 1, debug=debug) + fibonacci(i - 2, debug=debug)\n", " memo[i] = recurse\n", " return recurse" ] }, { "cell_type": "code", "execution_count": 147, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fibonacci(12) is calculated\n", "fibonacci(11) is calculated\n", "fibonacci(10) is calculated\n", "fibonacci(9) is calculated\n", "fibonacci(8) is calculated\n", "fibonacci(7) is calculated\n", "fibonacci(6) is calculated\n", "fibonacci(5) is calculated\n", "fibonacci(4) is calculated\n", "fibonacci(3) is calculated\n", "fibonacci(2) is calculated\n" ] }, { "data": { "text/plain": [ "144" ] }, "execution_count": 147, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fibonacci(12, debug=True) # = 13th number, 11 recursive calls necessary" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Now, calling `fibonacci()` has the *side effect* of growing the `memo` in the *global scope*. So, subsequent calls to `fibonacci()` need not calculate any Fibonacci number with an index `i` smaller than the maximum `i` used so far. Because of that, this `fibonacci()` is *not* a *pure* function." ] }, { "cell_type": "code", "execution_count": 148, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "144" ] }, "execution_count": 148, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fibonacci(12, debug=True) # = 13th number, no recursive calls needed" ] }, { "cell_type": "code", "execution_count": 149, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{0: 0,\n", " 1: 1,\n", " 2: 1,\n", " 3: 2,\n", " 4: 3,\n", " 5: 5,\n", " 6: 8,\n", " 7: 13,\n", " 8: 21,\n", " 9: 34,\n", " 10: 55,\n", " 11: 89,\n", " 12: 144}" ] }, "execution_count": 149, "metadata": {}, "output_type": "execute_result" } ], "source": [ "memo" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "##### Efficiency of Algorithms (continued)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "With memoization, the recursive `fibonacci()` implementation is as fast as its iterative counterpart, even for large numbers.\n", "\n", "The `%%timeit` magic, by default, runs a code cell seven times. Whereas in the first run, *new* Fibonacci numbers (i.e., intermediate results) are added to the `memo`, `fibonacci()` has no work to do in the subsequent six runs. `%%timeit` realizes this and tells us that \"an intermediate result is being cached.\"" ] }, { "cell_type": "code", "execution_count": 150, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The slowest run took 183.70 times longer than the fastest. This could mean that an intermediate result is being cached.\n", "21.4 µs ± 50.4 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(99) # = 100th number" ] }, { "cell_type": "code", "execution_count": 151, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The slowest run took 1974.48 times longer than the fastest. This could mean that an intermediate result is being cached.\n", "580 µs ± 1.41 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(999) # = 1,000th number" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The iterative implementation still has an advantage as the `RecursionError` shows for even larger numbers.\n", "\n", "This exception occurs as Python must keep track of *every* function call *until* it has returned, and with large enough `i`, the recursion tree above grows too big. By default, Python has a limit of up to 3000 *simultaneous* function calls. So, theoretically this exception is not a bug in the narrow sense but the result of a \"security\" measure that is supposed to keep a computer from crashing. However, practically most high-level languages like Python incur such an overhead cost: It results from the fact that someone (i.e., Python) needs to manage each function call's *local scope*. With the `for`-loop in the iterative version, we do this managing ourselves." ] }, { "cell_type": "code", "execution_count": 152, "metadata": { "scrolled": true, "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "RecursionError", "evalue": "maximum recursion depth exceeded in comparison", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_cell_magic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'timeit'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'-n 1'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'fibonacci(9999) # = 10,000th number\\n'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/site-packages/IPython/core/interactiveshell.py\u001b[0m in \u001b[0;36mrun_cell_magic\u001b[0;34m(self, magic_name, line, cell)\u001b[0m\n\u001b[1;32m 2357\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbuiltin_trap\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2358\u001b[0m \u001b[0margs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mmagic_arg_s\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcell\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2359\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2360\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2361\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36mtimeit\u001b[0;34m(self, line, cell, local_ns)\u001b[0m\n", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/site-packages/IPython/core/magic.py\u001b[0m in \u001b[0;36m\u001b[0;34m(f, *a, **k)\u001b[0m\n\u001b[1;32m 185\u001b[0m \u001b[0;31m# but it's overkill for just that one bit of state.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 186\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmagic_deco\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 187\u001b[0;31m \u001b[0mcall\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mlambda\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 188\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 189\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcallable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/site-packages/IPython/core/magics/execution.py\u001b[0m in \u001b[0;36mtimeit\u001b[0;34m(self, line, cell, local_ns)\u001b[0m\n\u001b[1;32m 1160\u001b[0m \u001b[0;32mbreak\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1161\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1162\u001b[0;31m \u001b[0mall_runs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtimer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrepeat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrepeat\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnumber\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1163\u001b[0m \u001b[0mbest\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mall_runs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mnumber\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1164\u001b[0m \u001b[0mworst\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mall_runs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mnumber\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/timeit.py\u001b[0m in \u001b[0;36mrepeat\u001b[0;34m(self, repeat, number)\u001b[0m\n\u001b[1;32m 202\u001b[0m \u001b[0mr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 203\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrepeat\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 204\u001b[0;31m \u001b[0mt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimeit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumber\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 205\u001b[0m \u001b[0mr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 206\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mr\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/site-packages/IPython/core/magics/execution.py\u001b[0m in \u001b[0;36mtimeit\u001b[0;34m(self, number)\u001b[0m\n\u001b[1;32m 167\u001b[0m \u001b[0mgc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdisable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 168\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 169\u001b[0;31m \u001b[0mtiming\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minner\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimer\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 170\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mgcold\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36minner\u001b[0;34m(_it, _timer)\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36mfibonacci\u001b[0;34m(i, debug)\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"fibonacci({i}) is calculated\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 26\u001b[0;31m \u001b[0mrecurse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfibonacci\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdebug\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdebug\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mfibonacci\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdebug\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdebug\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 27\u001b[0m \u001b[0mmemo\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrecurse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mrecurse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "... last 1 frames repeated, from the frame below ...\n", "\u001b[0;32m\u001b[0m in \u001b[0;36mfibonacci\u001b[0;34m(i, debug)\u001b[0m\n\u001b[1;32m 24\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"fibonacci({i}) is calculated\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 26\u001b[0;31m \u001b[0mrecurse\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfibonacci\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdebug\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdebug\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mfibonacci\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdebug\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdebug\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 27\u001b[0m \u001b[0mmemo\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrecurse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mrecurse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded in comparison" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(9999) # = 10,000th number" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We could \"hack\" a bit with Python's default configuration using the [sys](https://docs.python.org/3/library/sys.html) module in the [standard library](https://docs.python.org/3/library/index.html) and make it work anyhow. As we are good citizens, we reset everything to the defaults after our hack is completed." ] }, { "cell_type": "code", "execution_count": 153, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "import sys" ] }, { "cell_type": "code", "execution_count": 154, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "old_recursion_limit = sys.getrecursionlimit()" ] }, { "cell_type": "code", "execution_count": 155, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "3000" ] }, "execution_count": 155, "metadata": {}, "output_type": "execute_result" } ], "source": [ "old_recursion_limit" ] }, { "cell_type": "code", "execution_count": 156, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "sys.setrecursionlimit(99999)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Computational speed is *not* the problem here." ] }, { "cell_type": "code", "execution_count": 157, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The slowest run took 27724.52 times longer than the fastest. This could mean that an intermediate result is being cached.\n", "3.11 ms ± 7.61 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(9999) # = 10,000th number" ] }, { "cell_type": "code", "execution_count": 158, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "sys.setrecursionlimit(old_recursion_limit)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### \"Easy at third Glance\" Example: [Fibonacci Numbers](https://en.wikipedia.org/wiki/Fibonacci_number) (revisited)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "It is considered *bad practice* to make a function and thereby its correctness dependent on a program's *global state*: For example, `memo` above could be \"manipulated.\"\n", "\n", "More often than not, such things happen by accident: Imagine we wrote two independent recursive functions with memoization built-in to solve different problems, and, unintentionally, we made both work with the *same* global `memo`. As a result, we would observe \"random\" bugs depending on the order in which we executed these functions. Such bugs are hard to track down in practice.\n", "\n", "A common pattern is to avoid global state and pass intermediate results \"down\" the recursion tree in a \"hidden\" argument. By convention, we prefix variable and argument names with a single leading underscore `_`, such as with `_memo` below, to indicate that a user of our code *must not* use it. Also, we make `_memo` a *keyword-only* argument to force ourselves to always explicitly name it in a function call. Because it is an **implementation detail**, the `_memo` argument is *not* even mentioned in the docstring.\n", "\n", "When initially called, `fibonacci()` creates a new `dict` object named `_memo` in its *local scope*. That is then shared \"internally\" between successive function calls by passing it on as an argument. Once the first call to `fibonacci()` returns, `_memo` is \"forgotten.\"" ] }, { "cell_type": "code", "execution_count": 159, "metadata": { "code_folding": [], "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "def fibonacci(i, *, debug=False, _memo=None):\n", " \"\"\"Calculate the ith Fibonacci number.\n", "\n", " Args:\n", " i (int): index of the Fibonacci number to calculate\n", " debug (bool): show non-cached calls; defaults to False\n", "\n", " Returns:\n", " ith_fibonacci (int)\n", "\n", " Raises:\n", " TypeError: if i is not an integer\n", " ValueError: if i is not positive\n", " \"\"\"\n", " if not isinstance(i, numbers.Integral):\n", " raise TypeError(\"i must be an integer\")\n", " elif i < 0:\n", " raise ValueError(\"i must be non-negative\")\n", "\n", " if _memo is None:\n", " _memo = {\n", " 0: 0,\n", " 1: 1,\n", " }\n", "\n", " if i in _memo:\n", " return _memo[i]\n", "\n", " if debug: # added for didactical purposes\n", " print(f\"fibonacci({i}) is calculated\")\n", "\n", " recurse = (\n", " fibonacci(i - 1, debug=debug, _memo=_memo)\n", " + fibonacci(i - 2, debug=debug, _memo=_memo)\n", " )\n", " _memo[i] = recurse\n", " return recurse" ] }, { "cell_type": "code", "execution_count": 160, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fibonacci(12) is calculated\n", "fibonacci(11) is calculated\n", "fibonacci(10) is calculated\n", "fibonacci(9) is calculated\n", "fibonacci(8) is calculated\n", "fibonacci(7) is calculated\n", "fibonacci(6) is calculated\n", "fibonacci(5) is calculated\n", "fibonacci(4) is calculated\n", "fibonacci(3) is calculated\n", "fibonacci(2) is calculated\n" ] }, { "data": { "text/plain": [ "144" ] }, "execution_count": 160, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fibonacci(12, debug=True) # = 13th number, 11 recursive calls necessary" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Because `fibonacci()` is now independent from *global state*, the same eleven recursive function calls are made each time. So, this `fibonacci()` is a *pure* function." ] }, { "cell_type": "code", "execution_count": 161, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fibonacci(12) is calculated\n", "fibonacci(11) is calculated\n", "fibonacci(10) is calculated\n", "fibonacci(9) is calculated\n", "fibonacci(8) is calculated\n", "fibonacci(7) is calculated\n", "fibonacci(6) is calculated\n", "fibonacci(5) is calculated\n", "fibonacci(4) is calculated\n", "fibonacci(3) is calculated\n", "fibonacci(2) is calculated\n" ] }, { "data": { "text/plain": [ "144" ] }, "execution_count": 161, "metadata": {}, "output_type": "execute_result" } ], "source": [ "fibonacci(12, debug=True) # = 13th number, still 11 recursive calls necessary" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "##### Efficiency of Algorithms (continued)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Also, the runtime is now stable (i.e., no message that \"an intermediate result is being cached\"). The limitation with respect to the maximum number of simultaneous function calls still applies." ] }, { "cell_type": "code", "execution_count": 162, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "138 µs ± 5.01 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(99) # = 100th number" ] }, { "cell_type": "code", "execution_count": 163, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.55 ms ± 56.7 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(999) # = 1,000th number" ] }, { "cell_type": "code", "execution_count": 164, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "sys.setrecursionlimit(99999)" ] }, { "cell_type": "code", "execution_count": 165, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "20.8 ms ± 8.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" ] } ], "source": [ "%%timeit -n 1\n", "fibonacci(9999) # = 10,000th number" ] }, { "cell_type": "code", "execution_count": 166, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "sys.setrecursionlimit(old_recursion_limit)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "### Specialized Mappings" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [collections](https://docs.python.org/3/library/collections.html) module in the [standard library](https://docs.python.org/3/library/index.html) provides specialized mapping types for common enough use cases." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "#### The `OrderedDict` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [OrderedDict](https://docs.python.org/3/library/collections.html#collections.OrderedDict) type may be used to create a `dict`-like object with the added feature of *explicitly* remembering the *insertion* order of its items.\n", "\n", "Let's look at a quick example: We create an `OrderedDict` object by passing an iterable of $2$-element iterables, one of the three ways to use [dict()](https://docs.python.org/3/library/functions.html#func-dict)." ] }, { "cell_type": "code", "execution_count": 167, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from collections import OrderedDict" ] }, { "cell_type": "code", "execution_count": 168, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "ordered = OrderedDict([(\"first\", 1), (\"second\", 2), (\"third\", 3)])" ] }, { "cell_type": "code", "execution_count": 169, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "OrderedDict([('first', 1), ('second', 2), ('third', 3)])" ] }, "execution_count": 169, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ordered" ] }, { "cell_type": "code", "execution_count": 170, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "collections.OrderedDict" ] }, "execution_count": 170, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(ordered)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The iteration order is the insertion order, and ..." ] }, { "cell_type": "code", "execution_count": 171, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "first 1\n", "second 2\n", "third 3\n" ] } ], "source": [ "for key, value in ordered.items():\n", " print(key, value)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... we may also loop over `ordered` in *reverse* order." ] }, { "cell_type": "code", "execution_count": 172, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "third 3\n", "second 2\n", "first 1\n" ] } ], "source": [ "for key, value in reversed(ordered.items()):\n", " print(key, value)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "For `OrderedDict` objects, the [popitem()](https://docs.python.org/3/library/collections.html#collections.OrderedDict.popitem) method takes an optional and boolean argument `last`. That allows us to remove either the *first* or *last* item inserted.\n", "\n", "Further, the [move_to_end()](https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end) method allows us to move any item by its key to the end of the order. It also takes an optional argument `last`." ] }, { "cell_type": "code", "execution_count": 173, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "ordered.move_to_end(\"first\")" ] }, { "cell_type": "code", "execution_count": 174, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "second 2\n", "third 3\n", "first 1\n" ] } ], "source": [ "for key, value in ordered.items():\n", " print(key, value)" ] }, { "cell_type": "code", "execution_count": 175, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "ordered.move_to_end(\"first\", last=False)" ] }, { "cell_type": "code", "execution_count": 176, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "first 1\n", "second 2\n", "third 3\n" ] } ], "source": [ "for key, value in ordered.items():\n", " print(key, value)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Otherwise, an `OrderedDict` object is no different than a normal `dict` one, starting with Python 3.7." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "#### The `defaultdict` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "A more useful mapping is the [defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict) type, which allows us to define a factory function. That creates default values whenever we look up a key that does not yet exist. Ordinary `dict` objects would throw a `KeyError` exception in such situations.\n", "\n", "Let's say we have a `list` with *records* of goals scored during a soccer game. The records consist of the fields \"Country,\" \"Player,\" and the \"Time\" when a goal was scored. Our task is to group the goals by player and/or country." ] }, { "cell_type": "code", "execution_count": 177, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "goals = [\n", " (\"Germany\", \"Müller\", 11), (\"Germany\", \"Klose\", 23),\n", " (\"Germany\", \"Kroos\", 24), (\"Germany\", \"Kroos\", 26),\n", " (\"Germany\", \"Khedira\", 29), (\"Germany\", \"Schürrle\", 69),\n", " (\"Germany\", \"Schürrle\", 79), (\"Brazil\", \"Oscar\", 90),\n", "]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Using a normal `dict` object, we have to tediously check if a player has already scored a goal before. If not, we must create a *new* `list` object with the first time the player scored. Otherwise, we append the goal to an already existing `list` object." ] }, { "cell_type": "code", "execution_count": 178, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{'Müller': [11],\n", " 'Klose': [23],\n", " 'Kroos': [24, 26],\n", " 'Khedira': [29],\n", " 'Schürrle': [69, 79],\n", " 'Oscar': [90]}" ] }, "execution_count": 178, "metadata": {}, "output_type": "execute_result" } ], "source": [ "goals_by_player = {}\n", "\n", "for _, player, minute in goals:\n", " if player not in goals_by_player:\n", " goals_by_player[player] = [minute]\n", " else:\n", " goals_by_player[player].append(minute)\n", "\n", "goals_by_player" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We could replace the `if`-`else`-logic with the [setdefault()](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method mentioned above." ] }, { "cell_type": "code", "execution_count": 179, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{'Müller': [11],\n", " 'Klose': [23],\n", " 'Kroos': [24, 26],\n", " 'Khedira': [29],\n", " 'Schürrle': [69, 79],\n", " 'Oscar': [90]}" ] }, "execution_count": 179, "metadata": {}, "output_type": "execute_result" } ], "source": [ "goals_by_player = {}\n", "\n", "for _, player, minute in goals:\n", " goals_by_player.setdefault(player, []).append(minute)\n", "\n", "goals_by_player" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Instead, with a `defaultdict` object, we can portray the code fragment's intent in a concise form. We pass a reference to the [list()](https://docs.python.org/3/library/functions.html#func-list) built-in to `defaultdict`." ] }, { "cell_type": "code", "execution_count": 180, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from collections import defaultdict" ] }, { "cell_type": "code", "execution_count": 181, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "defaultdict(list,\n", " {'Müller': [11],\n", " 'Klose': [23],\n", " 'Kroos': [24, 26],\n", " 'Khedira': [29],\n", " 'Schürrle': [69, 79],\n", " 'Oscar': [90]})" ] }, "execution_count": 181, "metadata": {}, "output_type": "execute_result" } ], "source": [ "goals_by_player = defaultdict(list)\n", "\n", "for _, player, minute in goals:\n", " goals_by_player[player].append(minute)\n", "\n", "goals_by_player" ] }, { "cell_type": "code", "execution_count": 182, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "collections.defaultdict" ] }, "execution_count": 182, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(goals_by_player)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The factory function is stored in the `default_factory` attribute." ] }, { "cell_type": "code", "execution_count": 183, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "list" ] }, "execution_count": 183, "metadata": {}, "output_type": "execute_result" } ], "source": [ "goals_by_player.default_factory" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If we want this code to produce a normal `dict` object, we pass `goals_by_player` to the [dict()](https://docs.python.org/3/library/functions.html#func-dict) built-in." ] }, { "cell_type": "code", "execution_count": 184, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{'Müller': [11],\n", " 'Klose': [23],\n", " 'Kroos': [24, 26],\n", " 'Khedira': [29],\n", " 'Schürrle': [69, 79],\n", " 'Oscar': [90]}" ] }, "execution_count": 184, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dict(goals_by_player)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Being creative, we use a factory function that returns another [defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict) with [list()](https://docs.python.org/3/library/functions.html#func-list) as its factory to group on the country and the player level simultaneously." ] }, { "cell_type": "code", "execution_count": 185, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "defaultdict(()>,\n", " {'Germany': defaultdict(list,\n", " {'Müller': [11],\n", " 'Klose': [23],\n", " 'Kroos': [24, 26],\n", " 'Khedira': [29],\n", " 'Schürrle': [69, 79]}),\n", " 'Brazil': defaultdict(list, {'Oscar': [90]})})" ] }, "execution_count": 185, "metadata": {}, "output_type": "execute_result" } ], "source": [ "goals_by_country_and_player = defaultdict(lambda: defaultdict(list))\n", "\n", "for country, player, minute in goals:\n", " goals_by_country_and_player[country][player].append(minute)\n", "\n", "goals_by_country_and_player" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Conversion into a normal and nested `dict` object is now a bit tricky but can be achieved in one line with a comprehension." ] }, { "cell_type": "code", "execution_count": 186, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'Brazil': {'Oscar': [90]},\n", " 'Germany': {'Khedira': [29],\n", " 'Klose': [23],\n", " 'Kroos': [24, 26],\n", " 'Müller': [11],\n", " 'Schürrle': [69, 79]}}\n" ] } ], "source": [ "pprint({country: dict(by_player) for country, by_player in goals_by_country_and_player.items()})" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "#### The `Counter` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "A common task is to count the number of occurrences of elements in an iterable.\n", "\n", "The [Counter](https://docs.python.org/3/library/collections.html#collections.Counter) type provides an easy-to-use interface that can be called with any iterable and returns a `dict`-like object of type `Counter` that maps each unique elements to the number of times it occurs.\n", "\n", "To continue the previous example, let's create an overview that shows how many goals a player scorred. We use a generator expression as the argument to `Counter`." ] }, { "cell_type": "code", "execution_count": 187, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "[('Germany', 'Müller', 11),\n", " ('Germany', 'Klose', 23),\n", " ('Germany', 'Kroos', 24),\n", " ('Germany', 'Kroos', 26),\n", " ('Germany', 'Khedira', 29),\n", " ('Germany', 'Schürrle', 69),\n", " ('Germany', 'Schürrle', 79),\n", " ('Brazil', 'Oscar', 90)]" ] }, "execution_count": 187, "metadata": {}, "output_type": "execute_result" } ], "source": [ "goals" ] }, { "cell_type": "code", "execution_count": 188, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from collections import Counter" ] }, { "cell_type": "code", "execution_count": 189, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "scorers = Counter(x[1] for x in goals)" ] }, { "cell_type": "code", "execution_count": 190, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "Counter({'Müller': 1,\n", " 'Klose': 1,\n", " 'Kroos': 2,\n", " 'Khedira': 1,\n", " 'Schürrle': 2,\n", " 'Oscar': 1})" ] }, "execution_count": 190, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scorers" ] }, { "cell_type": "code", "execution_count": 191, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "collections.Counter" ] }, "execution_count": 191, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(scorers)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Now we can look up individual players. `scores` behaves like a normal dictionary with regard to key look-ups." ] }, { "cell_type": "code", "execution_count": 192, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 192, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scorers[\"Müller\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "By default, it returns `0` if a key is not found. So, we do not have to handle a `KeyError`." ] }, { "cell_type": "code", "execution_count": 193, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "0" ] }, "execution_count": 193, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scorers[\"Lahm\"]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`Counter` objects have a [most_common()](https://docs.python.org/3/library/collections.html#collections.Counter.most_common) method that returns a `list` object containing $2$-element `tuple` objects, where the first element is the element from the original iterable and the second the number of occurrences. The `list` object is sorted in descending order of occurrences." ] }, { "cell_type": "code", "execution_count": 194, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "[('Kroos', 2), ('Schürrle', 2)]" ] }, "execution_count": 194, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scorers.most_common(2)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We can increase the count of individual entries with the [update()](https://docs.python.org/3/library/collections.html#collections.Counter.update) method: That takes an *iterable* of the elements we want to count.\n", "\n", "Imagine if [Philipp Lahm](https://en.wikipedia.org/wiki/Philipp_Lahm) had also scored against Brazil." ] }, { "cell_type": "code", "execution_count": 195, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "scorers.update([\"Lahm\"])" ] }, { "cell_type": "code", "execution_count": 196, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "Counter({'Müller': 1,\n", " 'Klose': 1,\n", " 'Kroos': 2,\n", " 'Khedira': 1,\n", " 'Schürrle': 2,\n", " 'Oscar': 1,\n", " 'Lahm': 1})" ] }, "execution_count": 196, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scorers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "If we use a `str` object as the argument instead, each individual character is treated as an element to be updated. That is most likely not what we want." ] }, { "cell_type": "code", "execution_count": 197, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "scorers.update(\"Lahm\")" ] }, { "cell_type": "code", "execution_count": 198, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "Counter({'Müller': 1,\n", " 'Klose': 1,\n", " 'Kroos': 2,\n", " 'Khedira': 1,\n", " 'Schürrle': 2,\n", " 'Oscar': 1,\n", " 'Lahm': 1,\n", " 'L': 1,\n", " 'a': 1,\n", " 'h': 1,\n", " 'm': 1})" ] }, "execution_count": 198, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scorers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "#### The `ChainMap` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Consider `to_words`, `more_words`, and `even_more_words` below. Instead of merging the items of the three `dict` objects together into a *new* one, we want to create an object that behaves as if it contained all the unified items in it without materializing them in memory a second time." ] }, { "cell_type": "code", "execution_count": 199, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "to_words = {\n", " 0: \"zero\",\n", " 1: \"one\",\n", " 2: \"two\",\n", "}" ] }, { "cell_type": "code", "execution_count": 200, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "more_words = {\n", " 2: \"TWO\", # upper case to illustrate a point\n", " 3: \"three\",\n", " 4: \"four\",\n", "}" ] }, { "cell_type": "code", "execution_count": 201, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "even_more_words = {\n", " 4: \"FOUR\", # upper case to illustrate a point\n", " 5: \"five\",\n", " 6: \"six\",\n", "}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [ChainMap](https://docs.python.org/3/library/collections.html#collections.ChainMap) type allows us to do precisely that." ] }, { "cell_type": "code", "execution_count": 202, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "from collections import ChainMap" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We simply pass all mappings as positional arguments to `ChainMap` and obtain a **proxy** object that occupies almost no memory but gives us access to the union of all the items." ] }, { "cell_type": "code", "execution_count": 203, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "chain = ChainMap(to_words, more_words, even_more_words)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Let's loop over the items in `chain` and see what is \"in\" it. The order is obviously *unpredictable* but all seven items we expected are there. Keys of later mappings do *not* overwrite earlier keys." ] }, { "cell_type": "code", "execution_count": 204, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4 four\n", "5 five\n", "6 six\n", "2 two\n", "3 three\n", "0 zero\n", "1 one\n" ] } ], "source": [ "for number, word in chain.items():\n", " print(number, word)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "When looking up a non-existent key, `ChainMap` objects raise a `KeyError` just like normal `dict` objects would." ] }, { "cell_type": "code", "execution_count": 205, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "KeyError", "evalue": "10", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mchain\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/collections/__init__.py\u001b[0m in \u001b[0;36m__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 914\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mKeyError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 915\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 916\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__missing__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# support subclasses that define __missing__\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 917\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 918\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdefault\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/.pyenv/versions/anaconda3-2019.10/lib/python3.7/collections/__init__.py\u001b[0m in \u001b[0;36m__missing__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 906\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 907\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__missing__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 908\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mKeyError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 909\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 910\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__getitem__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mKeyError\u001b[0m: 10" ] } ], "source": [ "chain[10]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## The `set` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Python's provides a built-in `set` type that resembles [mathematical sets](https://en.wikipedia.org/wiki/Set_%28mathematics%29): Each element may only be a member of a set once, and there is no *predictable* order regarding the elements (cf., [documentation](https://docs.python.org/3/library/stdtypes.html#set)).\n", "\n", "To create a set, we use the literal notation, `{..., ...}`, and list all the elements. The redundant `7` and `4` are discarded." ] }, { "cell_type": "code", "execution_count": 206, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "numbers = {7, 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4, 4}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`set` objects are objects on their own." ] }, { "cell_type": "code", "execution_count": 207, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "139953088728672" ] }, "execution_count": 207, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(numbers)" ] }, { "cell_type": "code", "execution_count": 208, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "set" ] }, "execution_count": 208, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(numbers)" ] }, { "cell_type": "code", "execution_count": 209, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" ] }, "execution_count": 209, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To create an empty set, we must use the [set()](https://docs.python.org/3/library/functions.html#func-set) built-in as empty curly brackets, `{}`, already creates an empty `dict` object." ] }, { "cell_type": "code", "execution_count": 210, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "empty_dict = {}" ] }, { "cell_type": "code", "execution_count": 211, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "dict" ] }, "execution_count": 211, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(empty_dict)" ] }, { "cell_type": "code", "execution_count": 212, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "empty_set = set()" ] }, { "cell_type": "code", "execution_count": 213, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "set()" ] }, "execution_count": 213, "metadata": {}, "output_type": "execute_result" } ], "source": [ "empty_set" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [set()](https://docs.python.org/3/library/functions.html#func-set) built-in takes any iterable and only keeps unique elements.\n", "\n", "For example, we obtain all unique letters of a long word like so." ] }, { "cell_type": "code", "execution_count": 214, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{'a', 'b', 'c', 'd', 'r'}" ] }, "execution_count": 214, "metadata": {}, "output_type": "execute_result" } ], "source": [ "set(\"abracadabra\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Sets are like \"Dictionaries without Values\"" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The curly brackets notation can be viewed as a hint that `dict` objects are conceptually generalizations of `set` objects, and we think of `set` objects as a collection consisting of a `dict` object's keys with all the mapped values removed.\n", "\n", "Like `dict` objects, `set` objects are built on top of [hash tables](https://en.wikipedia.org/wiki/Hash_table), and, thus, each element must be a *hashable* (i.e., immutable at the very least) object and can only be contained in a set once due to the buckets logic." ] }, { "cell_type": "code", "execution_count": 215, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "TypeError", "evalue": "unhashable type: 'list'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;34m{\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: unhashable type: 'list'" ] } ], "source": [ "{0, [1, 2], 3}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "[len()](https://docs.python.org/3/library/functions.html#len) tells us the number of elements in a `set` object, which is always `Sized`." ] }, { "cell_type": "code", "execution_count": 216, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "12" ] }, "execution_count": 216, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(numbers)" ] }, { "cell_type": "code", "execution_count": 217, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 217, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(numbers, abc.Sized)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "We may loop over the elements in a `set` object, but we must keep in mind that there is no *predictable* order. In contrast to `dict` objects, the iteration order is also *not* guaranteed to be the insertion order. Because of the special hash values for `int` objects, `numbers` seems to be \"magically\" sorted, which is not the case." ] }, { "cell_type": "code", "execution_count": 218, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 2 3 4 5 6 7 8 9 10 11 12 " ] } ], "source": [ "for number in numbers:\n", " print(number, end=\" \")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`set` objects are `Iterable`." ] }, { "cell_type": "code", "execution_count": 219, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 219, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(numbers, abc.Iterable)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`set` objects are not `Reversible`." ] }, { "cell_type": "code", "execution_count": 220, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "ename": "TypeError", "evalue": "'set' object is not reversible", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mnumber\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mreversed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumbers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumber\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mend\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\" \"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: 'set' object is not reversible" ] } ], "source": [ "for number in reversed(numbers):\n", " print(number, end=\" \")" ] }, { "cell_type": "code", "execution_count": 221, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 221, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(numbers, abc.Reversible)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The boolean `in` operator checks if a given and immutable object evaluates equal to an element in a `set` object. As with `dict` objects, membership testing is an *extremely* fast operation. Conceptually, it has the same result as conducting a linear search with the `==` operator behind the scenes." ] }, { "cell_type": "code", "execution_count": 222, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 222, "metadata": {}, "output_type": "execute_result" } ], "source": [ "0 in numbers" ] }, { "cell_type": "code", "execution_count": 223, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 223, "metadata": {}, "output_type": "execute_result" } ], "source": [ "1 in numbers" ] }, { "cell_type": "code", "execution_count": 224, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 224, "metadata": {}, "output_type": "execute_result" } ], "source": [ "2.0 in numbers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "So, a `set` object is a `Container`. " ] }, { "cell_type": "code", "execution_count": 225, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 225, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(numbers, abc.Container)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "There is a `Set` ABC in the [collections.abc](https://docs.python.org/3/library/collections.abc.html) module that formalizes, in particular, the operators supported by `set` objects. Furthermore, the ordinary `set` type is *mutable*, as expressed with the `MutableSet` ABC. The latter formalizes all the methods that mutate a `set` object." ] }, { "cell_type": "code", "execution_count": 226, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 226, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(numbers, abc.Set)" ] }, { "cell_type": "code", "execution_count": 227, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 227, "metadata": {}, "output_type": "execute_result" } ], "source": [ "isinstance(numbers, abc.MutableSet)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### No Indexing / Key Look-up / Slicing" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "As `set` objects come without a *predictable* order, indexing and slicing is not supported and results in a `TypeError`. In particular, as there are no values to be looked up, these operations are not *semantically* meaningful. Instead, we check membership via the `in` operator, as shown in the previous sub-section." ] }, { "cell_type": "code", "execution_count": 228, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "ename": "TypeError", "evalue": "'set' object is not subscriptable", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnumbers\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: 'set' object is not subscriptable" ] } ], "source": [ "numbers[0]" ] }, { "cell_type": "code", "execution_count": 229, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "ename": "TypeError", "evalue": "'set' object is not subscriptable", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnumbers\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: 'set' object is not subscriptable" ] } ], "source": [ "numbers[:]" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Mutability & Set Methods" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Because the `[]` operator does not work for `set` objects, they are mutated maily via methods (cf., [documentation](https://docs.python.org/3/library/stdtypes.html#set)).\n", "\n", "We may add new elements to an existing `set` object with the [add()](https://docs.python.org/3/library/stdtypes.html#frozenset.add) method." ] }, { "cell_type": "code", "execution_count": 230, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "numbers.add(99)" ] }, { "cell_type": "code", "execution_count": 231, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99}" ] }, "execution_count": 231, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [update()](https://docs.python.org/3/library/stdtypes.html#frozenset.update) method takes an iterable and adds all its elements to a `set` object if they are not already contained in it." ] }, { "cell_type": "code", "execution_count": 232, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "numbers.update(range(5))" ] }, { "cell_type": "code", "execution_count": 233, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99}" ] }, "execution_count": 233, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To remove an element by value, we use the [remove()](https://docs.python.org/3/library/stdtypes.html#frozenset.remove) or [discard()](https://docs.python.org/3/library/stdtypes.html#frozenset.discard) methods. If the element to be removed is not in the `set` object, [remove()](https://docs.python.org/3/library/stdtypes.html#frozenset.remove) raises a loud `KeyError` while [discard()](https://docs.python.org/3/library/stdtypes.html#frozenset.discard) stays *silent*." ] }, { "cell_type": "code", "execution_count": 234, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "numbers.remove(99)" ] }, { "cell_type": "code", "execution_count": 235, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "ename": "KeyError", "evalue": "99", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mnumbers\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mremove\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m99\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mKeyError\u001b[0m: 99" ] } ], "source": [ "numbers.remove(99)" ] }, { "cell_type": "code", "execution_count": 236, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [], "source": [ "numbers.discard(0)" ] }, { "cell_type": "code", "execution_count": 237, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "numbers.discard(0)" ] }, { "cell_type": "code", "execution_count": 238, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" ] }, "execution_count": 238, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [pop()](https://docs.python.org/3/library/stdtypes.html#frozenset.pop) method removes an *arbitrary* element. As not even the insertion order is tracked, that removes a \"random\" element in theory." ] }, { "cell_type": "code", "execution_count": 239, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 239, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers.pop()" ] }, { "cell_type": "code", "execution_count": 240, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" ] }, "execution_count": 240, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The [clear()](https://docs.python.org/3/library/stdtypes.html#frozenset.clear) method discards all elements but keeps the `set` object alive." ] }, { "cell_type": "code", "execution_count": 241, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "numbers.clear()" ] }, { "cell_type": "code", "execution_count": 242, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "set()" ] }, "execution_count": 242, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "code", "execution_count": 243, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "139953088728672" ] }, "execution_count": 243, "metadata": {}, "output_type": "execute_result" } ], "source": [ "id(numbers) # same memory location as before" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Set Operations" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The arithmetic and relational operators are overloaded with typical set operations from math. The operators have methods that do the equivalent. We omit them for brevity in this sub-section and only show them as comments in the code cells. Both the operators and the methods return *new* `set` objects without modifying the operands.\n", "\n", "We showcase the set operations with easy math examples." ] }, { "cell_type": "code", "execution_count": 244, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [], "source": [ "numbers = set(range(1, 13))\n", "zero = {0}\n", "evens = set(range(2, 13, 2))" ] }, { "cell_type": "code", "execution_count": 245, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" ] }, "execution_count": 245, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers" ] }, { "cell_type": "code", "execution_count": 246, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{0}" ] }, "execution_count": 246, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zero" ] }, { "cell_type": "code", "execution_count": 247, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{2, 4, 6, 8, 10, 12}" ] }, "execution_count": 247, "metadata": {}, "output_type": "execute_result" } ], "source": [ "evens" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The bitwise OR operator `|` returns the union of two `set` objects." ] }, { "cell_type": "code", "execution_count": 248, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" ] }, "execution_count": 248, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zero | numbers # zero.union(numbers) or reversed order" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Of course, the operators may be *chained*." ] }, { "cell_type": "code", "execution_count": 249, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" ] }, "execution_count": 249, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zero | numbers | evens # zero.union(numbers).union(evens) or any possible order" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To obtain an intersection of two or more `set` objects, we use the bitwise AND operator `&`." ] }, { "cell_type": "code", "execution_count": 250, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "set()" ] }, "execution_count": 250, "metadata": {}, "output_type": "execute_result" } ], "source": [ "zero & numbers # zero.intersection(numbers) or reversed order" ] }, { "cell_type": "code", "execution_count": 251, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{2, 4, 6, 8, 10, 12}" ] }, "execution_count": 251, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers & evens # numbers.intersection(evens) or reversed order" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "To calculate a `set` object containing all elements that are in one but not the other `set` object, we use the minus operator `-`. This operation is *not* symmetric." ] }, { "cell_type": "code", "execution_count": 252, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{1, 3, 5, 7, 9, 11}" ] }, "execution_count": 252, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers - evens # numbers.difference(evens)" ] }, { "cell_type": "code", "execution_count": 253, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "set()" ] }, "execution_count": 253, "metadata": {}, "output_type": "execute_result" } ], "source": [ "evens - numbers # evens.difference(numbers)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The *symmetric* difference is defined as the `set` object containing all elements that are in one but not both `set` objects. It is calculated with the bitwise XOR operator `^`." ] }, { "cell_type": "code", "execution_count": 254, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "text/plain": [ "{1, 3, 5, 7, 9, 11}" ] }, "execution_count": 254, "metadata": {}, "output_type": "execute_result" } ], "source": [ "numbers ^ evens # numbers.symmetric_difference(evens)" ] }, { "cell_type": "code", "execution_count": 255, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{1, 3, 5, 7, 9, 11}" ] }, "execution_count": 255, "metadata": {}, "output_type": "execute_result" } ], "source": [ "evens ^ numbers # evens.symmetric_difference(numbers)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The augmented versions of the four operators (e.g., `|` becomes `|=`) are also defined: They mutate the left operand *in place*." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Set Comprehensions" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Python provides a literal notation for **set comprehensions**, or **setcomps** for short, that works exactly like the one for dictionary comprehensions described above except that they use a single loop variable instead of a key-value pair.\n", "\n", "For example, let's create a new `set` object that consists of the squares of all the elements of `numbers`." ] }, { "cell_type": "code", "execution_count": 256, "metadata": { "slideshow": { "slide_type": "slide" } }, "outputs": [ { "data": { "text/plain": [ "{1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144}" ] }, "execution_count": 256, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{x ** 2 for x in numbers}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "As before, we may have multiple `for`- and/or `if`-clauses.\n", "\n", "For example, let's only keep the squares if they turn out to be an even number, or ..." ] }, { "cell_type": "code", "execution_count": 257, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{4, 16, 36, 64, 100, 144}" ] }, "execution_count": 257, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{x ** 2 for x in numbers if (x ** 2) % 2 == 0}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "... create a `set` object with all products obtained from the Cartesian product of `numbers` with itself as long as the products are greater than `80`." ] }, { "cell_type": "code", "execution_count": 258, "metadata": { "slideshow": { "slide_type": "-" } }, "outputs": [ { "data": { "text/plain": [ "{81, 84, 88, 90, 96, 99, 100, 108, 110, 120, 121, 132, 144}" ] }, "execution_count": 258, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{x * y for x in numbers for y in numbers if x * y > 80}" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "### The `frozenset` Type" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "As `set` objects are mutable, they may *not* be used, for example, as keys in a `dict` object. Similar to how we replace `list` with `tuple` objects, we may often use a `frozenset` object instead of an ordinary one. The `frozenset` type is a built-in, and as `frozenset` objects are immutable, the only limitation is that we must specify *all* elements *upon* creation (cf., [documentation](https://docs.python.org/3/library/stdtypes.html#frozenset)).\n", "\n", "`frozenset` objects are created by passing an iterable to the [frozenset()](https://docs.python.org/3/library/functions.html#func-frozenset) built-in. Note that even though `frozenset` objects are hashable, their elements do still *not* come in a predictable order." ] }, { "cell_type": "code", "execution_count": 259, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "frozenset({1, 2, 3})" ] }, "execution_count": 259, "metadata": {}, "output_type": "execute_result" } ], "source": [ "frozenset([1, 1, 2, 2, 3, 3])" ] }, { "cell_type": "code", "execution_count": 260, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "text/plain": [ "frozenset({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})" ] }, "execution_count": 260, "metadata": {}, "output_type": "execute_result" } ], "source": [ "frozenset(numbers)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "## TL;DR" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "`dict` objects are **mutable** one-to-one **mappings** from a set of **key** objects to a set of **value** objects. The association between a key-value pair is also called **item**.\n", "\n", "The items contained in a `dict` object have **no order** that is *predictable*.\n", "\n", "The underlying **data structure** of the `dict` type are **hash tables**. They make key look-ups *extremely* fast by converting the items' keys into *deterministic* hash values specifiying *precisely* one of a fixed number of equally \"wide\" buckets in which an item's references are stored. A limitation is that objects used as keys must be *immutable* (for technical reasons) and *unique* (for practical reasons).\n", "\n", "A `set` object is a **mutable** and **unordered collection** of **immutable** objects. The `set` type mimics sets we know from math." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "## Further Resources" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "Next, we list some conference talks that go into great detail regarding the workings of the `dict` type." ] }, { "cell_type": "code", "execution_count": 261, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "image/jpeg": "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\n", "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "execution_count": 261, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import YouTubeVideo\n", "YouTubeVideo(\"C4Kc8xzcA68\", width=\"60%\")" ] }, { "cell_type": "code", "execution_count": 262, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "image/jpeg": "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jaZrfV2i5bO0tdRTiioZJH1MsUU5jutrbIAy4SyuY5/A854PQ1zc73RAQH+D/s3Q22wUDKGCpiFVCytqX18O4uNTVVLQ+aouDC8kVROBjJwGtAOAFPkRAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAUW2j2wZQ1b4JYy5oo2TxCLnVFTUy1BgipIIjwfI4gY4+EnABKlKh20+xEVxrzVVAZobQcmglYSKulqeUidtTTPLcRSNLWEPHHLejC3hm39o5Vc+3sbS9qNqo6OGH4SG5qnwunlgpIqmuEEbCNb3ughJbC3UGmVwAJBVnctu4I5blAxrgaC2R3HlMsVVyN7ZBVkZlihPxQbTB2tmdWp4bktcBjL5s7eqpkTZpaWZ3I5qWUNq62jgbOXuay4bqmZ/GHPi0Zp3kBpBDXEElU67YqudBWQMdSkVuzVNZ3PdLK0w1dIyvax4aITvKd5rzzuBG66Dnh0UYbzhKdXcunyJXWbV0MM4ppJXCXVDG8tgnfBFLUFoginqWR7mnkeXsAbIQTvGeEZt9r9oZKSahpom04krnysbNVyuhp2GFgfuwWtJkqX6ubHwyI5Tnm4OArdhpXV1TLojqKatqqaqk3lwuNPuHQxUsMjORU3xFZwpWPDnluC7ByAFJNrqCqmEYhjo6uAtkjqqCu5kNQH7sxyCYQvLHMLHDQWkHenoIC1tBNWNlKo4u+rXq5mKum1lVT07zPTw09Wyhu9TuHSSTNkdbTCGSwPYwNfSu3zHc8td8YwY6cZel2opTC6SWUNdFLR007QyXm1Va2mMETW6cvDjVwAFuRz+ngVEYtgasU7Y95TMxb7/SMha+Yw0vwrJSPpKeBzmanUsTadzckD8HDQOAzDtj5DcKGp3rBTxU8XK4edmaromSx0MjBjSWAVdS4k8c09L4OGzUDWMqvh4fp5mUh2vt75nwiZ2pm/GswVDYJHUurlMcFS6Pc1ErND8sjJI3cn5JxQt+2VJUvpzDK3k00FXUb2eKqpi+KmbSPM8BnhDH0wbVNJkJxxGM4diN2fYGeBwicyGSOnNa6lqn3G5SPzUxVMMX+jn/xanlEdU9hkaTka8NGrm3902HlqYKKmkljYyKxXC01D2anP3lbDQRCWEFuHsHJZTzsdLfPg407/AOf54GVOta9l/lvUkVh2mo65zmU75NYjbKGTU9RTOkheSGzwipjBmhJGNbMjiPCsyorZbTXSV0NbXtpYnUlFPRwspJZZhMaqWklnnkMsTdyP4lCBGNWNb8uPBSpcpJJ6jvTcmtYREWp0CIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIDkLt9bR+OpfRG+0nb62j8dS+iN9pauV9s60GsowQC11XSggjIIM8YIIPAjB6F6l4WklfNXI8BHHYhu2e+ZsHt+7RePpPRWe0nb92i8fSeis9pdUN2YtuP5PovRYPYT7mLb5PovRYPYVTpdD4aPQ924r4z6+Zyv2/dovH0norPaTt+7RePpPRWe0uqPuYtvk+i9Fg9hPuYtvk+i9Fg9hNMofDRnu3FfGfXzOV+37tF4+k9FZ7Sdv3aLx9J6Kz2l1R9zFt8n0XosHsJ9zFt8n0XosHsJplD4aHduK+M+vmcr9v3aLx9J6Kz2l72+to/HUnojfaXT9z2atwgmIoKIERSEHksHAhh/mLg2E81v5o/sUzC9jXv7CVitygsThM29Vu9+htTt9bR+OpfRG+0nb62j8dS+iN9pauRS9FpcK5Fb3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJotLhXId4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJotLhXId4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJotLhXId4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJotLhXId4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJotLhXId4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNodvraPx1J6I32l72+to/HUvojfaWriiaLS4VyM6fiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyMd4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJotLhXId4YjjfM2j2+to/HUvojfaTt9bR+OpfRG+0tXImi0uFch3hiON8zaPb62j8dS+iN9pO31tH46l9Eb7S1ciaLS4VyHeGI43zNo9vraPx1L6I32k7fW0fjqX0RvtLVyJot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\n", "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "execution_count": 262, "metadata": {}, "output_type": "execute_result" } ], "source": [ "YouTubeVideo(\"66P5FMkWoVU\", width=\"60%\")" ] }, { "cell_type": "code", "execution_count": 263, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [ { "data": { "image/jpeg": "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dfw1e3XmZbwsbcXO668vktwY+pI1law3eI1b8bdYSRodaOyXQlmcXhrGPrZynJYlzlnKyixi45cZka9mKsx9K/Rc/Vrd+DfDonDTe0HjAGxuiXanI5SvYfkaFao+GcxwWaF+HI47IwaeDapzx6PazUOBZIAfi+MgTVax6BOjCTZxuUfNPVdLlrjbb6mNrOpYqjux8MRUqz5HFoOp1cSOTYxpy57OQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAUDj20uBndUlWr3EcvLitGWJBbBblX4qOZsbodyUmVrXlgI5F3MkaGeFa6p9HfBdHdijpDKRZi9fFjc+XqXbtpz688vD4m+KlkAEdT4mdY112U837xXrZ+jMJJ9+eP4roeNJvtNhgcK1owyS1WyPswQT8LhWJ2Nil1jjJPwUnLwTp9XNscbFGyV9kcOSCC0x7I5ZGugtTRV68msbDyfJNGAOv4x6gdIxDstkW36tqzJA+Knkbt11l924+SWtPWyEEMTaT4+56nBZajb4BOohJ5EnWw2V2TksULLonsLJMjQGOfIySENwuIyrLVWEN0LiNO69x/U4SRHkCs8yHeaulq6radP5eegljdv8WS5plsMdHIyKZslC/GarpN3hG4HwfwSN++0tfLoCCSDoCryztbQjsmq+ZwkEsUD3iCw6vHYmDDDXlttj4EU7xJHpG5wPwsf8oa4rP7Kz2I84xj4QcmyBsG8X6MMVdkLuLozkN5pPg6q2vbL3nd1U2Gr3Bcvx3n2HySi3C3iwzz12wCLclc58Lg2XfG6JR4J3fCjNgZudVbOXj8/AzVTbTHS2G1mTSGR1ieoHGtabB3VWdK2at3S6Lg8ccGQhmupDdRqNFYv22hlvUKlQmVti1agmlfXssic2tUtSudUsvYIbBbNAxhLC7rcqFPZKwyGCMvhLos9cyj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\n", "text/html": [ "\n", " \n", " " ], "text/plain": [ "" ] }, "execution_count": 263, "metadata": {}, "output_type": "execute_result" } ], "source": [ "YouTubeVideo(\"npw4s1QTmPg\", width=\"60%\")" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "skip" } }, "source": [ "The `dict` type's order has been worked on in many PEPs in recent years:\n", "- [PEP 412](https://www.python.org/dev/peps/pep-0412/): Key-Sharing Dictionary\n", "- [PEP 468](https://www.python.org/dev/peps/pep-0468/): Preserving the order of \\*\\*kwargs in a function\n", "- [PEP 520](https://www.python.org/dev/peps/pep-0520/): Preserving Class Attribute Definition Order" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.7.4" }, "livereveal": { "auto_select": "code", "auto_select_fragment": true, "scroll": true, "theme": "serif" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "384px" }, "toc_section_display": false, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }