diff --git a/.gitignore b/.gitignore index 35008af..79eb637 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +**/.ipynb_checkpoints/ .python-version .venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..676b025 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +default_stages: [commit] +fail_fast: true +repos: +- repo: local + hooks: + - id: doctests + name: Run the xdoctests in the source files + entry: poetry run nox -s doctests -- + language: system + stages: [commit, merge-commit] + types: [python] + - id: fix-branch-references + name: Check for wrong branch references + entry: poetry run nox -s fix-branch-references -- + language: system + stages: [commit, merge-commit] + types: [text] +# Enable hooks provided by the pre-commit project to +# enforce rules that local tools could not that easily. +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: check-added-large-files + args: [--maxkb=250] + - id: check-merge-conflict + - id: no-commit-to-branch + args: [--branch, main] + - id: trailing-whitespace diff --git a/00_intro/00_content.ipynb b/00_intro/00_content.ipynb new file mode 100644 index 0000000..0b6ba58 --- /dev/null +++ b/00_intro/00_content.ipynb @@ -0,0 +1,873 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/00_intro/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# An Introduction to Python and Programming" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This book is a *thorough* introduction to programming in [Python ](https://www.python.org/).\n", + "\n", + "It teaches the concepts behind and the syntax of the core Python language as defined by the [Python Software Foundation ](https://www.python.org/psf/) in the official [language reference ](https://docs.python.org/3/reference/index.html). Furthermore, it introduces commonly used functionalities from the [standard library ](https://docs.python.org/3/library/index.html) and popular third-party libraries like [numpy ](https://www.numpy.org/), [pandas ](https://pandas.pydata.org/), [matplotlib ](https://matplotlib.org/), and others." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Prerequisites" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There are *no* prerequisites for reading this book." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Objective" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The **main goal** of this introduction is to **prepare** the student **for further studies** in the \"field\" of **data science**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### Why data science?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The term **[data science ](https://en.wikipedia.org/wiki/Data_science)** is rather vague and does *not* refer to an academic discipline. Instead, the term was popularized by the tech industry, who also coined non-meaningful job titles such as \"[rockstar](https://www.quora.com/Why-are-engineers-called-rockstars-and-ninjas)\" or \"[ninja developers](https://www.quora.com/Why-are-engineers-called-rockstars-and-ninjas).\" Most *serious* definitions describe the field as being **multi-disciplinary** *integrating* scientific methods, algorithms, and systems thinking to extract knowledge from structured and unstructured data, *and* also emphasize the importance of **[domain knowledge ](https://en.wikipedia.org/wiki/Domain_knowledge)**.\n", + "\n", + "Recently, this integration aspect feeds back into the academic world. The [MIT](https://www.mit.edu/), for example, created the new [Stephen A. Schwarzman College of Computing](http://computing.mit.edu) for [artificial intelligence ](https://en.wikipedia.org/wiki/Artificial_intelligence) with a 1 billion dollar initial investment where students undergo a \"bilingual\" curriculum with half the classes in quantitative and method-centric fields and the other half in domains such as biology, business, chemistry, politics, (art) history, or linguistics (cf., the [official Q&As](http://computing.mit.edu/faq/) or this [NYT article](https://www.nytimes.com/2018/10/15/technology/mit-college-artificial-intelligence.html)). Their strategists see a future where programming skills are just as naturally embedded into students' curricula as are nowadays subjects like calculus, statistics, or academic writing. Then, programming literacy is not just another \"nice to have\" skill but a prerequisite, or an enabler, to understanding more advanced topics in the actual domains studied." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Installation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To \"read\" this book in the most meaningful way, a working installation of **Python 3.8** with [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) is needed.\n", + "\n", + "For a tutorial on how to install Python on your computer, follow the instructions in the [README.md](https://github.com/webartifex/intro-to-python/blob/main/README.md#installation) file in the project's [GitHub repository ](https://github.com/webartifex/intro-to-python). If you cannot install Python on your own machine, you may open the book interactively in the cloud with [Binder ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Jupyter Notebooks" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The document you are viewing is a so-called [Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/notebook.html), a file format introduced by the [Jupyter Project](https://jupyter.org/).\n", + "\n", + "\"Jupyter\" is an [acronym ](https://en.wikipedia.org/wiki/Acronym) derived from the names of the three major programming languages **[Julia](https://julialang.org/)**, **[Python ](https://www.python.org)**, and **[R](https://www.r-project.org/)**, all of which play significant roles in the world of data science. The Jupyter Project's idea is to serve as an integrating platform such that different programming languages and software packages can be used together within the same project.\n", + "\n", + "Jupyter notebooks have become a de-facto standard for communicating and exchanging results in the data science community - both in academia and business - and provide an alternative to command-line interface (CLI or \"terminal\") based ways of running Python code. As an example for the latter case, we could start the default [Python interpreter ](https://docs.python.org/3/tutorial/interpreter.html) that comes with every installation by typing the `python` command into a CLI (or `poetry run python` if the project is managed with the [poetry](https://python-poetry.org/docs/) CLI tool as explained in the [README.md](https://github.com/webartifex/intro-to-python/blob/main/README.md#alternative-installation-for-instructors) file). Then, as the screenshot below shows, we could execute Python code like `1 + 2` or `print(\"Hello World\")` line by line simply by typing it following the `>>>` **prompt** and pressing the **Enter** key. For an introductory course, however, this would be rather tedious and probably scare off many beginners." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "One reason for the popularity of Jupyter notebooks is that they allow mixing text with code in the same document. Text may be formatted with the [Markdown ](https://guides.github.com/features/mastering-markdown/) language and mathematical formulas typeset with [LaTeX](https://www.overleaf.com/learn/latex/Free_online_introduction_to_LaTeX_%28part_1%29). Moreover, we may include pictures, plots, and even videos. Because of these features, the notebooks developed for this book come in a self-contained \"tutorial\" style enabling students to simply read them from top to bottom while executing the code snippets.\n", + "\n", + "Other ways of running Python code are to use the [IPython ](https://ipython.org/) CLI tool instead of the default interpreter or a full-fledged [Integrated Development Environment ](https://en.wikipedia.org/wiki/Integrated_development_environment) (e.g., the commercial [PyCharm](https://www.jetbrains.com/pycharm/) or the free [Spyder ](https://github.com/spyder-ide/spyder) that comes with the Anaconda Distribution)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Markdown Cells vs. Code Cells" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A Jupyter notebook consists of cells that have a type associated with them. So far, only cells of type \"Markdown\" have been used, which is the default way to present formatted text.\n", + "\n", + "The cells below are examples of \"Code\" cells containing actual Python code: They calculate the sum of `1` and `2` and print out `\"Hello World\"` when executed, respectively. To edit an existing code cell, enter into it with a mouse click. You are \"in\" a code cell if its frame is highlighted in blue. We call that the **edit mode**.\n", + "\n", + "There is also a **command mode** that you reach by hitting the **Escape** key. That un-highlights the frame. You are now \"out\" of but still \"on\" the cell. If you were already in command mode, hitting the Escape key does *nothing*.\n", + "\n", + "Using the **Enter** and **Escape** keys, you can now switch between the two modes.\n", + "\n", + "To **execute**, or \"run,\" a code cell, hold down the **Control** key and press **Enter**. Note how you do *not* go to the subsequent cell if you keep re-executing the cell you are on. Alternatively, you can hold the **Shift** key and press **Enter**, which executes a cell *and* places your focus on the subsequent cell or creates a new one if there is none." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 + 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello World\n" + ] + } + ], + "source": [ + "print(\"Hello World\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, a Markdown cell is also in either edit or command mode. For example, double-click on the text you are reading: This puts you into edit mode. Now, you could change the formatting (e.g., print a word in *italics* or **bold**) and \"execute\" the cell to render the text as specified.\n", + "\n", + "To change a cell's type, choose either \"Code\" or \"Markdown\" in the navigation bar at the top. Alternatively, you can press either the **Y** or **M** key in command mode.\n", + "\n", + "Sometimes, a code cell starts with an exclamation mark `!`. Then, the Jupyter notebook behaves as if the following command were typed directly into a terminal. The cell below asks the `python` CLI to show its version number and is *not* Python code but a command in the [Shell ](https://en.wikipedia.org/wiki/Shell_%28computing%29) language. The `!` is useful to execute short CLI commands without leaving a Jupyter notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Python 3.12.2\n" + ] + } + ], + "source": [ + "!python --version" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Programming vs. Computer Science vs. IT" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this book, **programming** is defined as\n", + "- a *structured* way of *problem-solving*\n", + "- by *expressing* the steps of a *computation* or *process*\n", + "- and thereby *documenting* the process in a formal way.\n", + "\n", + "Programming is always *concrete* and based on a *particular case*. It exhibits elements of an *art* or a *craft* as we hear programmers call code \"beautiful\" or \"ugly\" or talk about the \"expressive\" power of an application." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "That is different from **computer science**, which is\n", + "- a field of study comparable to applied *mathematics* that\n", + "- asks *abstract* questions (e.g., \"Is something computable at all?\"),\n", + "- develops and analyses *algorithms* and *data structures*,\n", + "- and *proves* the *correctness* of a program.\n", + "\n", + "In a sense, a computer scientist does not need to know a programming language to work, and many computer scientists only know how to produce \"ugly\" looking code in the eyes of professional programmers." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**IT** or **information technology** is a term that has many meanings to different people. Often, it has something to do with hardware or physical devices, both of which are out of scope for programmers and computer scientists. Sometimes, it refers to a [support function](https://en.wikipedia.org/wiki/Value_chain#Support_activities) within a company. Many computer scientists and programmers are more than happy if their printer and internet connection work as they often do not know a lot more about that than \"non-technical\" people." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Why Python?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### What is Python?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Here is a brief history of and some background on Python (cf., also this [TechRepublic article](https://www.techrepublic.com/article/python-is-eating-the-world-how-one-developers-side-project-became-the-hottest-programming-language-on-the-planet/) for a more elaborate story):\n", + "\n", + "- [Guido van Rossum ](https://en.wikipedia.org/wiki/Guido_van_Rossum) (Python’s **[Benevolent Dictator for Life ](https://en.wikipedia.org/wiki/Benevolent_dictator_for_life)**) was bored during a week around Christmas 1989 and started Python as a hobby project \"that would keep \\[him\\] occupied\" for some days\n", + "- the idea was to create a **general-purpose** scripting **language** that would allow fast *prototyping* and would *run on every operating system*\n", + "- Python grew through the 90s as van Rossum promoted it via his \"Computer Programming for Everybody\" initiative that had the *goal to encourage a basic level of coding literacy* as an equal knowledge alongside English literacy and math skills\n", + "- to become more independent from its creator, the next major version **Python 2** - released in 2000 and still in heavy use as of today - was **open-source** from the get-go which attracted a *large and global community of programmers* that *contributed* their expertise and best practices in their free time to make Python even better\n", + "- **Python 3** resulted from a significant overhaul of the language in 2008 taking into account the *learnings from almost two decades*, streamlining the language, and getting ready for the age of **big data**\n", + "- the language is named after the sketch comedy group [Monty Python ](https://en.wikipedia.org/wiki/Monty_Python)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "#### Summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python is a **general-purpose** programming **language** that allows for *fast development*, is *easy to read*, **open-source**, long-established, unifies the knowledge of *hundreds of thousands of experts* around the world, runs on basically every machine, and can handle the complexities of applications involving **big data**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Why open-source?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Couldn't a company like Google, Facebook, or Microsoft come up with a better programming language? The following is an argument on why this can likely not be the case.\n", + "\n", + "Wouldn't it be weird if professors and scholars of English literature and language studies dictated how we'd have to speak in day-to-day casual conversations or how authors of poesy and novels should use language constructs to achieve a particular type of mood? If you agree with that premise, it makes sense to assume that even programming languages should evolve in a \"natural\" way as users *use* the language over time and in *new* and *unpredictable* contexts creating new conventions." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Loose *communities* are the primary building block around which open-source software projects are built. Someone - like Guido - starts a project and makes it free to use for anybody (e.g., on a code-sharing platform like [GitHub ](https://github.com/)). People find it useful enough to solve one of their daily problems and start using it. They see how a project could be improved and provide new use cases (e.g., via the popularized concept of a [pull request ](https://help.github.com/articles/about-pull-requests/)). The project grows both in lines of code and people using it. After a while, people start local user groups to share their same interests and meet regularly (e.g., this is a big market for companies like [Meetup](https://www.meetup.com/) or non-profits like [PyData ](https://pydata.org/)). Out of these local and usually monthly meetups grow yearly conferences on the country or even continental level (e.g., the original [PyCon ](https://us.pycon.org/) in the US, [EuroPython ](https://europython.eu/), or [PyCon.DE ](https://de.pycon.org/)). The content presented at these conferences is made publicly available via GitHub and YouTube (e.g., [PyCon 2019 ](https://www.youtube.com/channel/UCxs2IIVXaEHHA4BtTiWZ2mQ) or [EuroPython ](http://europython.tv/)) and serves as references on what people are working on and introductions to the endless number of specialized fields." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While these communities are somewhat loose and continuously changing, smaller in-groups, often democratically organized and elected (e.g., the [Python Software Foundation ](https://www.python.org/psf/)), take care of, for example, the development of the \"core\" Python language itself.\n", + "\n", + "Python itself is just a specification (i.e., a set of rules) as to what is allowed and what not: It must first be implemented (c.f., next section below). The current version of Python can always be looked up in the [Python Language Reference ](https://docs.python.org/3/reference/index.html). To make changes to that, anyone can make a so-called **[Python Enhancement Proposal ](https://www.python.org/dev/peps/)**, or **PEP** for short, where it needs to be specified what exact changes are to be made and argued why that is a good thing to do. These PEPs are reviewed by the [core developers ](https://devguide.python.org/coredev/) and interested people and are then either accepted, modified, or rejected if, for example, the change introduces internal inconsistencies. This process is similar to the **double-blind peer review** established in academia, just a lot more transparent. Many of the contributors even held or hold positions in academia, one more indicator of the high quality standards in the Python community. To learn more about PEPs, check out [PEP 1 ](https://www.python.org/dev/peps/pep-0001/) that describes the entire process.\n", + "\n", + "In total, no one single entity can control how the language evolves, and the users' needs and ideas always feed back to the language specification via a quality controlled and \"democratic\" process." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Besides being **free** as in \"free beer,\" a major benefit of open-source is that one can always *look up how something works in detail*: That is the literal meaning of *open* source and a difference to commercial languages (e.g., [MATLAB](https://www.mathworks.com/products/matlab.html)) as a programmer can always continue to *study best practices* or find out how things are implemented. Along this way, many *errors are uncovered*, as well. Furthermore, if one runs an open-source application, one can be reasonably sure that no bad people built in a \"backdoor.\" [Free software ](https://en.wikipedia.org/wiki/Free_software) is consequently free of charge but brings *many other freedoms* with it, most notably the freedom to change the code." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Isn't C a lot faster?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The default Python implementation is written in the C language and called CPython. This is also what the Anaconda Distribution uses.\n", + "\n", + "[C ](https://en.wikipedia.org/wiki/C_%28programming_language%29) and [C++ ](https://en.wikipedia.org/wiki/C%2B%2B) (cf., this [introduction](https://www.learncpp.com/)) are wide-spread and long-established (i.e., since the 1970s) programming languages employed in many mission-critical software systems (e.g., operating systems themselves, low latency databases and web servers, nuclear reactor control systems, airplanes, ...). They are fast, mainly because the programmer not only needs to come up with the **business logic** but also manage the computer's memory.\n", + "\n", + "In contrast, Python automatically manages the memory for the programmer. So, speed here is a trade-off between application run time and engineering/development time. Often, the program's run time is not that important: For example, what if C needs 0.001 seconds in a case where Python needs 0.1 seconds to do the same thing? When the requirements change and computing speed becomes an issue, the Python community offers many third-party libraries - usually also written in C - where specific problems can be solved in near-C time." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "#### Summary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While it is true that a language like C is a lot faster than Python when it comes to *pure* **computation time**, this does not matter in many cases as the *significantly shorter* **development cycles** are the more significant cost factor in a rapidly changing world." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Who uses it?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While ad-hominem arguments are usually not the best kind of reasoning, we briefly look at some examples of who uses Python and leave it up to the reader to decide if this is convincing or not:\n", + "\n", + "- **[Massachusetts Institute of Technology](https://www.mit.edu/)**\n", + " - teaches Python in its [introductory course](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/) to computer science independent of the student's major\n", + " - replaced the infamous course on the [Scheme](https://groups.csail.mit.edu/mac/projects/scheme/) language (cf., [source ](https://news.ycombinator.com/item?id=602307))\n", + "- **[Google](https://www.google.com/)**\n", + " - used the strategy \"Python where we can, C++ where we must\" from its early days on to stay flexible in a rapidly changing environment (cf., [source ](https://stackoverflow.com/questions/2560310/heavy-usage-of-python-at-google))\n", + " - the very first web-crawler was written in Java and so difficult to maintain that it was rewritten in Python right away (cf., [source](https://www.amazon.com/Plex-Google-Thinks-Works-Shapes/dp/1416596585/ref=sr_1_1?ie=UTF8&qid=1539101827&sr=8-1&keywords=in+the+plex))\n", + " - Guido van Rossom was hired by Google from 2005 to 2012 to advance the language there\n", + "- **[NASA](https://www.nasa.gov/)** open-sources many of its projects, often written in Python and regarding analyses with big data (cf., [source](https://code.nasa.gov/language/python/))\n", + "- **[Facebook](https://facebook.com/)** uses Python besides C++ and its legacy PHP (a language for building websites; the \"cool kid\" from the early 2000s)\n", + "- **[Instagram](https://instagram.com/)** operates the largest installation of the popular **web framework [Django](https://www.djangoproject.com/)** (cf., [source](https://instagram-engineering.com/web-service-efficiency-at-instagram-with-python-4976d078e366))\n", + "- **[Spotify](https://spotify.com/)** bases its data science on Python (cf., [source](https://labs.spotify.com/2013/03/20/how-we-use-python-at-spotify/))\n", + "- **[Netflix](https://netflix.com/)** also runs its predictive models on Python (cf., [source](https://medium.com/netflix-techblog/python-at-netflix-86b6028b3b3e))\n", + "- **[Dropbox](https://dropbox.com/)** \"stole\" Guido van Rossom from Google to help scale the platform (cf., [source](https://medium.com/dropbox-makers/guido-van-rossum-on-finding-his-way-e018e8b5f6b1))\n", + "- **[JPMorgan Chase](https://www.jpmorganchase.com/)** requires new employees to learn Python as part of the onboarding process starting with the 2018 intake (cf., [source](https://www.ft.com/content/4c17d6ce-c8b2-11e8-ba8f-ee390057b8c9?segmentId=a7371401-027d-d8bf-8a7f-2a746e767d56))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As images tell more than words, here are two plots of popular languages' \"market shares\" based on the number of questions asked on [Stack Overflow ](https://stackoverflow.blog/2017/09/06/incredible-growth-python/), the most relevant platform for answering programming-related questions: As of late 2017, Python surpassed [Java](https://www.java.com/en/), heavily used in big corporates, and [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), the \"language of the internet\" that does everything in web browsers, in popularity. Two blog posts from \"technical\" people explain this in more depth to the layman: [Stack Overflow ](https://stackoverflow.blog/2017/09/14/python-growing-quickly/) and [DataCamp](https://www.datacamp.com/community/blog/python-scientific-computing-case)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the graph below shows, neither Google's very own language **[Go](https://golang.org/)** nor **[R](https://www.r-project.org/)**, a domain-specific language in the niche of statistics, can compete with Python's year-to-year growth." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[IEEE Sprectrum](https://spectrum.ieee.org/computing/software/the-top-programming-languages-2019) provides a more recent comparison of programming language's popularity. Even news and media outlets notice the recent popularity of Python: [Economist](https://www.economist.com/graphic-detail/2018/07/26/python-is-becoming-the-worlds-most-popular-coding-language), [Huffington Post](https://www.huffingtonpost.com/entry/why-python-is-the-best-programming-language-with-which_us_59ef8f62e4b04809c05011b9), [TechRepublic](https://www.techrepublic.com/article/why-python-is-so-popular-with-developers-3-reasons-the-language-has-exploded/), and [QZ](https://qz.com/1408660/the-rise-of-python-as-seen-through-a-decade-of-stack-overflow/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## How to learn Programming" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### ABC Rule" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**A**lways **b**e **c**oding.\n", + "\n", + "Programming is more than just writing code into a text file. It means reading through parts of the [documentation ](https://docs.python.org/), blogs with best practices, and tutorials, or researching problems on [Stack Overflow ](https://stackoverflow.com/) while trying to implement features in the application at hand. Also, it means using command-line tools to automate some part of the work or manage different versions of a program, for example, with **[git](https://git-scm.com/)**. In short, programming involves a lot of \"muscle memory,\" which can only be built and kept up through near-daily usage.\n", + "\n", + "Further, many aspects of software architecture and best practices can only be understood after having implemented some requirements for the very first time. Coding also means \"breaking\" things to find out what makes them work in the first place.\n", + "\n", + "Therefore, coding is learned best by just doing it for some time on a daily or at least a regular basis and not right before some task is due, just like learning a \"real\" language." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### The Maker's Schedule" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[Y Combinator ](https://www.ycombinator.com/) co-founder [Paul Graham ](https://en.wikipedia.org/wiki/Paul_Graham_%28programmer%29) wrote a very popular and often cited [article](http://www.paulgraham.com/makersschedule.html) where he divides every person into belonging to one of two groups:\n", + "\n", + "- **Managers**: People that need to organize things and command others (e.g., a \"boss\" or manager). Their schedule is usually organized by the hour or even 30-minute intervals.\n", + "- **Makers**: People that create things (e.g., programmers, artists, or writers). Such people think in half days or full days.\n", + "\n", + "Have you ever wondered why so many tech people work during nights and sleep at \"weird\" times? The reason is that many programming-related tasks require a \"flow\" state in one's mind that is hard to achieve when one can get interrupted, even if it is only for one short question. Graham describes that only knowing that one has an appointment in three hours can cause a programmer to not get into a flow state.\n", + "\n", + "As a result, do not set aside a certain amount of time for learning something but rather plan in an *entire evening* or a *rainy Sunday* where you can work on a problem in an *open end* setting. And do not be surprised anymore to hear \"I looked at it over the weekend\" from a programmer." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "### Phase Iteration" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When being asked the above question, most programmers answer something that can be classified into one of two broader groups.\n", + "\n", + "**1) Toy Problem, Case Study, or Prototype**: Pick some problem, break it down into smaller sub-problems, and solve them with an end in mind.\n", + "\n", + "**2) Books, Video Tutorials, and Courses**: Research the best book, blog, video, or tutorial for something and work it through from start to end.\n", + "\n", + "The truth is that you need to iterate between these two phases.\n", + "\n", + "Building a prototype always reveals issues no book or tutorial can think of before. Data is never as clean as it should be. An algorithm from a textbook must be adapted to a peculiar aspect of a case study. It is essential to learn to \"ship a product\" because only then will one have looked at all the aspects.\n", + "\n", + "The major downside of this approach is that one likely learns bad \"patterns\" overfitted to the case at hand, and one does not get the big picture or mental concepts behind a solution. This gap can be filled in by well-written books: For example, check the Python/programming books offered by [Packt](https://www.packtpub.com/packt/offers/free-learning/) or [O’Reilly](https://www.oreilly.com/)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Contents" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Part A: Expressing Logic**\n", + "\n", + "- What is a programming language? What kind of words exist?\n", + " - *Chapter 1*: [Elements of a Program ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb)\n", + " - *Chapter 2*: [Functions & Modularization ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb)\n", + "- What is the flow of execution? How can we form sentences from words?\n", + " - *Chapter 3*: [Conditionals & Exceptions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb)\n", + " - *Chapter 4*: [Recursion & Looping ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Part B: Managing Data and Memory**\n", + "\n", + "- How is data stored in memory?\n", + " - *Chapter 5*: [Numbers & Bits ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb)\n", + " - *Chapter 6*: [Text & Bytes ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb)\n", + " - *Chapter 7*: [Sequential Data ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb)\n", + " - *Chapter 8*: [Map, Filter, & Reduce ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb)\n", + " - *Chapter 9*: [Mappings & Sets ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb)\n", + " - *Chapter 10*: Arrays & Dataframes\n", + "- How can we create custom data types?\n", + " - *Chapter 11*: [Classes & Instances ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## xkcd Comic" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with every good book, there has to be a [xkcd](https://xkcd.com/353/) comic somewhere." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import antigravity" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/00_intro/01_exercises_markdown.ipynb b/00_intro/01_exercises_markdown.ipynb new file mode 100644 index 0000000..d80f9d8 --- /dev/null +++ b/00_intro/01_exercises_markdown.ipynb @@ -0,0 +1,115 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/00_intro/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 0: Introduction (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read [Chapter 0 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/00_intro/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Mastering Markdown" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Briefly review GitHub's guide on [Mastering Markdown](https://guides.github.com/features/mastering-markdown/) and create nicely formatted \"text\" cells below!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Check the latest [Bundesliga standings](https://www.bundesliga.com/en/bundesliga/table) and provide a table of the top three teams with the following four columns: rank, team name, games played, and points scored. Render the rank in **bold**, make the team name a clickable link (to the team's website), and put both the games played and points scored in *italics*. The header row should be visually different from the three rows with the teams' information." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: The quote \"Education is what remains after one has forgotten what one has learned in school\" is attributed to Albert Einstein. Display the author and his quote appropriately!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Integrate the image of the delicious dessert milk rice at this [URL](https://i.ytimg.com/vi/-BoSRlzy9c4/maxresdefault.jpg) into this notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/00_intro/02_review.ipynb b/00_intro/02_review.ipynb new file mode 100644 index 0000000..bf3a39a --- /dev/null +++ b/00_intro/02_review.ipynb @@ -0,0 +1,215 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 0: Introduction (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read [Chapter 0 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/00_intro/00_content.ipynb).\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Describe the difference between the terms **programming** and **computer science**!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Explain what is a **pull request** and elaborate on how this concept fits a *distributed* organization of work!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: In what sense are **open-source** communities democracies? How are they near-perfect [meritocracies ](https://en.wikipedia.org/wiki/Meritocracy)? How is open-source software development similar to academia?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What is a *significant* advantage of a \"slow\" programming language like Python over a faster one like C?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Python has been the fastest-growing *major* programming language in recent years." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Python is named after a snake to emphasize its agility and fast development speed right in its name." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Python was initially designed for highly intensive numerical computing, in particular for use cases from physics and astronomy." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: JavaScript is a subset of the Java language." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Python is **free software**. That means it does not cost anything." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: The primary purpose of PEPs is to regulate how code should be documented and styled." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/00_intro/static/cli_example.png b/00_intro/static/cli_example.png new file mode 100644 index 0000000..0e40e04 Binary files /dev/null and b/00_intro/static/cli_example.png differ diff --git a/00_intro/static/example_python_users.png b/00_intro/static/example_python_users.png new file mode 100644 index 0000000..b123bfb Binary files /dev/null and b/00_intro/static/example_python_users.png differ diff --git a/00_intro/static/growth_of_major_programming_languages.png b/00_intro/static/growth_of_major_programming_languages.png new file mode 100644 index 0000000..acfdfbb Binary files /dev/null and b/00_intro/static/growth_of_major_programming_languages.png differ diff --git a/00_intro/static/growth_of_smaller_programming_languages.png b/00_intro/static/growth_of_smaller_programming_languages.png new file mode 100644 index 0000000..51123f2 Binary files /dev/null and b/00_intro/static/growth_of_smaller_programming_languages.png differ diff --git a/00_intro/static/logo.png b/00_intro/static/logo.png new file mode 100644 index 0000000..b3b5b22 Binary files /dev/null and b/00_intro/static/logo.png differ diff --git a/00_intro/static/xkcd.png b/00_intro/static/xkcd.png new file mode 100644 index 0000000..23a4c6e Binary files /dev/null and b/00_intro/static/xkcd.png differ diff --git a/01_elements/00_content.ipynb b/01_elements/00_content.ipynb new file mode 100644 index 0000000..af2d022 --- /dev/null +++ b/01_elements/00_content.ipynb @@ -0,0 +1,2188 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 1: Elements of a Program" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Do you remember how you first learned to speak in your mother tongue? Probably not. No one's memory goes back that far. Your earliest memory as a child should probably be around the age of three or four years old when you could already say simple things and interact with your environment. Although you did not know any grammar rules yet, other people just understood what you said. At least most of the time.\n", + "\n", + "It is intuitively best to take the very mindset of a small child when learning a new language. And a programming language is no different from that. This first chapter introduces simplistic examples and we accept them as they are *without* knowing any of the \"grammar\" rules yet. Then, we analyze them in parts and slowly build up our understanding.\n", + "\n", + "Consequently, if parts of this chapter do not make sense right away, let's not worry too much. Besides introducing the basic elements, it also serves as an outlook for what is to come. So, many terms and concepts used here are deconstructed in great detail in the following chapters." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example: Averaging all even Numbers in a List" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As our introductory example, we want to calculate the *average* of all *evens* in a **list** of whole numbers: `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]`.\n", + "\n", + "While we are used to finding an [analytical solution](https://math.stackexchange.com/questions/935405/what-s-the-difference-between-analytical-and-numerical-approaches-to-problems/935446#935446) in math (i.e., derive some equation with \"pen and paper\"), we solve this task *programmatically* instead.\n", + "\n", + "We start by creating a list called `numbers` that holds all the individual numbers between **brackets** `[` and `]`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To verify that something happened in our computer's memory, we **reference** `numbers`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, so good. Let's see how the desired **computation** could be expressed as a **sequence of instructions** in the next code cell.\n", + "\n", + "Intuitively, the line `for number in numbers` describes a \"loop\" over all the numbers in the `numbers` list, one at a time.\n", + "\n", + "The `if number % 2 == 0` may look confusing at first sight. Both `%` and `==` must have an unintuitive meaning here. Luckily, the **comment** in the same line after the `#` symbol has the answer: The program does something only for an even `number`.\n", + "\n", + "In particular, it increases `count` by `1` and adds the current `number` onto the [running ](https://en.wikipedia.org/wiki/Running_total) `total`. Both `count` and `number` are **initialized** to `0` and the single `=` symbol reads as \"... is *set* equal to ...\". It cannot indicate a mathematical equation as, for example, `count` is generally *not* equal to `count + 1`.\n", + "\n", + "Lastly, the `average` is calculated as the ratio of the final **values** of `total` and `count`. Overall, we divide the sum of all even numbers by their count: This is nothing but the definition of an average.\n", + "\n", + "The lines of code \"within\" the `for` and `if` **statements** are **indented** and aligned with multiples of *four spaces*: This shows immediately how the lines relate to each other." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "count = 0 # initialize variables to keep track of the\n", + "total = 0 # running total and the count of even numbers\n", + "\n", + "for number in numbers:\n", + " if number % 2 == 0: # only work with even numbers\n", + " count = count + 1\n", + " total = total + number\n", + "\n", + "average = total / count" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We do not see any **output** yet but obtain the value of `average` by referencing it again." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Output in a Jupyter Notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Only two of the previous four code cells generate an **output** while two remained \"silent\" (i.e., nothing appears below the cell after running it).\n", + "\n", + "By default, Jupyter notebooks only show the value of the **expression** in the last line of a code cell. And, this output may also be suppressed by ending the last line with a semicolon `;`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'I am feeling great :-)'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Hello, World!\"\n", + "\"I am feeling great :-)\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "\"I am invisible!\";" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To see any output other than that, we use the built-in [print() ](https://docs.python.org/3/library/functions.html#print) **function**. Here, the parentheses `()` indicate that we **call** (i.e., \"execute\") code written somewhere else." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello, World!\n", + "I am feeling great :-)\n" + ] + } + ], + "source": [ + "print(\"Hello, World!\")\n", + "print(\"I am feeling great :-)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Outside Jupyter notebooks, the semicolon `;` is used as a **separator** between statements that must otherwise be on a line on their own. However, it is *not* considered good practice to use it as it makes code less readable." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello, World!\n", + "I am feeling great :-)\n" + ] + } + ], + "source": [ + "print(\"Hello, World!\"); print(\"I am feeling great :-)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## (Arithmetic) Operators" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python comes with many built-in **[operators ](https://docs.python.org/3/reference/lexical_analysis.html#operators)**: They are **tokens** (i.e., \"symbols\") that have a special meaning to the Python interpreter.\n", + "\n", + "The arithmetic operators either \"operate\" with the number immediately following them, so-called **unary** operators (e.g., negation), or \"process\" the two numbers \"around\" them, so-called **binary** operators (e.g., addition).\n", + "\n", + "By definition, operators on their own have *no* permanent **side effects** in the computer's memory. Although the code cells in this section do indeed create *new* numbers in memory (e.g., `77 + 13` creates `90`), they are immediately \"forgotten\" as they are not stored in a **variable** like `numbers` or `average` above. We develop this thought further in the second part of this chapter when we compare **expressions** with **statements**.\n", + "\n", + "Let's see some examples of operators. We start with the binary `+` and the `-` operators for addition and subtraction. Binary operators mimic what mathematicians call [infix notation ](https://en.wikipedia.org/wiki/Infix_notation) and have the expected meaning." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "90" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "77 + 13" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "101 - 93" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `-` operator may be used as a unary operator as well. Then, it unsurprisingly flips the sign of a number." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When we compare the output of the `*` and `/` operators for multiplication and division, we note the subtle *difference* between the `42` and the `42.0`: They are the *same* number represented as a *different* **data type**." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 * 21" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "84 / 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The so-called **floor division operator** `//` always \"rounds\" to an integer and is thus also called **integer division operator**. It is an example of an arithmetic operator we commonly do not know from high school mathematics." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "84 // 2" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "85 // 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Even though it appears that the `//` operator **truncates** (i.e., \"cuts off\") the decimals so as to effectively round down (i.e., the `42.5` became `42` in the previous code cell), this is *not* the case: The result is always \"rounded\" towards minus infinity!" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-43" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-85 // 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To obtain the remainder of a division, we use the **modulo operator** `%`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "85 % 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The remainder is `0` *only if* a number is *divisible* by another.\n", + "\n", + "A popular convention in both computer science and mathematics is to abbreviate \"only if\" as \"iff\", which is short for \"**[if and only if ](https://en.wikipedia.org/wiki/If_and_only_if)**.\" The iff means that a remainder of `0` implies that a number is divisible by another but also that a number's being divisible by another implies a remainder of `0`. The implication goes in *both* directions!\n", + "\n", + "So, `49` is divisible by `7`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "49 % 7" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Modulo division is also useful if we want to extract the last couple of digits in a large integer." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "789 % 10" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "89" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "789 % 100" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The built-in [divmod() ](https://docs.python.org/3/library/functions.html#divmod) function combines the integer and modulo divisions into one step. However, grammatically this is *not* an operator but a function. Also, [divmod() ](https://docs.python.org/3/library/functions.html#divmod) returns a \"pair\" of integers and not a single one." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(4, 2)" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "divmod(42, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Raising a number to a power is performed with the **exponentiation operator** `**`. It is different from the `^` operator other programming languages may use and that also exists in Python with a *different* meaning." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 ** 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The standard [order of precedence ](https://docs.python.org/3/reference/expressions.html#operator-precedence) from mathematics applies (i.e., [PEMDAS](http://mathworld.wolfram.com/PEMDAS.html) rule) when several operators are combined." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "18" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "3 ** 2 * 2 " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Parentheses help avoid confusion and take the role of a **delimiter** here." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "18" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(3 ** 2) * 2" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "81" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "3 ** (2 * 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Some programmers also use \"style\" conventions. For example, we might play with the **whitespace**, which is an umbrella term that refers to any non-printable sign like spaces, tabs, or the like. However, this is *not* a good practice and parentheses convey a much clearer picture." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "18" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "3**2 * 2 # bad style; it is better to use parentheses here" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There exist many non-mathematical operators that are introduced throughout this book, together with the concepts they implement. They often come in a form different from the unary and binary ones mentioned above." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Operator Overloading" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python **overloads** certain operators. For example, you may not only \"add\" numbers but also text: This is called **concatenation**." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "greeting = \"Hi \"\n", + "audience = \"class\"" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hi class'" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "greeting + audience" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Multiplying text with a whole number also works." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hi Hi Hi Hi Hi Hi Hi Hi Hi Hi '" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10 * greeting" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Objects vs. Types vs. Values" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python is a so-called **object-oriented** language, which is a paradigm of organizing a program's memory.\n", + "\n", + "An **object** may be viewed as a \"bag\" of $0$s and $1$s in a given memory location. The $0$s and $1$s in a bag make up the object's **value**. There exist different **types** of bags, and each type comes with its own rules how the $0$s and $1$s are interpreted and may be worked with.\n", + "\n", + "So, an object *always* has *three* main characteristics. Let's look at the following examples and work them out." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a = 42\n", + "b = 42.0\n", + "c = \"Python rocks\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Identity / \"Memory Location\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The built-in [id() ](https://docs.python.org/3/library/functions.html#id) function shows an object's \"address\" in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94371758832672" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(a)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140673826512720" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(b)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140673817995952" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(c)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "These addresses are *not* meaningful for anything other than checking if two variables reference the *same* object.\n", + "\n", + "Obviously, `a` and `b` have the same *value* as revealed by the **equality operator** `==`: We say `a` and `b` \"evaluate equal.\" The resulting `True` - and the `False` further below - is yet another data type, a so-called **boolean**. We look into them in [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#Boolean-Expressions)." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a == b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, `a` and `b` are *different* objects as the **identity operator** `is` shows: They are stored at *different* addresses in the memory." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a is b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we want to check the opposite case, we use the negated version of the `is` operator, namely `is not`." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a is not b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### (Data) Type / \"Behavior\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [type() ](https://docs.python.org/3/library/functions.html#type) built-in shows an object's type. For example, `a` is an integer (i.e., `int`) while `b` is a so-called [floating-point number ](https://en.wikipedia.org/wiki/Floating-point_arithmetic) (i.e., `float`)." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "int" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(a)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "float" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(b)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Different types imply different behaviors for the objects. The `b` object, for example, may be \"asked\" if it is a whole number with the [.is_integer() ](https://docs.python.org/3/library/stdtypes.html#float.is_integer) \"functionality\" that comes with *every* `float` object.\n", + "\n", + "Formally, we call such type-specific functionalities **methods** (i.e., as opposed to functions) and we look at them in detail in [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb). For now, it suffices to know that we access them with the **dot operator** `.` on the object. Of course, `b` is a whole number, which the boolean object `True` tells us." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b.is_integer()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For an `int` object, this [.is_integer() ](https://docs.python.org/3/library/stdtypes.html#float.is_integer) check does *not* make sense as we already know it is an `int`: We see the `AttributeError` below as `a` does not even know what `is_integer()` means." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'int' object has no attribute 'is_integer'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\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[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mis_integer\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;31mAttributeError\u001b[0m: 'int' object has no attribute 'is_integer'" + ] + } + ], + "source": [ + "a.is_integer()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `c` object is a so-called **string** type (i.e., `str`), which is Python's way of representing text. Strings also come with peculiar behaviors, for example, to make a text lower or upper case." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "str" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(c)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'python rocks'" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c.lower()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'PYTHON ROCKS'" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c.upper()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Value / (Semantic) \"Meaning\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Almost trivially, every object also has a value to which it **evaluates** when referenced. We think of the value as the **conceptual idea** of what the $0$s and $1$s in the bag mean to *humans*. In other words, an object's value regards its *semantic* meaning.\n", + "\n", + "For built-in data types, Python prints out an object's value as a so-called **[literal ](https://docs.python.org/3/reference/lexical_analysis.html#literals)**: This means that we may copy and paste the value back into a code cell and create a *new* object with the *same* value." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this book, we follow the convention of creating strings with **double quotes** `\"` instead of the **single quotes** `'` to which Python defaults in its *literal* notation for `str` objects. Both types of quotes may be used interchangeably. So, the `\"Python rocks\"` from above and `'Python rocks'` below create two objects that evaluate equal (i.e., `\"Python rocks\" == 'Python rocks'`)." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Python rocks'" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Formal vs. Natural Languages" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Just like the language of mathematics is good at expressing relationships among numbers and symbols, any programming language is just a formal language that is good at expressing computations.\n", + "\n", + "Formal languages come with their own \"grammatical rules\" called **syntax**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Syntax Errors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we do not follow the rules, the code cannot be **parsed** correctly, i.e., the program does not even start to run but **raises** a **syntax error** indicated as `SyntaxError` in the output. Computers are very dumb in the sense that the slightest syntax error leads to the machine not understanding our code.\n", + "\n", + "If we were to write an accounting program that adds up currencies, we would, for example, have to model dollar prices as `float` objects as the dollar symbol cannot be understood by Python." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (73631267.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[47], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m 3.99 $ + 10.40 $\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "3.99 $ + 10.40 $" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python requires certain symbols at certain places (e.g., a `:` is missing here)." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "expected ':' (2545637715.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[48], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m for number in numbers\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m expected ':'\n" + ] + } + ], + "source": [ + "for number in numbers\n", + " print(number)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Furthermore, it relies on whitespace (i.e., indentation), unlike many other programming languages. The `IndentationError` below is just a particular type of a `SyntaxError`." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "IndentationError", + "evalue": "expected an indented block after 'for' statement on line 1 (1544118965.py, line 2)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[49], line 2\u001b[0;36m\u001b[0m\n\u001b[0;31m print(number)\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mIndentationError\u001b[0m\u001b[0;31m:\u001b[0m expected an indented block after 'for' statement on line 1\n" + ] + } + ], + "source": [ + "for number in numbers:\n", + "print(number)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Runtime Errors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Syntax errors are easy to find as the code does *not* even run in the first place.\n", + "\n", + "However, there are also so-called **runtime errors** that occur whenever otherwise (i.e., syntactically) correct code does not run because of invalid input. Runtime errors are also often referred to as **exceptions**.\n", + "\n", + "This example does not work because just like in the \"real\" world, Python does not know how to divide by `0`. The syntactically correct code leads to a `ZeroDivisionError`." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ZeroDivisionError", + "evalue": "division by zero", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[50], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;241;43m1\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\n", + "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" + ] + } + ], + "source": [ + "1 / 0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Semantic Errors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So-called **semantic errors**, on the contrary, are hard to spot as they do *not* crash the program. The only way to find such errors is to run a program with test input for which we can predict the output. However, testing software is a whole discipline on its own and often very hard to do in practice.\n", + "\n", + "The cell below copies our first example from above with a \"tiny\" error. How fast could you have spotted it without the comment?" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "count = 0\n", + "total = 0\n", + "\n", + "for number in numbers:\n", + " if number % 2 == 0:\n", + " count = count + 1\n", + " total = total + count # count is wrong here, it should be number\n", + "\n", + "average = total / count" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.5" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Systematically finding errors is called **debugging**. For the history of the term, see this [article ](https://en.wikipedia.org/wiki/Debugging)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Best Practices" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Thus, adhering to just syntax rules is *never* enough. Over time, **best practices** and **style guides** were created to make it less likely for a developer to mess up a program and also to allow \"onboarding\" him as a contributor to an established code base, often called **legacy code**, faster. These rules are *not* enforced by Python itself: Badly styled code still runs. At the very least, Python programs should be styled according to [PEP 8 ](https://www.python.org/dev/peps/pep-0008/) and documented \"inline\" (i.e., in the code itself) according to [PEP 257 ](https://www.python.org/dev/peps/pep-0257/).\n", + "\n", + "An easier to read version of PEP 8 is [here](https://pep8.org/). The video below features a well known **[Pythonista](https://en.wiktionary.org/wiki/Pythonista)** talking about the importance of code style." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAUDBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIChANCAgOCggIDRUNDhERExMTCAsWGBYSGBASExIBBQUFCAcIDwkJDxUVEBUVFhUVFRUSFRUVFRISEhUVFRUVFRUVFRUVFRIVEhUVFRUVFRUVFRUVFRUVFRUVFRUVFf/AABEIAWgB4AMBIgACEQEDEQH/xAAcAAEAAgMBAQEAAAAAAAAAAAAABgcEBQgDAgH/xABfEAABAwICBQUHDgsGAggFBQABAAIDBBEFEgYHEyExQVFTYZIUFyJxgZHTCCMyNUJSYnJzdKGxs8EVJCUzNIKDorK0wkNjZJOjw9HwJ1RVlKS10uEWJmWVpTY3RFZ1/8QAHAEBAAIDAQEBAAAAAAAAAAAAAAUGAwQHAgEI/8QAQBEAAQIDAwgGBwcEAwEAAAAAAQACAwQRBSExBhITQVFxkcEWYWKBobEUIjRTcpLRIyQyMzWC4QdSsvBCovEV/9oADAMBAAIRAxEAPwDjJEREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREX6innexrOmpu3L6JO9jWdNTduX0SUKmuj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P8AuncFA0U872NZ01N25fRJ3sazpqbty+iShTo9P+6dwUDRTzvY1nTU3bl9EnexrOmpu3L6JKFOj0/7p3BQNFPO9jWdNTduX0Sd7Gs6am7cvokoU6PT/uncFbCIi2F3RERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERERTTVfoizE5JZJ3HueAtaYmOLXyvcMwDnDeyMDlG8ncCLFa81NMl4RiPwCwTMwyBDMR+AULRdEt0MwoC34PpfGYWl3lefCJ67rT41qxw2YEwiSlfyOheXM8Topcwy/FynrUDDyplnGjgR10B5qHZlDBJo4OHXd9VRyKRaX6H1eG+FIBJASA2oiByXO4NlYd8Lid2+4NwASdyjqsMCPDjMDoZBB1hTUGOyM3OYahERFlWVEUz0Q1eVOIQCoMzKWJxOyzxOkfI0btoGh7bMJuASd9r8LEyin1Qwf2tbO75KKKP+POomYtuUguLXPvFxABN/BRsW15WG4tc68agCeVFUiK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U0gTprZ/a+VWyiqbvnVnQUvYl9KnfOrOgpexL6VNIE6a2f2vlUDREWNceREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREREX//2Q==\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"Hwckt4J96dI\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For example, while the above code to calculate the average of the even numbers in `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]` is correct, a Pythonista would rewrite it in a more \"Pythonic\" way and use the built-in [sum() ](https://docs.python.org/3/library/functions.html#sum) and [len() ](https://docs.python.org/3/library/functions.html#len) functions (cf., [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Built-in-Functions)) as well as a so-called **list comprehension** (cf., [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb#List-Comprehensions)). Pythonic code runs faster in many cases and is less error-prone." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "evens = [n for n in numbers if n % 2 == 0] # use of a list comprehension" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[8, 12, 2, 6, 10, 4]" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "evens" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "average = sum(evens) / len(evens) # use built-in functions" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To get a rough overview of the mindset of a typical Python programmer, look at these rules, also known as the **Zen of Python**, that are deemed so important that they are included in every Python installation." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The Zen of Python, by Tim Peters\n", + "\n", + "Beautiful is better than ugly.\n", + "Explicit is better than implicit.\n", + "Simple is better than complex.\n", + "Complex is better than complicated.\n", + "Flat is better than nested.\n", + "Sparse is better than dense.\n", + "Readability counts.\n", + "Special cases aren't special enough to break the rules.\n", + "Although practicality beats purity.\n", + "Errors should never pass silently.\n", + "Unless explicitly silenced.\n", + "In the face of ambiguity, refuse the temptation to guess.\n", + "There should be one-- and preferably only one --obvious way to do it.\n", + "Although that way may not be obvious at first unless you're Dutch.\n", + "Now is better than never.\n", + "Although never is often better than *right* now.\n", + "If the implementation is hard to explain, it's a bad idea.\n", + "If the implementation is easy to explain, it may be a good idea.\n", + "Namespaces are one honking great idea -- let's do more of those!\n" + ] + } + ], + "source": [ + "import this" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Jupyter Notebook Aspects" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "#### The Order of Code Cells is arbitrary" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We can run the code cells in a Jupyter notebook in *any* arbitrary order.\n", + "\n", + "That means, for example, that a variable defined towards the bottom could accidentally be referenced at the top of the notebook. This happens quickly when we iteratively built a program and go back and forth between cells.\n", + "\n", + "As a good practice, it is recommended to click on \"Kernel\" > \"Restart Kernel and Run All Cells\" in the navigation bar once a notebook is finished. That restarts the Python process forgetting all **state** (i.e., all variables) and ensures that the notebook runs top to bottom without any errors the next time it is opened." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "#### Notebooks are linear" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While this book is built with Jupyter notebooks, it is crucial to understand that \"real\" programs are almost never \"linear\" (i.e., top to bottom) sequences of instructions but instead may take many different **flows of execution**.\n", + "\n", + "At the same time, for a beginner's course, it is often easier to code linearly.\n", + "\n", + "In real data science projects, one would probably employ a mixed approach and put reusable code into so-called Python modules (i.e., *.py* files; cf., [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Local-Modules-and-Packages)) and then use Jupyter notebooks to build up a linear report or storyline for an analysis." + ] + } + ], + "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.12.2" + }, + "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": "303.333px" + }, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/01_exercises_print.ipynb b/01_elements/01_exercises_print.ipynb new file mode 100644 index 0000000..68432ee --- /dev/null +++ b/01_elements/01_exercises_print.ipynb @@ -0,0 +1,172 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb) of Chapter 1.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Printing Output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: *Concatenate* `greeting` and `audience` below with the `+` operator and print out the resulting message `\"Hello World\"` with only *one* call of the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function!\n", + "\n", + "Hint: You may have to \"add\" a space character in between `greeting` and `audience`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "greeting = \"Hello\"\n", + "audience = \"World\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: How is your answer to **Q1** an example of the concept of **operator overloading**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Read the documentation on the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function! How can you print the above message *without* concatenating `greeting` and `audience` first in *one* call of [print() ](https://docs.python.org/3/library/functions.html#print)?\n", + "\n", + "Hint: The `*objects` in the documentation implies that we can put several *expressions* (i.e., variables) separated by commas within the same call of the [print() ](https://docs.python.org/3/library/functions.html#print) function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What does the `sep=\" \"` mean in the documentation on the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function? Adjust and use it to print out the three names referenced by `first`, `second`, and `third` on *one* line separated by *commas* with only *one* call of the [print() ](https://docs.python.org/3/library/functions.html#print) function!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "first = \"Anthony\"\n", + "second = \"Berta\"\n", + "third = \"Christian\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Lastly, what does the `end=\"\\n\"` mean in the documentation? Adjust and use it within the `for`-loop to print the numbers `1` through `10` on *one* line with only *one* call of the [print() ](https://docs.python.org/3/library/functions.html#print) function!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:\n", + " print(...)" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/02_exercises_for-loops.ipynb b/01_elements/02_exercises_for-loops.ipynb new file mode 100644 index 0000000..fab3299 --- /dev/null +++ b/01_elements/02_exercises_for-loops.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb) of Chapter 1.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simple `for`-loops" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`for`-loops are extremely versatile in Python. That is different from many other programming languages.\n", + "\n", + "As shown in the first example in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List), we can create a `list` like `numbers` and loop over the numbers in it on a one-by-one basis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Fill in the *condition* of the `if` statement such that only numbers divisible by `3` are printed! Adjust the call of the [print() ](https://docs.python.org/3/library/functions.html#print) function such that the `for`-loop prints out all the numbers on *one* line of output!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in numbers:\n", + " if ...:\n", + " print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of looping over an *existing* object referenced by a variable like `numbers`, we may also create a *new* object within the `for` statement and loop over it directly. For example, below we write out the `list` object as a *literal*.\n", + "\n", + "Generically, the objects contained in a `list` objects are referred to as its **elements**. We reflect that in the name of the *target* variable `element` that is assigned a different number in every iteration of the `for`-loop. While we could use *any* syntactically valid name, it is best to choose one that makes sense in the context (e.g., `number` in `numbers`).\n", + "\n", + "**Q2**: Fill in the condition of the `if` statement such that only numbers consisting of *one* digit are printed out! As before, print out all the numbers on *one* line of output!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for element in [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]:\n", + " if ...:\n", + " print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An easy way to loop over a `list` object in a sorted manner, is to wrap it with the built-in [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function.\n", + "\n", + "**Q3**: Fill in the condition of the `if` statement such that only odd numbers are printed out! Put all the numbers on *one* line of output!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in sorted(numbers):\n", + " if ...:\n", + " print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Whenever we want to loop over numbers representing a [series ](https://en.wikipedia.org/wiki/Series_%28mathematics%29) in the mathematical sense (i.e., a rule to calculate the next number from its predecessor), we may be able to use the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in.\n", + "\n", + "For example, to loop over the whole numbers from `0` to `9` (both including) in order, we could write them out in a `list` like below.\n", + "\n", + "**Q4**: Fill in the call to the [print() ](https://docs.python.org/3/library/functions.html#print) function such that all the numbers are printed on *one* line ouf output!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]:\n", + " print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Read the documentation on the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in! It may be used with either one, two, or three expressions \"passed\" in. What do `start`, `stop`, and `step` mean? Fill in the calls to [range() ](https://docs.python.org/3/library/functions.html#func-range) and [print() ](https://docs.python.org/3/library/functions.html#print) to mimic the output of **Q4**!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in range(...):\n", + " print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Fill in the calls to [range() ](https://docs.python.org/3/library/functions.html#func-range) and [print() ](https://docs.python.org/3/library/functions.html#print) to print out *all* numbers from `1` to `10` (both including)!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in range(...):\n", + " print(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Fill in the calls to [range() ](https://docs.python.org/3/library/functions.html#func-range) and [print() ](https://docs.python.org/3/library/functions.html#print) to print out the *even* numbers from `1` to `10` (both including)! Do *not* use an `if` statement to accomplish this!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in range(...):\n", + " print(...)" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/03_content.ipynb b/01_elements/03_content.ipynb new file mode 100644 index 0000000..a438ab7 --- /dev/null +++ b/01_elements/03_content.ipynb @@ -0,0 +1,1725 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/03_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 1: Elements of a Program (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this second part of Chapter 1, we look a bit closer into how the memory works and introduce a couple of \"theoretical\" terms." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Variables vs. Names vs. Identifiers vs. References" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Variables** are created with the **[assignment statement ](https://docs.python.org/3/reference/simple_stmts.html#assignment-statements)** `=`, which is *not* an operator because of its *side effect* of making a **[name ](https://docs.python.org/3/reference/lexical_analysis.html#identifiers)** reference an object in memory.\n", + "\n", + "We read the terms **variable**, **name**, and **identifier** used interchangebly in many Python-related texts. In this book, we adopt the following convention: First, we treat *name* and *identifier* as perfect synonyms but only use the term *name* in the text for clarity. Second, whereas *name* only refers to a string of letters, numbers, and some other symbols, a *variable* means the combination of a *name* and a *reference* to an object in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "variable = 20.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When used as a *literal*, a variable evaluates to the value of the object it references. Colloquially, we could say that `variable` evaluates to `20.0`, but this would not be an accurate description of what is going on in memory. We see some more colloquialisms in this section but should always relate this to what Python actually does in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20.0" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A variable may be **re-assigned** as often as we wish. Thereby, we could also assign an object of a *different* type. Because this is allowed, Python is said to be a **dynamically typed** language. On the contrary, a **statically typed** language like C also allows re-assignment but only with objects of the *same* type. This subtle distinction is one reason why Python is slower at execution than C: As it runs a program, it needs to figure out an object's type each time it is referenced." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "variable = 20" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we want to re-assign a variable while referencing its \"old\" (i.e., current) object, we may also **update** it using a so-called **[augmented assignment statement ](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)** (i.e., *not* operator), as introduced with [PEP 203 ](https://www.python.org/dev/peps/pep-0203/): The currently mapped object is implicitly inserted as the first operand on the right-hand side." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "variable *= 4 # same as variable = variable * 4" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "80" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "variable //= 2 # same as variable = variable // 2; \"//\" to retain the integer type" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "40" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "variable += 2 # same as variable = variable + 2" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Variables are **[dereferenced ](https://docs.python.org/3/reference/simple_stmts.html#the-del-statement)** (i.e., \"deleted\") with the `del` statement. This does *not* delete the object a variable references but merely removes the variable's name from the \"global list of all names.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "del variable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we refer to an unknown name, a *runtime* error occurs, namely a `NameError`. The `Name` in `NameError` gives a hint why we choose the term *name* over *identifier* above: Python uses it more often in its error messages." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'variable' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mvariable\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'variable' is not defined" + ] + } + ], + "source": [ + "variable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Some variables magically exist when a Python process is started or are added by Jupyter. We may safely ignore the former until [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) and the latter for good." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'__main__'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "__name__" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To see all defined names, the built-in function [dir() ](https://docs.python.org/3/library/functions.html#dir) is helpful." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['In',\n", + " 'Out',\n", + " 'Path',\n", + " '_',\n", + " '_10',\n", + " '_11',\n", + " '_14',\n", + " '_2',\n", + " '_4',\n", + " '_6',\n", + " '_8',\n", + " '__',\n", + " '___',\n", + " '__builtin__',\n", + " '__builtins__',\n", + " '__doc__',\n", + " '__loader__',\n", + " '__name__',\n", + " '__package__',\n", + " '__session__',\n", + " '__spec__',\n", + " '_dh',\n", + " '_i',\n", + " '_i1',\n", + " '_i10',\n", + " '_i11',\n", + " '_i12',\n", + " '_i13',\n", + " '_i14',\n", + " '_i15',\n", + " '_i2',\n", + " '_i3',\n", + " '_i4',\n", + " '_i5',\n", + " '_i6',\n", + " '_i7',\n", + " '_i8',\n", + " '_i9',\n", + " '_ih',\n", + " '_ii',\n", + " '_iii',\n", + " '_oh',\n", + " 'atexit',\n", + " 'exit',\n", + " 'get_ipython',\n", + " 'history',\n", + " 'history_path',\n", + " 'open',\n", + " 'os',\n", + " 'quit',\n", + " 'readline',\n", + " 'state_home',\n", + " 'write_history']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Naming Conventions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[Phil Karlton](https://skeptics.stackexchange.com/questions/19836/has-phil-karlton-ever-said-there-are-only-two-hard-things-in-computer-science) famously noted during his time at [Netscape ](https://en.wikipedia.org/wiki/Netscape):\n", + "\n", + "> \"There are *two* hard problems in computer science: *naming things* and *cache invalidation* ... and *off-by-one* errors.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Variable names may contain upper and lower case letters, numbers, and underscores (i.e., `_`) and be as long as we want them to be. However, they must not begin with a number. Also, they must not be any of Python's built-in **[keywords ](https://docs.python.org/3/reference/lexical_analysis.html#keywords)** like `for` or `if`.\n", + "\n", + "Variable names should be chosen such that they do not need any more documentation and are self-explanatory. A widespread convention is to use so-called **[snake\\_case ](https://en.wikipedia.org/wiki/Snake_case)**: Keep everything lowercase and use underscores to separate words.\n", + "\n", + "See this [link ](https://en.wikipedia.org/wiki/Naming_convention_%28programming%29#Python_and_Ruby) for a comparison of different naming conventions." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "#### Good examples" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "pi = 3.14" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "answer_to_everything = 42" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "my_name = \"Alexander\"" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "work_address = \"WHU, Burgplatz 2, Vallendar\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "#### Bad examples" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "PI = 3.14 # unless used as a \"global\" constant" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "answerToEverything = 42 # this is a style used in languages like Java" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "name = \"Alexander\" # name of what?" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "cannot assign to expression here. Maybe you meant '==' instead of '='? (202417111.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[23], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m address@work = \"WHU, Burgplatz 2, Vallendar\"\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m cannot assign to expression here. Maybe you meant '==' instead of '='?\n" + ] + } + ], + "source": [ + "address@work = \"WHU, Burgplatz 2, Vallendar\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If a variable name collides with a built-in name, we add a trailing underscore." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "type_ = \"student\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Variables with leading and trailing double underscores, referred to as **dunder** in Python jargon, are used for built-in functionalities and to implement object-oriented features as we see in [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb). We must *not* use this style for variables!" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'__main__'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "__name__" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Who am I? And how many?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is *crucial* to understand that *several* variables may reference the *same* object in memory. Not having this in mind may lead to many hard to track down bugs.\n", + "\n", + "Let's make `b` reference whatever object `a` is referencing." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a = 42" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "b = a" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For \"simple\" types like `int` or `float` this never causes troubles.\n", + "\n", + "Let's \"change the value\" of `a`. To be precise, let's create a *new* `87` object and make `a` reference it." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "a = 87" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "87" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`b` \"is still the same\" as before. To be precise, `b` still references the *same object* as before." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, if a variable references an object of a more \"complex\" type (e.g., `list`), predicting the outcome of a code snippet may be unintuitive for a beginner." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "x = [1, 2, 3]" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "list" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(x)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "y = x" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3]" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's change the first element of `x`.\n", + "\n", + "[Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#The-list-Type) discusses lists in more depth. For now, let's view a `list` object as some sort of **container** that holds an arbitrary number of references to other objects and treat the brackets `[]` attached to it as yet another operator, namely the **indexing operator**. So, `x[0]` instructs Python to first follow the reference from the global list of all names to the `x` object. Then, it follows the first reference it finds there to the `1` object we put in the list. The indexing operator must be an operator as we merely *read* the first element and do not change anything in memory permanently.\n", + "\n", + "Python **begins counting at 0**. This is not the case for many other languages, for example, [MATLAB ](https://en.wikipedia.org/wiki/MATLAB), [R ](https://en.wikipedia.org/wiki/R_%28programming_language%29), or [Stata ](https://en.wikipedia.org/wiki/Stata). To understand why this makes sense, see this short [note](https://www.cs.utexas.edu/users/EWD/transcriptions/EWD08xx/EWD831.html) by one of the all-time greats in computer science, the late [Edsger Dijkstra ](https://en.wikipedia.org/wiki/Edsger_W._Dijkstra)." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To change the first entry in the list, we use the assignment statement `=` again. Here, this does *not* create a *new* variable, nor overwrite an existing one, but only changes the object referenced as the first element in `x`. As we only change parts of the `x` object, we say that we **mutate** its **state**. To use the bag analogy from above, we keep the same bag but \"flip\" some of the $0$s into $1$s and some of the $1$s into $0$s." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "x[0] = 99" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[99, 2, 3]" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The changes made to the object `x` is referencing can also be seen through the `y` variable!" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[99, 2, 3]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The difference in behavior illustrated in this sub-section has to do with the fact that `int` and `float` objects are **immutable** types while `list` objects are **mutable**.\n", + "\n", + "In the first case, an object cannot be changed \"in place\" once it is created in memory. When we assigned `87` to the already existing `a`, we did *not* change the $0$s and $1$s in the object `a` referenced before the assignment but created a new `int` object and made `a` reference it while the `b` variable is *not* affected.\n", + "\n", + "In the second case, `x[0] = 99` creates a *new* `int` object `99` and merely changes the first reference in the `x` list.\n", + "\n", + "In general, the assignment statement creates a new name and makes it reference whatever object is on the right-hand side *iff* the left-hand side is a *pure* name (i.e., it contains no operators like the indexing operator in the example). Otherwise, it *mutates* an already existing object. And, we must always expect that the latter may have more than one variable referencing it.\n", + "\n", + "Visualizing what is going on in memory with a tool like [PythonTutor ](http://pythontutor.com/visualize.html#code=x%20%3D%20%5B1,%202,%203%5D%0Ay%20%3D%20x%0Ax%5B0%5D%20%3D%2099%0Aprint%28y%5B0%5D%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) may be helpful for a beginner." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## How Python reads \"Commands\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As we saw in the previous `list` example, it is important to understand in what order Python executes the \"commands\" (= not an officially used term) we give it. In this last section of the chapter, we introduce a classification scheme and look at its implications." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Expressions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "An **[expression ](https://docs.python.org/3/reference/expressions.html)** is any syntactically correct *combination* of *variables* and *literals* with *operators* that **evaluates** (i.e., \"becomes\") to an object. That object may already exist *before* the expression is parsed or created as a result thereof. The point is that *after* the expression is parsed, Python returns a reference to this object that may be used for further processing.\n", + "\n", + "In simple words, anything that may be used on the *right-hand* side of an assignment statement without creating a `SyntaxError` is an expression.\n", + "\n", + "What we have said about *individual* operators before, namely that they have *no* permanent side effects in memory, actually belongs here, to begin with: The absence of any *permanent* side effects is the characteristic property of expressions, and all the code cells in the \"*(Arithmetic) Operators*\" section in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Arithmetic%29-Operators) of this chapter are examples of expressions.\n", + "\n", + "The simplest possible expressions contain only one variable or literal. The output below a code cell is Jupyter's way of returning the reference to the object to us!\n", + "\n", + "Whereas `a` evaluates to the *existing* `87` object, ..." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "87" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... parsing the literal `42` creates a *new* `int` object and returns a reference to it (Note: for optimization reasons, the CPython implementation may already have a `42` object in memory)." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For sure, we need to include operators to achieve something useful. Here, Python takes the existing `a` object, creates a new `42` object, creates the resulting `45` object, and returns a reference to that." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a - 42" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The definition of an expression is **recursive**. So, the sub-expression `a - 42` is combined with the literal `9` by the operator `//` to form the full expression `(a - 42) // 9`." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(a - 42) // 9" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Below, the variable `x` is combined with the literal `2` by the indexing operator `[]`. The resulting expression evaluates to the third element in the `x` list." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x[2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When not used as a delimiter, parentheses also constitute an operator, namely the **call operator** `()`. We saw this syntax before when we called built-in functions and methods." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "104" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(x)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Statements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A **[statement ](https://docs.python.org/3/reference/simple_stmts.html)** is any *single* command Python understands.\n", + "\n", + "So, what we mean with statement is a [strict superset ](https://en.wikipedia.org/wiki/Subset) of what we mean with expression: All expressions are statements, but some statements are *not* expressions. That implies that all code cells in the previous \"*Expressions*\" section are also examples of statements.\n", + "\n", + "Unfortunately, many texts on Python use the two terms as mutually exclusive concepts by saying that a statement (always) *changes* the *state of a program* with a permanent *side effect* in memory (e.g., by creating a variable) whereas an expression does not. That is not an accurate way of distinguishing the two in all cases!\n", + "\n", + "So, often, the term statement is used in the meaning of \"a statement that is *not* an expression.\" We can identify such statements easily in Jupyter as the correspoding code cells have *no* output below them.\n", + "\n", + "While many statements, for example `=` and `del`, indeed have *permanent* side effects, ..." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a = 21 * 2" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "del a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... calling the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function does *neither* change the memory *nor* evaluate to an object (disregarding the `None` object explained in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Function-Definitions)). We could view changing the computer's screen as a side effect but this is outside of Python's memory!\n", + "\n", + "Also, the cell below has *no* output! It only looks like it does as Jupyter redirects whatever [print() ](https://docs.python.org/3/library/functions.html#print) writes to the \"screen\" to below a cell. We see a difference to the expressions above in that there are no brackets `[...]` next to the output showing the execution count number." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Don't I change the state of the computer's display?\n" + ] + } + ], + "source": [ + "print(\"Don't I change the state of the computer's display?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Logical vs. Physical Lines" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "How many lines of code does the next cell constitute?" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The answer is: 42\n" + ] + } + ], + "source": [ + "result = 21 * 2; print(\"The answer is:\", result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The answer depends on how we are counting. If we count the number of lines of written source code, the answer is *one*. This gives us the number of **physical lines**. On the contrary, if we count the number of statements separated by the `;`, the answer is *two*. This gives us the number of **logical lines**.\n", + "\n", + "While physical lines are what we, the humans, see, logical lines are how computers read. To align the two ways, it is a best practice to not write more than one logical lines on a single physical one. So, from Python's point of view, the cells above and below are the same. More importantly, the one above is *not* somehow \"magically\" faster." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The answer is: 42\n" + ] + } + ], + "source": [ + "result = 21 * 2\n", + "print(\"The answer is:\", result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is also possible to write a single logical line over several physical ones. The cell below is yet another physical representation of the same *two* logical lines. Any pair of delimiters, like `(` and `)` below, can be used to \"format\" the code in between with whitespace. The style guides mentioned before should still be taken into account (i.e., indent with 4 spaces)." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The answer is: 42\n" + ] + } + ], + "source": [ + "result = 21 * 2\n", + "print(\n", + " \"The answer is:\",\n", + " result\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another situation, in which several physical lines are treated as a logical one, is with so-called **[compound statements ](https://docs.python.org/3/reference/compound_stmts.html)**. In contrast to simple statements like `=` and `del` above, the purpose of compound statements is to group other statements.\n", + "\n", + "We have seen two examples of compound statements already: `for` and `if`. They both consist of a **header** line ending with a `:` and an indented **(code) block** spanning an arbitrary number of logical lines.\n", + "\n", + "In the example, the first logical line spans the entire cell, the second logical line contains all physical lines except the first, and the third and fourth logical lines are just the third and fourth physical lines, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 -> 4\n" + ] + } + ], + "source": [ + "for number in [1, 2, 3]:\n", + " if number % 2 == 0:\n", + " double = 2 * number \n", + " print(number, \"->\", double)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Comments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A comment is a physical line that is *not* a logical line.\n", + "\n", + "We use the `#` symbol to write comments in plain text right into the code. Anything after the `#` until the end of the physical line is ignored by Python.\n", + "\n", + "As a good practice, comments should *not* describe *what* happens. This should be evident by reading the code. Otherwise, it is most likely badly written code. Rather, comments should describe *why* something happens.\n", + "\n", + "Comments may be added either at the end of a line of code, by convention separated with two spaces, or on a line on their own." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "distance = 891 # in meters\n", + "elapsed_time = 93 # in seconds\n", + "\n", + "# Calculate the speed in km/h.\n", + "speed = 3.6 * distance / elapsed_time" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "But let's think wisely if we need to use a comment.\n", + "The second cell is a lot more Pythonic." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "seconds = 365 * 24 * 60 * 60 # = seconds in the year" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "seconds_per_year = 365 * 24 * 60 * 60" + ] + } + ], + "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.12.2" + }, + "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": "303.333px" + }, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/04_exercises_calculator.ipynb b/01_elements/04_exercises_calculator.ipynb new file mode 100644 index 0000000..5b66384 --- /dev/null +++ b/01_elements/04_exercises_calculator.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/04_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb) Chapter 1.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Python as a Calculator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [volume of a sphere ](https://en.wikipedia.org/wiki/Sphere) is defined as $\\frac{4}{3} * \\pi * r^3$.\n", + "\n", + "**Q1**: Calculate it for `r = 2.88` and approximate $\\pi$ with `pi = 3.14`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pi = 3.14\n", + "r = 2.88" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "While Python may be used as a calculator, it behaves a bit differently compared to calculator apps that phones or computers come with and that we are accustomed to.\n", + "\n", + "A major difference is that Python \"forgets\" intermediate results that are not assigned to variables. On the contrary, the calculators we work with outside of programming always keep the last result and allow us to use it as the first input for the next calculation.\n", + "\n", + "One way to keep on working with intermediate results in Python is to write the entire calculation as just *one* big expression that is composed of many sub-expressions representing the individual steps in our overall calculation.\n", + "\n", + "**Q2.1**: Given `a` and `b` like below, subtract the smaller `a` from the larger `b`, divide the difference by `9`, and raise the result to the power of `2`! Use operators that preserve the `int` type of the final result! The entire calculations *must* be placed within *one* code cell.\n", + "\n", + "Hint: You may need to group sub-expressions with parentheses `(` and `)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = 42\n", + "b = 87" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code cell below contains nothing but a single underscore `_`. In both, a Python command-line prompt and Jupyter notebooks, the variable `_` is automatically updated and always references the object to which the *last* expression executed evaluated to.\n", + "\n", + "**Q2.2**: Execute the code cell below! It should evaluate to the *same* result as the previous code cell (i.e., your answer to **Q2.1** assuming you go through this notebook in order)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2.3**: Implement the same overall calculation as in your answer to **Q2.1** in several independent steps (i.e., code cells)! Use only *one* operator per code cell!\n", + "\n", + "Hint: You should need *two* more code cells after the `b - a` one immediately below. If you *need* to use parentheses, you must be doing something wrong." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "b - a" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_ ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_ ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.1**: When answering the questions above, you should have used only **expressions** in the code cells. What are expressions syntactically?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.2**: The code cells that provide the numbers to work with contain **statements** that are *not* expressions. What are statements? How are they different from expressions?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/05_summary.ipynb b/01_elements/05_summary.ipynb new file mode 100644 index 0000000..e5a7dd0 --- /dev/null +++ b/01_elements/05_summary.ipynb @@ -0,0 +1,142 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 1: Elements of a Program (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We end each chapter with a summary of the main points (i.e., **TL;DR** = \"too long; didn't read\"). The essence in this first chapter is that just as a sentence in a real language like English may be decomposed into its parts (e.g., subject, predicate, and objects), the same may be done with programming languages.\n", + "\n", + "- program\n", + " - **sequence** of **instructions** that specify how to perform a computation (= a \"recipe\")\n", + " - a \"black box\" that processes **inputs** and transforms them into meaningful **outputs** in a *deterministic* way\n", + " - conceptually similar to a mathematical function $f$ that maps some input $x$ to an output $y = f(x)$\n", + "\n", + "\n", + "- input (examples)\n", + " - data from a CSV file\n", + " - text entered on a command line\n", + " - data obtained from a database\n", + " - etc.\n", + "\n", + "\n", + "- output (examples)\n", + " - result of a computation (e.g., statistical summary of a sample dataset)\n", + " - a \"side effect\" (e.g., a transformation of raw input data into cleaned data)\n", + " - a physical \"behavior\" (e.g., a robot moving or a document printed)\n", + " - etc.\n", + "\n", + "\n", + "- objects\n", + " - distinct and well-contained areas/parts of the memory that hold the actual data\n", + " - the concept by which Python manages the memory for us\n", + " - can be classified into objects of the same **type** (i.e., same abstract \"structure\" but different concrete data)\n", + " - built-in objects (incl. **literals**) vs. user-defined objects (cf., [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb))\n", + " - e.g., `1`, `1.0`, and `\"one\"` are three different objects of distinct types that are also literals (i.e., by the way we type them into the command line Python knows what the value and type are)\n", + "\n", + "\n", + "- variables\n", + " - storage of intermediate **state**\n", + " - are **names** referencing **objects** in **memory**\n", + " - e.g., `x = 1` creates the variable `x` that references the object `1`\n", + "\n", + "\n", + "- operators\n", + " - special built-in symbols that perform operations with objects in memory\n", + " - usually, operate with one or two objects\n", + " - e.g., addition `+`, subtraction `-`, multiplication `*`, and division `/` all take two objects, whereas the negation `-` only takes one\n", + "\n", + "\n", + "- expressions\n", + " - **combinations** of **variables** (incl. **literals**) and **operators**\n", + " - do *not* change the involved objects/state of the program\n", + " - evaluate to a **value** (i.e., the \"result\" of the expression, usually a new object)\n", + " - e.g., `x + 2` evaluates to the (new) object `3` and `1 - 1.0` to `0.0`\n", + "\n", + "\n", + "- statements\n", + " - instructions that **\"do\" something** and **have side effects** in memory\n", + " - (re-)assign names to (different) objects, *change* an existing object *in place*, or, more conceptually, *change* the state of the program\n", + " - usually, work with expressions\n", + " - e.g., the assignment statement `=` makes a name reference an object\n", + "\n", + "\n", + "- comments\n", + " - **prose** supporting a **human's understanding** of the program\n", + " - ignored by Python\n", + "\n", + "\n", + "- functions (cf., [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb))\n", + " - named sequences of instructions\n", + " - the smaller parts in a larger program\n", + " - make a program more modular and thus easier to understand\n", + " - include [built-in functions ](https://docs.python.org/3/library/functions.html) like [print() ](https://docs.python.org/3/library/functions.html#print), [sum() ](https://docs.python.org/3/library/functions.html#sum), or [len() ](https://docs.python.org/3/library/functions.html#len)\n", + "\n", + "\n", + "- flow control (cf., [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb) and [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb))\n", + " - expression of **business logic** or an **algorithm**\n", + " - conditional execution of parts of a program (e.g., `if` statements)\n", + " - repetitive execution of parts of a program (e.g., `for`-loops)" + ] + } + ], + "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.12.2" + }, + "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": "303.333px" + }, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/06_review.ipynb b/01_elements/06_review.ipynb new file mode 100644 index 0000000..60c9360 --- /dev/null +++ b/01_elements/06_review.ipynb @@ -0,0 +1,215 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 1: Elements of a Program (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb) and the [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb) part of Chapter 1.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Elaborate on how **modulo division** might be a handy operation to know!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What is a **dynamically typed** language? How does it differ from a **statically typed** language? What does that mean for Python?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Why is it useful to start counting at $0$?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What is **operator overloading**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What are the basic **naming conventions** for variables? What happens if a name collides with one of Python's [keywords ](https://docs.python.org/3/reference/lexical_analysis.html#keywords)?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Advocates of the [functional programming ](https://en.wikipedia.org/wiki/Functional_programming) paradigm suggest not to use **mutable** data types in a program. What are the advantages of that approach? What might be a downside?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: \"**dunder**\" refers to a group of Australian (i.e., \"down under\") geeks that work on core Python." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: The **Zen of Python** talks about Indian genius programmers." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: When NASA famously converted some measurements to the wrong unit and lost a Mars satellite in 1999 (cf., [source](https://www.wired.com/2010/11/1110mars-climate-observer-report/)), this is an example of a so-called **runtime error**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: [PEP 8 ](https://www.python.org/dev/peps/pep-0008/) suggests that developers use **8 spaces** per level of indentation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01_elements/07_resources.ipynb b/01_elements/07_resources.ipynb new file mode 100644 index 0000000..0bf7edb --- /dev/null +++ b/01_elements/07_resources.ipynb @@ -0,0 +1,115 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/07_resources.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 1: Elements of a Program (Further Resources)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This PyCon 2015 talk by [Ned Batchelder](https://nedbatchelder.com/), a well-known Pythonista and the organizer of the [Python User Group](https://www.meetup.com/bostonpython/) in Boston, summarizes all situations where some sort of assignment is done in Python and, thus, reviews how *variables are just names referencing objects*. The content is intermediate, and, thus, it might be worthwhile to come back to this talk at a later point in time. However, the contents should be known by everyone claiming to be proficient in Python." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"_AEJHKGk9ns\", width=\"60%\")" + ] + } + ], + "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.12.2" + }, + "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": "303.333px" + }, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/02_functions/00_content.ipynb b/02_functions/00_content.ipynb new file mode 100644 index 0000000..a40b1e6 --- /dev/null +++ b/02_functions/00_content.ipynb @@ -0,0 +1,3064 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/02_functions/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 2: Functions & Modularization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List), we simply typed the code to calculate the average of the even numbers in a list of whole numbers into several code cells. Then, we executed them one after another. We had no way of *reusing* the code except for either executing cells multiple times. And, whenever we find ourselves doing repetitive manual work, we can be sure that there must be a way of automating what we are doing.\n", + "\n", + "This chapter shows how Python offers language constructs that let us **define** functions ourselves that we may then **call** just like the built-in ones. Also, we look at how we can extend our Python installation with functionalities written by other people." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## [Built-in Functions ](https://docs.python.org/3/library/functions.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python comes with plenty of useful functions built in, some of which we have already seen before (e.g., [print() ](https://docs.python.org/3/library/functions.html#print), [sum() ](https://docs.python.org/3/library/functions.html#sum), [len() ](https://docs.python.org/3/library/functions.html#len), or [id() ](https://docs.python.org/3/library/functions.html#id)). The [documentation ](https://docs.python.org/3/library/functions.html) has the full list. Just as core Python itself, they are mostly implemented in C and thus very fast.\n", + "\n", + "Below, [sum() ](https://docs.python.org/3/library/functions.html#sum) adds up all the elements in the `numbers` list while [len() ](https://docs.python.org/3/library/functions.html#len) counts the number of elements in it." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "78" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`sum` and `len` are *no* [keywords ](https://docs.python.org/3/reference/lexical_analysis.html#keywords) like `for` or `if` but variables that reference *objects* in memory. Often, we hear people say that \"everything is an object in Python\" (e.g., this [question ](https://stackoverflow.com/questions/40478536/in-python-what-does-it-mean-by-everything-is-an-object)). While this phrase may sound abstract in the beginning, it simply means that the entire memory is organized with \"bags\" of $0$s and $1$s, and there are even bags for the built-in functions. That is *not* true for many other languages (e.g., C or Java) and often a source of confusion for people coming to Python from another language.\n", + "\n", + "The built-in [id() ](https://docs.python.org/3/library/functions.html#id) function tells us where in memory a particular built-in function is stored." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139940703477088" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(sum)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139940703476048" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(len)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[type() ](https://docs.python.org/3/library/functions.html#type) reveals that built-in functions like [sum() ](https://docs.python.org/3/library/functions.html#sum) or [len() ](https://docs.python.org/3/library/functions.html#len) are objects of type `builtin_function_or_method`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "builtin_function_or_method" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(sum)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "builtin_function_or_method" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(len)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python's object-oriented nature allows us to have functions work with themselves. While seemingly not useful from a beginner's point of view, that enables a lot of powerful programming styles later on." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139940703475648" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(id)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "type" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(type)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To execute a function, we **call** it with the **call operator** `()` as shown many times in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb) and above.\n", + "\n", + "If we are unsure whether a variable references a function or not, we can verify that with the built-in [callable() ](https://docs.python.org/3/library/functions.html#callable) function.\n", + "\n", + "Abstractly speaking, *any* object that can be called with the call operator `()` is a so-called **callable**. And, objects of type `builtin_function_or_method` are just one kind of examples thereof. We will see another one already in the next sub-section." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(sum)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(len)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`list` objects, for example, are *not* callable." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Constructors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The list of [built-in functions ](https://docs.python.org/3/library/functions.html) in the documentation should really be named a list of built-in *callables*.\n", + "\n", + "Besides the built-in functions, the list also features **constructors** for the built-in types. They may be used to **[cast ](https://en.wikipedia.org/wiki/Type_conversion)** (i.e., \"convert\") any object as an object of a given type.\n", + "\n", + "For example, to \"convert\" a `float` or a `str` into an `int` object, we use the [int() ](https://docs.python.org/3/library/functions.html#int) built-in. Below, *new* `int` objects are created from the `7.0` and `\"7\"` objects that are *newly* created themselves before being processed by [int() ](https://docs.python.org/3/library/functions.html#int) right away *without* ever being referenced by a variable." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(7.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(\"7\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Casting an object as an `int` is different from rounding with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function!" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(7.99)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "round(7.99)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Notice the subtle difference compared to the behavior of the `//` operator in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb##%28Arithmetic#%29-Operators) that \"rounds\" towards minus infinity: [int() ](https://docs.python.org/3/library/functions.html#int) always \"rounds\" towards `0`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-7" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(-7.99)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Not all conversions are valid and *runtime* errors may occur as the `ValueError` shows." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "invalid literal for int() with base 10: 'seven'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[18], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mseven\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mValueError\u001b[0m: invalid literal for int() with base 10: 'seven'" + ] + } + ], + "source": [ + "int(\"seven\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may also cast in the other direction with the [float() ](https://docs.python.org/3/library/functions.html#float) or [str() ](https://docs.python.org/3/library/functions.html#func-str) built-ins." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(7)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'7'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(7)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Constructors are full-fledged objects as well." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94623097764960" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94623097758720" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(float)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "They are of type `type`, which is different from `builtin_function_or_method` above." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "type" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "type" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(float)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As already noted, constructors are *callables*. In that regard, they behave the same as built-in functions. We may call them with the call operator `()`." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(int)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(float)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The attentive student may already have discovered that we refer to `builtin_function_or_method` objects as \"built-in functions\" and `type` objects as just \"built-ins.\" For a beginner, that difference is not so important. But, the ambitious student should already be aware that such subtleties exist.\n", + "\n", + "Next, let's look at a third kind of callables." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Function Definitions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may create so-called *user-defined* **functions** with the `def` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#function-definitions)). To extend an already familiar example, we reuse the introductory example from [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Best-Practices) in its final Pythonic version and transform it into the function `average_evens()` below. We replace the variable name `numbers` with `integers` for didactical purposes in the first couple of examples.\n", + "\n", + "A function's **name** must be chosen according to the same naming rules as ordinary variables since Python manages function names like variables. In this book, we further adopt the convention of ending function names with parentheses `()` in text cells for faster comprehension when reading (i.e., `average_evens()` vs. `average_evens`). These are *not* part of the name but must always be written out in the `def` statement for syntactic reasons.\n", + "\n", + "Functions may define an arbitrary number of **parameters** as inputs that can then be referenced within the indented **code block**: They are listed within the parentheses in the `def` statement (i.e., `integers` below). \n", + "\n", + "The code block is also called a function's **body**, while the first line starting with `def` and ending with a colon is the **header**.\n", + "\n", + "Together, the name and the list of parameters are also referred to as the function's **[signature ](https://en.wikipedia.org/wiki/Type_signature)** (i.e., `average_evens(integers)` below).\n", + "\n", + "A function may specify an *explicit* **return value** (i.e., \"result\" or \"output\") with the `return` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-return-statement)): Functions that have one are considered **fruitful**; otherwise, they are **void**. Functions of the latter kind are still useful because of their **side effects**. For example, the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function changes what we see on the screen. Strictly speaking, [print() ](https://docs.python.org/3/library/functions.html#print) and other void functions also have an *implicit* return value, namely the `None` object.\n", + "\n", + "A function should define a **docstring** that describes what it does in a short subject line, what parameters it expects (i.e., their types), and what it returns, if anything. A docstring is a syntactically valid multi-line string (i.e., type `str`) defined within **triple-double quotes** `\"\"\"`. Strings are covered in depth in [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb#The-str-Type). Widely adopted standards for docstrings are [PEP 257 ](https://www.python.org/dev/peps/pep-0257/) and section 3.8 of [Google's Python Style Guide ](https://github.com/google/styleguide/blob/gh-pages/pyguide.md)." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(integers):\n", + " \"\"\"Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " integers (list of int's): whole numbers to be averaged\n", + "\n", + " Returns:\n", + " average (float)\n", + " \"\"\"\n", + " evens = [n for n in integers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Once defined, a function may be referenced just like any other variable by its name (i.e., *without* the parentheses)." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This works as functions are full-fledged *objects*. So, `average_evens` is just a name referencing an object in memory with an **identity**, a **type**, namely `function`, and a **value**. In that regard, `average_evens` is *no* different from the variable `numbers` or the built-ins' names." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139940571731424" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(average_evens)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "function" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(average_evens)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Its value may seem awkward at first: It consists of a location showing where the function is defined (i.e., `__main__` here, which is Python's way of saying \"in this notebook\") and the signature wrapped inside angle brackets `<` and `>`.\n", + " \n", + "The angle brackets are a convention to indicate that the value may *not* be used as a *literal* (i.e., typed back into another code cell). [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) introduces the concept of a **text representation** of an object, which is related to the *semantic* meaning of an object's value as discussed in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Value-/-\"Meaning\"), and the angle brackets convention is one such way to represent an object as text. When executed, the angle brackets cause a `SyntaxError` because Python expects the `<` operator to come with an operand on both sides (cf., [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#Relational-Operators))." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (2246690741.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[31], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m \u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`average_evens` is, of course, callable. So, the `function` type is the third kind of callable in this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(average_evens)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The built-in [help() ](https://docs.python.org/3/library/functions.html#help) function shows a function's docstring.\n", + "\n", + "Whenever we use code to analyze or obtain information on an object, we say that we **[introspect ](https://en.wikipedia.org/wiki/Type_introspection)** it." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function average_evens in module __main__:\n", + "\n", + "average_evens(integers)\n", + " Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " integers (list of int's): whole numbers to be averaged\n", + "\n", + " Returns:\n", + " average (float)\n", + "\n" + ] + } + ], + "source": [ + "help(average_evens)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In JupyterLab, we can just as well add a question mark `?` to a function's name to achieve the same." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mSignature:\u001b[0m \u001b[0maverage_evens\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mintegers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m\n", + "Calculate the average of all even numbers in a list.\n", + "\n", + "Args:\n", + " integers (list of int's): whole numbers to be averaged\n", + "\n", + "Returns:\n", + " average (float)\n", + "\u001b[0;31mFile:\u001b[0m /tmp/ipykernel_152540/3598721284.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "average_evens?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Two question marks `??` show a function's source code." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mSignature:\u001b[0m \u001b[0maverage_evens\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mintegers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mSource:\u001b[0m \n", + "\u001b[0;32mdef\u001b[0m \u001b[0maverage_evens\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mintegers\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Calculate the average of all even numbers in a list.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Args:\u001b[0m\n", + "\u001b[0;34m integers (list of int's): whole numbers to be averaged\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Returns:\u001b[0m\n", + "\u001b[0;34m average (float)\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mevens\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mn\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mn\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mintegers\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mn\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0;36m2\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0maverage\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mevens\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mevens\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0maverage\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mFile:\u001b[0m /tmp/ipykernel_152540/3598721284.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "average_evens??" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[help() ](https://docs.python.org/3/library/functions.html#help) and the `?`s also work for built-ins." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function sum in module builtins:\n", + "\n", + "sum(iterable, /, start=0)\n", + " Return the sum of a 'start' value (default: 0) plus an iterable of numbers\n", + "\n", + " When the iterable is empty, return the start value.\n", + " This function is intended specifically for use with numeric values and may\n", + " reject non-numeric types.\n", + "\n" + ] + } + ], + "source": [ + "help(sum)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Function Calls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Once defined, we may call a function with the call operator `()` as often as we wish. The formal parameters are then filled in by **passing** *expressions* (e.g., literals or variables) as **arguments** to the function within the parentheses." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens([7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4])" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A function call's return value is commonly assigned to a variable for subsequent use. Otherwise, we lose access to the returned object right away." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "result = average_evens(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Scoping Rules" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Local Scope disappears" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The parameters listed in a function's definition (i.e., `integers` in the example) and variables created *inside* it during execution (i.e., `evens` and `average`) are **local** to that function. That means they only reference an object in memory *while* the function is being executed and are dereferenced immediately when the function call returns. We say they go out of **scope**. That is why we see the `NameError`s below." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'integers' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[41], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mintegers\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'integers' is not defined" + ] + } + ], + "source": [ + "integers" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'evens' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[42], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mevens\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'evens' is not defined" + ] + } + ], + "source": [ + "evens" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'average' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[43], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43maverage\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'average' is not defined" + ] + } + ], + "source": [ + "average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_evens%28integers%29%3A%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20integers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_evens%28numbers%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) visualizes what happens in memory: To be precise, in the exact moment when the function call is initiated and `numbers` passed in as the `integers` argument, there are *two* references to the *same* `list` object (cf., steps 4-5 in the visualization). We also see how Python creates a *new* **frame** that holds the function's local scope (i.e., \"internal names\") in addition to the **global** frame. Frames are nothing but [namespaces ](https://en.wikipedia.org/wiki/Namespace) to *isolate* the names of different **scopes** from each other. The list comprehension `[n for n in integers if n % 2 == 0]` constitutes yet another frame that is in scope as the `list` object assigned to `evens` is *being* created (cf., steps 6-20). When the function returns, only the global frame is left (cf., last step)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Global Scope is everywhere" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, while a function is *being* executed, it may reference the variables of **enclosing scopes** (i.e., \"outside\" of it). This is a common source of *semantic* errors. Consider the following stylized and incorrect example `average_wrong()`. The error is hard to spot with eyes: The function never references the `integers` parameter but the `numbers` variable in the **global scope** instead." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_wrong(integers):\n", + " \"\"\"Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " integers (list of int's): whole numbers to be averaged\n", + "\n", + " Returns:\n", + " average (float)\n", + " \"\"\"\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`numbers` in the global scope is, of course, *not* changed by merely defining `average_wrong()`." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sometimes a function may return a correct solution for *some* inputs ..." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_wrong(numbers) # correct by accident" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... but still be wrong *in general*." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_wrong([123, 456, 789])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_wrong%28integers%29%3A%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_wrong%28%5B123,%20456,%20789%5D%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) is again helpful at visualizing the error interactively: Creating the `list` object `evens` eventually references takes *16* computational steps, namely two for managing the list comprehension, one for setting up an empty `list` object, *twelve* for filling it with elements derived from `numbers` in the global scope (i.e., that is the error), and one to make `evens` reference it (cf., steps 6-21).\n", + "\n", + "The frames logic shown by PythonTutor is the mechanism with which Python not only manages the names inside *one* function call but also for *many* potentially *simultaneous* calls, as revealed in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb#Trivial-Example:-Countdown). It is the reason why we may reuse the same names for the parameters and variables inside both `average_evens()` and `average_wrong()` without Python mixing them up. So, as we already read in the [Zen of Python ](https://www.python.org/dev/peps/pep-0020/), \"namespaces are one honking great idea\" (cf., `import this`), and a frame is just a special kind of namespace." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Shadowing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Code gets even more confusing when variables by the *same* name from *different* scopes collide. In particular, what should we expect to happen if a function \"changes\" a globally defined variable in its body?\n", + "\n", + "`average_evens()` below works like `average_evens()` above except that it rounds the numbers in `integers` with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function before filtering and averaging them. [round() ](https://docs.python.org/3/library/functions.html#round) returns `int` objects independent of its argument being an `int` or a `float` object. On the first line in its body, `average_evens()` introduces a *local* variable `numbers` whose name collides with the one defined in the global scope." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(integers):\n", + " \"\"\"Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " integers (list of int's/float's): numbers to be averaged;\n", + " if non-whole numbers are provided, they are rounded\n", + "\n", + " Returns:\n", + " average (float)\n", + " \"\"\"\n", + " numbers = [round(n) for n in integers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As a good practice, let's first \"verify\" that `average_evens()` is \"correct\" by calling it with inputs for which we can calculate the answer in our heads. Treating a function as a \"black box\" (i.e., input-output specification) when testing is also called [unit testing ](https://en.wikipedia.org/wiki/Unit_testing) and plays an important role in modern software engineering." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens([40.0, 41.1, 42.2, 43.3, 44.4])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Such tests are often and conveniently expressed with the `assert` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)): If the expression following `assert` evaluates to `True`, nothing happens." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 42.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, if the expression evaluates to `False`, an `AssertionError` is raised." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "AssertionError", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[51], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m average_evens([\u001b[38;5;241m40.0\u001b[39m, \u001b[38;5;241m41.1\u001b[39m, \u001b[38;5;241m42.2\u001b[39m, \u001b[38;5;241m43.3\u001b[39m, \u001b[38;5;241m44.4\u001b[39m]) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m87.0\u001b[39m\n", + "\u001b[0;31mAssertionError\u001b[0m: " + ] + } + ], + "source": [ + "assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 87.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Calling `average_evens()` leaves `numbers` in the global scope unchanged." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To add to the confusion, let's also pass the global `numbers` list as an argument to `average_evens()`. The return value is the same as before." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In summary, Python is smart enough to keep all the involved `numbers` variables apart. So, the global `numbers` variable is still referencing the *same* `list` object as before." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The reason why everything works is that *every* time we (re-)assign an object to a variable *inside* a function's body with the `=` statement, this is done in the *local* scope by default. There are ways to change variables existing in an outer scope from within a function, but this is a rather advanced topic.\n", + "\n", + "[PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_evens%28integers%29%3A%0A%20%20%20%20numbers%20%3D%20%5Bround%28n%29%20for%20n%20in%20integers%5D%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_evens%28%5B40.0,%2041.1,%2042.2,%2043.3,%2044.4%5D%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how *two* `numbers` variables exist in *different* scopes referencing *different* objects (cf., steps 14-25) when we execute `average_evens([40.0, 41.1, 42.2, 43.3, 44.4])`.\n", + "\n", + "Variables whose names collide with the ones of variables in enclosing scopes - and the global scope is just the most enclosing scope - are said to **shadow** them.\n", + "\n", + "While this is not a problem for Python, it may lead to less readable code for humans and should be avoided if possible. But, as the software engineering wisdom goes, \"[naming things](https://skeptics.stackexchange.com/questions/19836/has-phil-karlton-ever-said-there-are-only-two-hard-things-in-computer-science)\" is often considered a hard problem as well, and we have to be prepared to encounter shadowing variables.\n", + "\n", + "Shadowing also occurs if a parameter in the function definition goes by the same name as a variable in an outer scope. Below, `average_evens()` is identical to the first version in this chapter except that the parameter `integers` is now called `numbers` as well." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(numbers):\n", + " \"\"\"Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " numbers (list of int's): whole numbers to be averaged\n", + "\n", + " Returns:\n", + " average (float)\n", + " \"\"\"\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return average" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_evens%28numbers%29%3A%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_evens%28numbers%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) reveals that in this example there are *two* `numbers` variables in *different* scope referencing the *same* `list` object in memory (cf., steps 4-23)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Positional vs. Keyword Arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, we have specified only one parameter in each of our user-defined functions. In [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Arithmetic%29-Operators), however, we saw the built-in [divmod() ](https://docs.python.org/3/library/functions.html#divmod) function take two arguments. And, the order in which they are passed in matters! Whenever we call a function and list its arguments in a comma separated manner, we say that we pass in the arguments *by position* or refer to them as **positional arguments**." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(4, 2)" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "divmod(42, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0, 10)" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "divmod(10, 42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For many functions, there is a natural order to the arguments: For example, for any kind of division passing the dividend first and the divisor second seems intuitive. But what if that is not the case in another setting? For example, let's create a close relative of the above `average_evens()` function that also scales the resulting average by a factor. What is more natural? Passing in `numbers` first? Or `scalar`? There is no obvious way and we continue with the first alternative for no concrete reason." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def scaled_average_evens(numbers, scalar):\n", + " \"\"\"Calculate the scaled average of all even numbers in a list.\n", + "\n", + " Args:\n", + " numbers (list of int's/float's): numbers to be averaged;\n", + " if non-whole numbers are provided, they are rounded\n", + " scalar (float): multiplies the average\n", + "\n", + " Returns:\n", + " scaled_average (float)\n", + " \"\"\"\n", + " numbers = [round(n) for n in numbers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return scalar * average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with [divmod() ](https://docs.python.org/3/library/functions.html#divmod), we may pass in the arguments by position." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scaled_average_evens(numbers, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, this function call is a bit harder to understand as we always need to remember what the `2` means. This becomes even harder with more parameters.\n", + "\n", + "Luckily, we may also pass in arguments *by name*. Then, we refer to them as **keyword arguments**." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scaled_average_evens(numbers=numbers, scalar=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When passing all arguments by name, we may do so in any order." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scaled_average_evens(scalar=2, numbers=numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may even combine positional and keyword arguments in the same function call." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scaled_average_evens(numbers, scalar=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Unfortunately, there are ways to screw this up with a `SyntaxError`: If positional and keyword arguments are mixed, the keyword arguments *must* come last." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "positional argument follows keyword argument (159253642.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[65], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m scaled_average_evens(numbers=numbers, 2)\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m positional argument follows keyword argument\n" + ] + } + ], + "source": [ + "scaled_average_evens(numbers=numbers, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, we must always pass in the right number of arguments. Otherwise, a `TypeError` is raised." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "scaled_average_evens() missing 1 required positional argument: 'scalar'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[66], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mscaled_average_evens\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnumbers\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: scaled_average_evens() missing 1 required positional argument: 'scalar'" + ] + } + ], + "source": [ + "scaled_average_evens(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "scaled_average_evens() takes 2 positional arguments but 3 were given", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[67], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mscaled_average_evens\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnumbers\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: scaled_average_evens() takes 2 positional arguments but 3 were given" + ] + } + ], + "source": [ + "scaled_average_evens(numbers, 2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Modularization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Defining `average_evens()` and `scaled_average_evens()` as above leads to a repetition of most of their code. That is *not* good as such a redundancy makes a code base hard to maintain in the long run: Whenever we change the logic in one function, we must *not* forget to do so for the other function as well. And, most likely, we forget about such issues in larger projects.\n", + "\n", + "Below, three of four lines in the functions' bodies are identical!" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(numbers):\n", + " \"\"\" ... ... ... \"\"\"\n", + " numbers = [round(n) for n in numbers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return average" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [], + "source": [ + "def scaled_average_evens(numbers, scalar):\n", + " \"\"\" ... ... ... \"\"\"\n", + " numbers = [round(n) for n in numbers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return scalar * average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A better way is to design related functions in a **modular** fashion such that they reuse each other's code.\n", + "\n", + "For example, as not scaling an average is just a special case of scaling it with `1`, we could redefine the two functions like below: In this version, the function resembling the *special* case, `average_evens()`, **forwards** the call to the more *general* function, `scaled_average_evens()`, passing a `scalar` argument of `1`. As the name `scaled_average_evens` within the body of `average_evens()` is looked up each time the function is *being* executed, we may define `average_evens()` before `scaled_average_evens()`." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(numbers):\n", + " \"\"\" ... ... ... \"\"\"\n", + " return scaled_average_evens(numbers, scalar=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [], + "source": [ + "def scaled_average_evens(numbers, scalar):\n", + " \"\"\" ... ... ... \"\"\"\n", + " numbers = [round(n) for n in numbers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return scalar * average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After **refactoring** the functions, it is a good idea to test them again." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "assert average_evens(numbers) == 7.0" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "assert scaled_average_evens(numbers, 2) == 14.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Default Arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "*Assuming* that scaling the average occurs rarely, it may be a good idea to handle both cases in *one* function definition by providing a **default argument** of `1` for the `scalar` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(numbers, scalar=1):\n", + " \"\"\"Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " numbers (list of int's/float's): numbers to be averaged;\n", + " if non-whole numbers are provided, they are rounded\n", + " scalar (float, optional): multiplies the average; defaults to 1\n", + "\n", + " Returns:\n", + " scaled_average (float)\n", + " \"\"\"\n", + " numbers = [round(n) for n in numbers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return scalar * average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, we call the function with or without passing a `scalar` argument.\n", + "\n", + "If `scalar` is *not* passed in, it automatically takes the value `1`." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If `scalar` is passed in, this may be done as either a positional or a keyword argument. Which of the two calls where `scalar` is `2` is faster to understand in a larger program?" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers, scalar=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Keyword-only Arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because we *assumed* that scaling occurs rarely, we would prefer that our new version of `average_evens()` be called with a *keyword argument* whenever `scalar` is passed in. Then, a function call is never ambiguous when reading the source code.\n", + "\n", + "Python offers a **keyword-only** syntax when defining a function that *forces* a caller to pass the `scalar` argument *by name* if it is passed in at all: To do so, we place an asterisk `*` before the arguments that may only be passed in by name. Note that the keyword-only syntax also works *without* a default argument." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(numbers, *, scalar=1):\n", + " \"\"\"Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " numbers (list of int's/float's): numbers to be averaged;\n", + " if non-whole numbers are provided, they are rounded\n", + " scalar (float, optional): multiplies the average; defaults to 1\n", + "\n", + " Returns:\n", + " scaled_average (float)\n", + " \"\"\"\n", + " numbers = [round(n) for n in numbers]\n", + " evens = [n for n in numbers if n % 2 == 0]\n", + " average = sum(evens) / len(evens)\n", + " return scalar * average" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As before, we may call `average_evens()` without passing in an argument for the `scalar` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we call `average_evens()` with a `scalar` argument, we *must* use keyword notation." + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(numbers, scalar=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If instead we pass in `scalar` as a positional argument, we get a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "average_evens() takes 1 positional argument but 2 were given", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[81], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43maverage_evens\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnumbers\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: average_evens() takes 1 positional argument but 2 were given" + ] + } + ], + "source": [ + "average_evens(numbers, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Anonymous Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `def` statement is a statement because of its *side effect* of creating a *new* name that references a *new* `function` object in memory.\n", + "\n", + "We can thus think of it as doing *two* things **atomically** (i.e., either both of them happen or none). First, a `function` object is created that contains the concrete $0$s and $1$s that resemble the instructions we put into the function's body. In the context of a function, these $0$s and $1$s are also called **[byte code ](https://en.wikipedia.org/wiki/Bytecode)**. Then, a name referencing the new `function` object is created.\n", + "\n", + "Only this second aspect makes `def` a statement: Merely creating a new object in memory without making it accessible for later reference does *not* constitute a side effect because the state the program is *not* changed. After all, if we cannot reference an object, how do we know it exists in the first place?\n", + "\n", + "Python provides a `lambda` expression syntax that allows us to *only* create a `function` object in memory *without* making a name reference it (cf., [reference ](https://docs.python.org/3/reference/expressions.html#lambda)). It starts with the keyword `lambda` followed by an optional listing of comma separated parameters, a mandatory colon, and *one* expression that serves as the return value of the resulting `function` object. Because it does *not* create a name referencing the object, we effectively create \"anonymous\" functions with it.\n", + "\n", + "In the example, we create a `function` object that adds `3` to the only argument passed in as the parameter `x` and returns that sum." + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(x)>" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lambda x: x + 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If you think this is rather pointless to do, you are absolutely correct!\n", + "\n", + "We created a `function` object, dit *not* call it, and Python immediately forgot about it. So what's the point?\n", + "\n", + "To inspect the object created by a `lambda` expression, we use the simple `=` statement and assign it to the variable `add_three`, which is really `add_three()` as per our convention from above." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "add_three = lambda x: x + 3 # we could and should use def instead" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[type() ](https://docs.python.org/3/library/functions.html#type) and [callable() ](https://docs.python.org/3/library/functions.html#callable) confirm that `add_three` is indeed a callable `function` object." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "function" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(add_three)" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "callable(add_three)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now we may call `add_three()` as if we defined it with the `def` statement." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "add_three(39)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we could call an `function` object created with a `lambda` expression right away (i.e., without assigning it to a variable), which looks quite weird for now as we need *two* pairs of parentheses: The first one serves as a delimiter whereas the second represents the call operator." + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(lambda x: x + 3)(39)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The main point of having functions without a reference to them is to use them in a situation where we know ahead of time that we use the function only *once*.\n", + "\n", + "Popular applications of lambda expressions occur in combination with the **map-filter-reduce** paradigm (cf., [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb#Lambda-Expressions))." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/02_functions/01_exercises_sphere-volume.ipynb b/02_functions/01_exercises_sphere-volume.ipynb new file mode 100644 index 0000000..49f957e --- /dev/null +++ b/02_functions/01_exercises_sphere-volume.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/02_functions/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 2: Functions & Modularization (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb) of Chapter 2.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Volume of a Sphere" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: The [volume of a sphere ](https://en.wikipedia.org/wiki/Sphere) is defined as $\\frac{4}{3} * \\pi * r^3$. Calculate this value for $r=10.0$ and round it to 10 digits after the comma.\n", + "\n", + "Hints:\n", + "- use an appropriate approximation for $\\pi$\n", + "- you may use the [standard library ](https://docs.python.org/3/library/index.html) to do so if you have already looked at the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/02_content.ipynb) of Chapter 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ... # you may drop this cell and use your own approximation for Pi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Encapsulate the logic into a function `sphere_volume()` that takes one *positional* argument `radius` and one *keyword-only* argument `digits` defaulting to `5`. The volume should be returned as a `float` object under *all* circumstances." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def sphere_volume(...):\n", + " \"\"\"Calculate the volume of a sphere.\n", + "\n", + " Args:\n", + " radius (float): radius of the sphere\n", + " digits (optional, int): number of digits\n", + " for rounding the resulting volume\n", + "\n", + " Returns:\n", + " volume (float)\n", + " \"\"\"\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Evaluate the function with `radius = 100.0` and 1, 5, 10, 15, and 20 digits respectively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "radius = ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sphere_volume(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sphere_volume(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sphere_volume(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sphere_volume(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sphere_volume(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What observation do you make?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Using the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in, write a `for`-loop and calculate the volume of a sphere with `radius = 42.0` for all `digits` from `1` through `20`. Print out each volume on a separate line.\n", + "\n", + "Note: This is the first task where you need to use the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "radius = ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for ... in ...:\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: What lesson do you learn about the `float` type?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/02_functions/02_content.ipynb b/02_functions/02_content.ipynb new file mode 100644 index 0000000..5e663cc --- /dev/null +++ b/02_functions/02_content.ipynb @@ -0,0 +1,1639 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/02_functions/02_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 2: Functions & Modularization (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, we have only used what we refer to as **core** Python in this book. By this, we mean all the syntactical rules as specified in the [language reference ](https://docs.python.org/3/reference/) and a minimal set of about 50 built-in [functions ](https://docs.python.org/3/library/functions.html). With this, we could already implement any algorithm or business logic we can think of!\n", + "\n", + "However, after our first couple of programs, we would already start seeing recurring patterns in the code we write. In other words, we would constantly be \"reinventing the wheel\" in each new project.\n", + "\n", + "Would it not be smarter to pull out the reusable components from our programs and put them into some project independent **library** of generically useful functionalities? Then we would only need a way of including these **utilities** in our projects.\n", + "\n", + "As all programmers across all languages face this very same issue, most programming languages come with a so-called **[standard library ](https://en.wikipedia.org/wiki/Standard_library)** that provides utilities to accomplish everyday tasks without much code. Examples are making an HTTP request to some website, open and read popular file types (e.g., CSV or Excel files), do something on a computer's file system, and many more." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The Standard Library" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python also comes with a [standard library ](https://docs.python.org/3/library/index.html) that is structured into coherent modules and packages for given topics: A **module** is just a plain text file with the file extension *.py* that contains Python code while a **package** is a folder that groups several related modules.\n", + "\n", + "The code in the [standard library ](https://docs.python.org/3/library/index.html) is contributed and maintained by many volunteers around the world. In contrast to so-called \"third-party\" packages (cf., the next section below), the Python core development team closely monitors and tests the code in the [standard library ](https://docs.python.org/3/library/index.html). Consequently, we can be reasonably sure that anything provided by it works correctly independent of our computer's operating system and will most likely also be there in the next Python versions. Parts in the [standard library ](https://docs.python.org/3/library/index.html) that are computationally expensive are often rewritten in C and, therefore, much faster than anything we could write in Python ourselves. So, whenever we can solve a problem with the help of the [standard library ](https://docs.python.org/3/library/index.html), it is almost always the best way to do so as well.\n", + "\n", + "The [standard library ](https://docs.python.org/3/library/index.html) has grown very big over the years, and we refer to the website [PYMOTW](https://pymotw.com/3/index.html) (i.e., \"Python Module of the Week\") that features well written introductory tutorials and how-to guides to most parts of the library. The same author also published a [book](https://www.amazon.com/Python-Standard-Library-Example-Developers/dp/0134291050/ref=as_li_ss_tl?ie=UTF8&qid=1493563121&sr=8-1&keywords=python+3+standard+library+by+example) that many Pythonistas keep on their shelf for reference. Knowing what is in the [standard library ](https://docs.python.org/3/library/index.html) is quite valuable for solving real-world tasks quickly.\n", + "\n", + "Throughout this book, we look at many modules and packages from the [standard library ](https://docs.python.org/3/library/index.html) in more depth, starting with the [math ](https://docs.python.org/3/library/math.html) and [random ](https://docs.python.org/3/library/random.html) modules in this chapter." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### [math ](https://docs.python.org/3/library/math.html) Module" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [math ](https://docs.python.org/3/library/math.html) module provides non-trivial mathematical functions like $sin(x)$ and constants like $\\pi$ or $\\text{e}$.\n", + "\n", + "To make functions and variables defined \"somewhere else\" available in our current program, we must first **import** them with the `import` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#import)). " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This creates the variable `math` that references a **[module object ](https://docs.python.org/3/glossary.html#term-module)** (i.e., type `module`) in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140177537558144" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(math)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "module" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(math)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`module` objects serve as namespaces to organize the names inside a module. In this context, a namespace is nothing but a prefix that avoids collision with the variables already defined at the location where we import the module into.\n", + "\n", + "Let's see what we can do with the `math` module.\n", + "\n", + "The [dir() ](https://docs.python.org/3/library/functions.html#dir) built-in function may also be used with an argument passed in. Ignoring the dunder-style names, `math` offers quite a lot of names. As we cannot know at this point if a listed name refers to a function or an ordinary variable, we use the more generic term **attribute** to mean either one of them." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['__doc__',\n", + " '__file__',\n", + " '__loader__',\n", + " '__name__',\n", + " '__package__',\n", + " '__spec__',\n", + " 'acos',\n", + " 'acosh',\n", + " 'asin',\n", + " 'asinh',\n", + " 'atan',\n", + " 'atan2',\n", + " 'atanh',\n", + " 'cbrt',\n", + " 'ceil',\n", + " 'comb',\n", + " 'copysign',\n", + " 'cos',\n", + " 'cosh',\n", + " 'degrees',\n", + " 'dist',\n", + " 'e',\n", + " 'erf',\n", + " 'erfc',\n", + " 'exp',\n", + " 'exp2',\n", + " 'expm1',\n", + " 'fabs',\n", + " 'factorial',\n", + " 'floor',\n", + " 'fmod',\n", + " 'frexp',\n", + " 'fsum',\n", + " 'gamma',\n", + " 'gcd',\n", + " 'hypot',\n", + " 'inf',\n", + " 'isclose',\n", + " 'isfinite',\n", + " 'isinf',\n", + " 'isnan',\n", + " 'isqrt',\n", + " 'lcm',\n", + " 'ldexp',\n", + " 'lgamma',\n", + " 'log',\n", + " 'log10',\n", + " 'log1p',\n", + " 'log2',\n", + " 'modf',\n", + " 'nan',\n", + " 'nextafter',\n", + " 'perm',\n", + " 'pi',\n", + " 'pow',\n", + " 'prod',\n", + " 'radians',\n", + " 'remainder',\n", + " 'sin',\n", + " 'sinh',\n", + " 'sqrt',\n", + " 'sumprod',\n", + " 'tan',\n", + " 'tanh',\n", + " 'tau',\n", + " 'trunc',\n", + " 'ulp']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir(math)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Common mathematical constants and functions are now available via the dot operator `.` on the `math` object. This operator is sometimes also called the **attribute access operator**, in line with the just introduced term." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.141592653589793" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.pi" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2.718281828459045" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.e" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.sqrt" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function sqrt in module math:\n", + "\n", + "sqrt(x, /)\n", + " Return the square root of x.\n", + "\n" + ] + } + ], + "source": [ + "help(math.sqrt)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.4142135623730951" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.sqrt(2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Observe how the arguments passed to functions do not need to be just variables or simple literals. Instead, we may pass in any *expression* that evaluates to a *new* object of the type the function expects.\n", + "\n", + "So just as a reminder from the expression vs. statement discussion in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Expressions): An expression is *any* syntactically correct combination of variables and literals with operators. And the call operator `()` is yet another operator. So both of the next two code cells are just expressions! They have no permanent side effects in memory. We may execute them as often as we want *without* changing the state of the program (i.e., this Jupyter notebook).\n", + "\n", + "So, regarding the very next cell in particular: Although the `2 ** 2` creates a *new* object `4` in memory that is then immediately passed into the [math.sqrt() ](https://docs.python.org/3/library/math.html#math.sqrt) function, once that function call returns, \"all is lost\" and the newly created `4` object is forgotten again, as well as the return value of [math.sqrt() ](https://docs.python.org/3/library/math.html#math.sqrt)." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2.0" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.sqrt(2 ** 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Even the **composition** of several function calls only constitutes another expression." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10.0" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.sqrt(sum([99, 100, 101]) / 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we only need one particular function from a module, we may also use the alternative `from ... import ...` syntax.\n", + "\n", + "This does *not* create a module object but only makes a variable in our current location reference an object defined inside a module directly." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "from math import sqrt" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sqrt(16)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### [random ](https://docs.python.org/3/library/random.html) Module" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Often, we need a random variable, for example, when we want to build a simulation. The [random ](https://docs.python.org/3/library/random.html) module in the [standard library ](https://docs.python.org/3/library/index.html) often suffices for that." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Besides the usual dunder-style attributes, the built-in [dir() ](https://docs.python.org/3/library/functions.html#dir) function lists some attributes in an upper case naming convention and many others starting with a *single* underscore `_`. To understand the former, we must wait until [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb), while the latter is explained further below." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['BPF',\n", + " 'LOG4',\n", + " 'NV_MAGICCONST',\n", + " 'RECIP_BPF',\n", + " 'Random',\n", + " 'SG_MAGICCONST',\n", + " 'SystemRandom',\n", + " 'TWOPI',\n", + " '_ONE',\n", + " '_Sequence',\n", + " '__all__',\n", + " '__builtins__',\n", + " '__cached__',\n", + " '__doc__',\n", + " '__file__',\n", + " '__loader__',\n", + " '__name__',\n", + " '__package__',\n", + " '__spec__',\n", + " '_accumulate',\n", + " '_acos',\n", + " '_bisect',\n", + " '_ceil',\n", + " '_cos',\n", + " '_e',\n", + " '_exp',\n", + " '_fabs',\n", + " '_floor',\n", + " '_index',\n", + " '_inst',\n", + " '_isfinite',\n", + " '_lgamma',\n", + " '_log',\n", + " '_log2',\n", + " '_os',\n", + " '_pi',\n", + " '_random',\n", + " '_repeat',\n", + " '_sha512',\n", + " '_sin',\n", + " '_sqrt',\n", + " '_test',\n", + " '_test_generator',\n", + " '_urandom',\n", + " '_warn',\n", + " 'betavariate',\n", + " 'binomialvariate',\n", + " 'choice',\n", + " 'choices',\n", + " 'expovariate',\n", + " 'gammavariate',\n", + " 'gauss',\n", + " 'getrandbits',\n", + " 'getstate',\n", + " 'lognormvariate',\n", + " 'normalvariate',\n", + " 'paretovariate',\n", + " 'randbytes',\n", + " 'randint',\n", + " 'random',\n", + " 'randrange',\n", + " 'sample',\n", + " 'seed',\n", + " 'setstate',\n", + " 'shuffle',\n", + " 'triangular',\n", + " 'uniform',\n", + " 'vonmisesvariate',\n", + " 'weibullvariate']" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir(random)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [random.random() ](https://docs.python.org/3/library/random.html#random.random) function generates a uniformly distributed `float` number between $0$ (including) and $1$ (excluding)." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.random" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function random:\n", + "\n", + "random() method of random.Random instance\n", + " random() -> x in the interval [0, 1).\n", + "\n" + ] + } + ], + "source": [ + "help(random.random)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.782609162553633" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.random()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While we could build some conditional logic with an `if` statement to map the number generated by [random.random() ](https://docs.python.org/3/library/random.html#random.random) to a finite set of elements manually, the [random.choice() ](https://docs.python.org/3/library/random.html#random.choice) function provides a lot more **convenience** for us. We call it with, for example, the `numbers` list and it draws one element out of it with equal chance." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + ">" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.choice" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on method choice in module random:\n", + "\n", + "choice(seq) method of random.Random instance\n", + " Choose a random element from a non-empty sequence.\n", + "\n" + ] + } + ], + "source": [ + "help(random.choice)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.choice(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To reproduce the *same* random numbers in a simulation each time we run it, we set the **[random seed ](https://en.wikipedia.org/wiki/Random_seed)**. It is good practice to do that at the beginning of a program or notebook. It becomes essential when we employ randomized machine learning algorithms, like the [Random Forest ](https://en.wikipedia.org/wiki/Random_forest), and want to obtain **reproducible** results for publication in academic journals.\n", + "\n", + "The [random ](https://docs.python.org/3/library/random.html) module provides the [random.seed() ](https://docs.python.org/3/library/random.html#random.seed) function to do that." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.6394267984578837" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.random()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.6394267984578837" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.random()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Third-party Packages" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the Python community is based around open source, many developers publish their code, for example, on the Python Package Index [PyPI](https://pypi.org) from where anyone may download and install it for free using command-line based tools like [pip](https://pip.pypa.io/en/stable/) or [conda](https://conda.io/en/latest/). This way, we can always customize our Python installation even more. Managing many such packages is quite a deep topic on its own, sometimes fearfully called **[dependency hell ](https://en.wikipedia.org/wiki/Dependency_hell)**.\n", + "\n", + "The difference between the [standard library ](https://docs.python.org/3/library/index.html) and such **third-party** packages is that in the first case, the code goes through a much more formalized review process and is officially endorsed by the Python core developers. Yet, many third-party projects also offer the highest quality standards and are also relied on by many businesses and researchers.\n", + "\n", + "Throughout this book, we will look at many third-party libraries, mostly from Python's [scientific stack](https://scipy.org/about.html), a tightly coupled set of third-party libraries for storing **big data** efficiently (e.g., [numpy](http://www.numpy.org/)), \"wrangling\" (e.g., [pandas](https://pandas.pydata.org/)) and visualizing them (e.g., [matplotlib](https://matplotlib.org/) or [seaborn](https://seaborn.pydata.org/)), fitting classical statistical models (e.g., [statsmodels](http://www.statsmodels.org/)), training machine learning models (e.g., [sklearn](http://scikit-learn.org/)), and much more.\n", + "\n", + "Below, we briefly show how to install third-party libraries." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### [numpy](http://www.numpy.org/) Library" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[numpy](http://www.numpy.org/) is the de-facto standard in the Python world for handling **array-like** data. That is a fancy word for data that can be put into a matrix or vector format.\n", + "\n", + "As [numpy](http://www.numpy.org/) is *not* in the [standard library ](https://docs.python.org/3/library/index.html), it must be *manually* installed, for example, with the [pip](https://pip.pypa.io/en/stable/) tool. As mentioned in [Chapter 0 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/00_intro/00_content.ipynb#Markdown-Cells-vs.-Code-Cells), to execute terminal commands from within a Jupyter notebook, we start a code cell with an exclamation mark.\n", + "\n", + "If you are running this notebook with an installation of the [Anaconda Distribution](https://www.anaconda.com/distribution/), then [numpy](http://www.numpy.org/) is probably already installed. Running the cell below confirms that." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: numpy in /home/alexander/Repositories/intro-to-python/.venv/lib64/python3.12/site-packages (1.26.4)\n" + ] + } + ], + "source": [ + "!pip install numpy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[numpy](http://www.numpy.org/) is conventionally imported with the shorter **idiomatic** name `np`. The `as` in the import statement changes the resulting variable name. It is a shortcut for the three lines `import numpy`, `np = numpy`, and `del numpy`." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`np` is used in the same way as `math` or `random` above." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's convert the above `numbers` list into a vector-like object of type `numpy.ndarray`." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "vec = np.array(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4])" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vec" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "numpy.ndarray" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(vec)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[numpy](http://www.numpy.org/) somehow magically adds new behavior to Python's built-in arithmetic operators. For example, we may now [scalar-multiply ](https://en.wikipedia.org/wiki/Scalar_multiplication) `vec`. Also, [numpy](http://www.numpy.org/)'s functions are implemented in highly optimized C code and, therefore, are fast, especially when dealing with bigger amounts of data." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([14, 22, 16, 10, 6, 24, 4, 12, 18, 20, 2, 8])" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 * vec" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This scalar multiplication would \"fail\" if we used a plain `list` object like `numbers` instead of an `numpy.ndarray` object like `vec`. The two types exhibit different **behavior** when used with the same operator, another example of **operator overloading**." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4, 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 * numbers # surprise, surprise" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[numpy](http://www.numpy.org/)'s `numpy.ndarray` objects integrate nicely with Python's built-in functions (e.g., [sum() ](https://docs.python.org/3/library/functions.html#sum)) or functions from the [standard library ](https://docs.python.org/3/library/index.html) (e.g., [random.choice() ](https://docs.python.org/3/library/random.html#random.choice))." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "78" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(vec)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.choice(vec)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Local Modules and Packages" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For sure, we can create local modules and packages. In the Chapter 2 directory, there is a [*sample_module.py* ](https://github.com/webartifex/intro-to-python/blob/main/02_functions/sample_module.py) file that contains, among others, a function equivalent to the final version of `average_evens()`. To be realistic, this sample module is structured in a modular manner with several functions building on each other. It is best to skim over it *now* before reading on.\n", + "\n", + "To make code we put into a *.py* file available in our program, we import it as a module just as we did above with modules in the [standard library ](https://docs.python.org/3/library/index.html) or third-party packages.\n", + "\n", + "The `pwd` utility tells us in which directory Python is currently in. We refer to that as the **working directory**, and `pwd` reads \"print working directory.\" JupyterLab automatically sets this to the directory in which the notebook is in." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/home/alexander/Repositories/intro-to-python/02_functions\n" + ] + } + ], + "source": [ + "!pwd" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The *name* to be imported is the file's name except for the *.py* part. For this to work, the file's name *must* adhere to the *same* rules as hold for [variable names ](https://docs.python.org/3/reference/lexical_analysis.html#identifiers) in general.\n", + "\n", + "What happens during an import is as follows. When Python sees the `import sample_module` part, it first creates a *new* object of type `module` in memory. This is effectively an *empty* namespace. Then, it executes the imported file's code from top to bottom. Whatever variables are still defined at the end of this, are put into the module's namespace. Only if the file's code does *not* raise an error, will Python make a variable in our current location (i.e., `mod` here) reference the created `module` object. Otherwise, it is discarded. In essence, it is as if we copied and pasted the file's code in place of the import statement. If we import an already imported module again, Python is smart enough to avoid doing all this work all over and does nothing." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "import sample_module as mod" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mod" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Disregarding the dunder-style attributes, `mod` defines the attributes `_round_all`, `_scaled_average`, `average`, `average_evens`, and `average_odds`, which are exactly the ones we would expect from reading the [*sample_module.py* ](https://github.com/webartifex/intro-to-python/blob/main/02_functions/sample_module.py) file.\n", + "\n", + "A convention when working with imported code is to *disregard* any attributes starting with a single underscore `_`. These are considered **private** and constitute **implementation details** the author of the imported code might change in a future version of his software. We *must not* rely on them in any way.\n", + "\n", + "In contrast, the three remaining **public** attributes are the functions `average()`, `average_evens()`, and `average_odds()` that we may use after the import." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['__builtins__',\n", + " '__cached__',\n", + " '__doc__',\n", + " '__file__',\n", + " '__loader__',\n", + " '__name__',\n", + " '__package__',\n", + " '__spec__',\n", + " '_round_all',\n", + " '_scaled_average',\n", + " 'average',\n", + " 'average_evens',\n", + " 'average_odds']" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir(mod)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We use the imported `mod.average_evens()` just like `average_evens()` defined in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb) of this chapter. The advantage we get from **modularization** with *.py* files is that we can now easily reuse functions across different Jupyter notebooks without redefining them again and again. Also, we can \"source out\" code that distracts from the storyline told in a notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mod.average_evens" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function average_evens in module sample_module:\n", + "\n", + "average_evens(numbers, *, scalar=1)\n", + " Calculate the average of all even numbers in a list.\n", + "\n", + " Args:\n", + " numbers (list of int's/float's): numbers to be averaged;\n", + " if non-whole numbers are provided, they are rounded\n", + " scalar (float, optional): multiplies the average; defaults to 1\n", + "\n", + " Returns:\n", + " scaled_average (float)\n", + "\n" + ] + } + ], + "source": [ + "help(mod.average_evens)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mod.average_evens(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mod.average_evens(numbers, scalar=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Packages are a generalization of modules, and we look at one in detail in [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/04_content.ipynb#Packages-vs.-Modules).\n", + "\n", + "As a further reading on modules and packages, we refer to the [official tutorial ](https://docs.python.org/3/tutorial/modules.html)." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/02_functions/03_summary.ipynb b/02_functions/03_summary.ipynb new file mode 100644 index 0000000..f5efdad --- /dev/null +++ b/02_functions/03_summary.ipynb @@ -0,0 +1,90 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 2: Functions & Modularization (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A user-defined **function** is a **named sequence** of statements that perform a computation.\n", + "\n", + "Functions provide benefits as they\n", + "\n", + "- make programs easier to comprehend and debug for humans as they give names to the smaller parts of a larger program (i.e., they **modularize** a code base), and\n", + "- eliminate redundancies by allowing **reuse of code**.\n", + "\n", + "Functions are **defined** once with the `def` statement. Then, they may be **called** many times with the call operator `()`.\n", + "\n", + "They may process **parameterized** inputs, **passed** in as **arguments**, and output a **return value**.\n", + "\n", + "Arguments may be passed in by **position** or **keyword**. Some functions may even require **keyword-only** arguments.\n", + "\n", + "**Lambda expressions** create anonymous functions.\n", + "\n", + "Functions are a special kind of **callables**. Any object that may be **called** with the call operator `()` is a callable. Built-in functions and **constructors** are other kinds of callables.\n", + "\n", + "Core Python can be extended with code from either the **standard library** or **third-party** libraries.\n", + "\n", + "Outside Jupyter notebooks, Python code is put into **modules** that are grouped in **packages**." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/02_functions/04_review.ipynb b/02_functions/04_review.ipynb new file mode 100644 index 0000000..f3b7ab9 --- /dev/null +++ b/02_functions/04_review.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 2: Functions & Modularization (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb) and the [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/02_content.ipynb) part of Chapter 2.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: What behavior of the `def` statement makes it a **statement**? Is there a way to use an **expression** to create a function?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: One of the first confusions of experienced programmers coming from other languages to Python regards the observation that **\"everything in Python is an object\"** (cf., this [discussion](https://www.reddit.com/r/learnpython/comments/8rypx9/everything_in_python_is_an_object/)). How does this relate to **functions**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: What does it mean for a variable to go out of **scope**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: How can a **global** variable be **shadowed**? Is this good or bad?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Explain the concept of **forwarding** a **function call**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: What are **keyword-only arguments** and when is it appropriate to use them?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: What are **callables**? How do they relate to `function` objects?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: A mere function **call** is just an **expression**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: When using the `import` statement, we need to ensure that the imported attributes do *not* overwrite any already defined variables and functions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10:** Functions always have a name by which we can call them." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: The [standard library ](https://docs.python.org/3/library/index.html) is a collection of numerical tools often used in scientific computing, for example, advanced mathematical functions or utilities for simulation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/02_functions/sample_module.py b/02_functions/sample_module.py new file mode 100644 index 0000000..3a097e1 --- /dev/null +++ b/02_functions/sample_module.py @@ -0,0 +1,76 @@ +"""This is a sample module. + +It defines three functions average(), average_evens(), and average_odds(). +The point is to show how we can put Python code in a .py file to be re-used +in some other place. + +We should never forget to document the code as well, both on the module +level (i.e., this docstring) but also in every function it defines. + +When imported, Python modules are executed top to bottom before the flow of +execution returns to wherever they were imported into. + +An important convention is to prefix variables and functions that are not to +be used outside the module with a single underscore "_". This way, we can +design the code within a module in a modular fashion and only "export" what we +want. + +Here, all three functions internally forward parts of their computations +to the utility functions _round_all() and _scaled_average() that contain all +the logic common to the three functions. + +While this example is stylized, it shows how Python modules are often +designed. +""" + +def _round_all(numbers): + """Internal utility function to round all numbers in a list.""" + return [round(n) for n in numbers] + + +def _scaled_average(numbers, scalar): + """Internal utility function to calculate scaled averages.""" + average = sum(numbers) / len(numbers) + return scalar * average + + +def average(numbers, *, scalar=1): + """Calculate the average of all numbers in a list. + + Args: + numbers (list of int's/float's): numbers to be averaged; + if non-whole numbers are provided, they are rounded + scalar (float, optional): multiplies the average; defaults to 1 + + Returns: + scaled_average (float) + """ + return _scaled_average(_round_all(numbers), scalar) + + +def average_evens(numbers, *, scalar=1): + """Calculate the average of all even numbers in a list. + + Args: + numbers (list of int's/float's): numbers to be averaged; + if non-whole numbers are provided, they are rounded + scalar (float, optional): multiplies the average; defaults to 1 + + Returns: + scaled_average (float) + """ + return _scaled_average([n for n in _round_all(numbers) if n % 2 == 0], scalar) + + +def average_odds(numbers, *, scalar=1): + """Calculate the average of all odd numbers in a list. + + Args: + numbers (list of int's/float's): numbers to be averaged; + if non-whole numbers are provided, they are rounded + scalar (float, optional): multiplies the average; defaults to 1 + + Returns: + scaled_average (float) + """ + return _scaled_average([n for n in _round_all(numbers) if n % 2 != 0], scalar) diff --git a/03_conditionals/00_content.ipynb b/03_conditionals/00_content.ipynb new file mode 100644 index 0000000..8797d0a --- /dev/null +++ b/03_conditionals/00_content.ipynb @@ -0,0 +1,2831 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/03_conditionals/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 3: Conditionals & Exceptions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We analyzed every aspect of the `average_evens()` function in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb) except for the `if`-related parts. While it does what we expect it to, there is a whole lot more to learn by taking it apart. In particular, the `if` may occur within both a **statement** or an **expression**, analogous as to how a noun in a natural language can be the subject of *or* an object in a sentence. What is common to both usages is that it leads to code being executed for *parts* of the input only. It is a way of controlling the **flow of execution** in a program.\n", + "\n", + "After deconstructing `if` in the first part of this chapter, we take a close look at a similar concept, namely handling **exceptions**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Boolean Expressions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Any expression that is either true or not is called a **boolean expression**. It is such simple true-or-false observations about the world on which mathematicians, and originally philosophers, base their rules of reasoning: They are studied formally in the field of [propositional logic ](https://en.wikipedia.org/wiki/Propositional_calculus).\n", + "\n", + "A trivial example involves the equality operator `==` that evaluates to either `True` or `False` depending on its operands \"comparing equal\" or not." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 == 42" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 == 123" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `==` operator handles objects of *different* types: Because of that, it implements a notion of equality in line with how humans think of things being equal or not. After all, `42` and `42.0` are different $0$s and $1$s for a computer and other programming languages may say `False` here! Technically, this is yet another example of operator overloading." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 == 42.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There are, however, cases where the `==` operator seems to not work intuitively. [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb#Imprecision) provides more insights into this \"bug.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 == 42.000000000000001" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `bool` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`True` and `False` are built-in *objects* of type `bool`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "True" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94478067031520" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "bool" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "False" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94478067031552" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(False)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "bool" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's not confuse the boolean `False` with `None`, another built-in object! We saw the latter before in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Function-Definitions) as the *implicit* return value of a function without a `return` statement.\n", + "\n", + "We might think of `None` indicating a \"maybe\" or even an \"unknown\" answer; however, for Python, there are no \"maybe\" or \"unknown\" objects, as we see further below!\n", + "\n", + "Whereas `False` is of type `bool`, `None` is of type `NoneType`. So, they are unrelated! On the contrary, as both `True` and `False` are of the same type, we could call them \"siblings.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "None" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94478066920032" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(None)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "NoneType" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(None)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Singletons" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`True`, `False`, and `None` have the property that they each exist in memory only *once*. Objects designed this way are so-called **singletons**. This **[design pattern ](https://en.wikipedia.org/wiki/Design_Patterns)** was originally developed to keep a program's memory usage at a minimum. It may only be employed in situations where we know that an object does *not* mutate its value (i.e., to reuse the bag analogy from [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Objects-vs.-Types-vs.-Values), no flipping of $0$s and $1$s in the bag is allowed). In languages \"closer\" to the memory like C, we would have to code this singleton logic ourselves, but Python has this built in for *some* types.\n", + "\n", + "We verify this with either the `is` operator or by comparing memory addresses." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a = True\n", + "b = True\n", + "\n", + "a is b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To contrast this, we create *two* `789` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a = 789\n", + "b = 789\n", + "\n", + "a is b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So the following expression regards *four* objects in memory: *One* `list` object holding six references to *three* other objects." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[True, False, None, True, False, None]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[True, False, None, True, False, None]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Relational Operators" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The equality operator is only one of several **relational** (i.e., \"comparison\") **operators** who all evaluate to a `bool` object." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 == 123" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 != 123" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The \"less than\" `<` or \"greater than\" `>` operators mean \"*strictly* less than\" or \"*strictly* greater than\" but may be combined with the equality operator into just `<=` and `>=`. This is a shortcut for using the logical `or` operator as described in the next section." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 < 123" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 <= 123" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 > 123" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 >= 123" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Logical Operators" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Boolean expressions may be combined or negated with the **logical operators** `and`, `or`, and `not` to form new boolean expressions. This may be done repeatedly to obtain boolean expressions of arbitrary complexity.\n", + "\n", + "Their usage is similar to how the equivalent words are used in everyday English:\n", + "\n", + "- `and` evaluates to `True` if *both* operands evaluate to `True` and `False` otherwise,\n", + "- `or` evaluates to `True` if either one *or* both operands evaluate to `True` and `False` otherwise, and\n", + "- `not` evaluates to `True` if its *only* operand evaluates to `False` and vice versa." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a = 42\n", + "b = 87" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Relational operators have **[higher precedence ](https://docs.python.org/3/reference/expressions.html#operator-precedence)** over logical operators. So the following expression means what we intuitively think it does." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a > 5 and b <= 100" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, sometimes, it is good to use *parentheses* around each operand for clarity." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(a > 5) and (b <= 100)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This is especially so when several logical operators are combined." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a <= 5 or not b > 100" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(a <= 5) or not (b > 100)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(a <= 5) or (not (b > 100)) # no need to \"over do\" it" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For even better readability, some practitioners suggest to *never* use the `>` and `>=` operators (cf., [source](https://llewellynfalco.blogspot.com/2016/02/dont-use-greater-than-sign-in.html); note that the included example is written in [Java ](https://en.wikipedia.org/wiki/Java_%28programming_language%29) where `&&` means `and` and `||` means `or`).\n", + "\n", + "We may **chain** operators if the expressions that contain them are combined with the `and` operator. For example, the following two cells implement the same logic, where the second is a lot easier to read." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "5 < a and a < 87" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "5 < a < 87" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Truthy vs. Falsy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The operands of a logical operator do not need to be *boolean* expressions but may be *any* expression. If an operand does *not* evaluate to an object of type `bool`, Python automatically casts it as such. Then, Pythonistas say that the expression is evaluated in a boolean context.\n", + "\n", + "For example, any non-zero numeric object is cast as `True`. While this behavior allows writing more concise and thus more \"beautiful\" code, it may also be a source of confusion.\n", + "\n", + "So, `(a - 40)` is cast as `True` and then the overall expression evaluates to `True` as well." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(a - 40) and (b < 100)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whenever we are unsure how Python evaluates a non-boolean expression in a boolean context, the [bool() ](https://docs.python.org/3/library/functions.html#bool) built-in allows us to do it ourselves. [bool() ](https://docs.python.org/3/library/functions.html#bool), like [int() ](https://docs.python.org/3/library/functions.html#int), is yet another *constructor*." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(a - 40)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(a - 42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's keep in mind that negative numbers also evaluate to `True`!" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(a - 44)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In a boolean context, `None` is cast as `False`! So, `None` is *not* a \"maybe\" answer but a \"no.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(None)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another good rule to know is that container types (e.g., `list`) evaluate to `False` whenever they are empty and `True` if they hold at least one element." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool([])" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool([False])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With `str` objects, the empty `\"\"` evaluates to `False`, and any other to `True`." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(\"Lorem ipsum dolor sit amet.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Pythonistas use the terms **truthy** or **falsy** to describe a non-boolean expression's behavior when evaluated in a boolean context." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Short-Circuiting" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When evaluating expressions involving the `and` and `or` operators, Python follows the **[short-circuiting ](https://en.wikipedia.org/wiki/Short-circuit_evaluation)** strategy: Once it is clear what the overall truth value is, no more operands are evaluated, and the result is *immediately* returned.\n", + "\n", + "Also, if such expressions are evaluated in a non-boolean context, the result is returned as is and *not* cast as a `bool` type.\n", + "\n", + "The two rules can be summarized as:\n", + "\n", + "- `a or b`: If `a` is truthy, it is returned *without* evaluating `b`. Otherwise, `b` is evaluated *and* returned.\n", + "- `a and b`: If `a` is falsy, it is returned *without* evaluating `b`. Otherwise, `b` is evaluated *and* returned.\n", + "\n", + "The rules may also be chained or combined.\n", + "\n", + "Let's look at a couple of examples below. To visualize which operands are evaluated, we define a helper function `expr()` that prints out the only argument it is passed before returning it." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def expr(arg):\n", + " \"\"\"Print and return the only argument.\"\"\"\n", + " print(\"Arg:\", arg)\n", + " return arg" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the `or` operator, the first truthy operand is returned." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 or 1" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 0\n", + "Arg: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(0) or expr(1) # both operands are evaluated" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 or 2" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(1) or expr(2) # 2 is not evaluated" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 or 1 or 2" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 0\n", + "Arg: 1\n" + ] + }, + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(0) or expr(1) or expr(2) # 2 is not evaluated" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If all operands are falsy, the last one is returned." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "False or [] or 0" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: False\n", + "Arg: []\n", + "Arg: 0\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(False) or expr([]) or expr(0) # all operands are evaluated" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the `and` operator, the first falsy operand is returned." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 and 1" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 0\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(0) and expr(1) # 1 is not evaluated" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 and 0" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 1\n", + "Arg: 0\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(1) and expr(0) # both operands are evaluated" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 and 0 and 2" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 1\n", + "Arg: 0\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(1) and expr(0) and expr(2) # 2 is not evaluated" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If all operands are truthy, the last one is returned." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 and 2 and 3" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arg: 1\n", + "Arg: 2\n", + "Arg: 3\n" + ] + }, + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expr(1) and expr(2) and expr(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The crucial takeaway is that Python does *not* necessarily evaluate *all* operands and, therefore, our code should never rely on that assumption." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `if` Statement" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To write useful programs, we need to control the flow of execution, for example, to react to user input. The logic by which a program follows the rules from the \"real world\" is referred to as **[business logic ](https://en.wikipedia.org/wiki/Business_logic)**.\n", + "\n", + "One language feature to do so is the `if` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#the-if-statement)). It consists of:\n", + "\n", + "- *one* mandatory `if`-clause,\n", + "- an *arbitrary* number of `elif`-clauses (i.e., \"else if\"), and\n", + "- an *optional* `else`-clause.\n", + "\n", + "The `if`- and `elif`-clauses each specify one *boolean* expression, also called **condition** here, while the `else`-clause serves as the \"catch everything else\" case.\n", + "\n", + "In contrast to our intuitive interpretation in natural languages, only the code in *one* of the alternatives, also called **branches**, is executed. To be precise, it is always the code in the first clause whose condition evaluates to `True`.\n", + "\n", + "In terms of syntax, the header lines end with a colon, and the code blocks are indented. Formally, any statement that is written across several lines is called a **[compound statement ](https://docs.python.org/3/reference/compound_stmts.html#compound-statements)**, the code blocks are called **suites** and belong to one header line, and the term **clause** refers to a header line and its suite as a whole. So far, we have seen three compound statements: `for`, `if`, and `def`. On the contrary, **[simple statements ](https://docs.python.org/3/reference/simple_stmts.html#simple-statements)**, for example, `=`, `del`, or `return`, are written on *one* line.\n", + "\n", + "As an example, let's write code that checks if a randomly drawn number is divisible by `2`, `3`, both, or none. The code should print out a customized message for each of the *four* cases." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### \"Wrong Logic\" Example: Is the number divisible by `2`, `3`, both, or none?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It turns out that translating this task into code is not so trivial. Whereas the code below looks right, it is *incorrect*. The reason is that the order of the `if`-, `elif`-, and `else`-clauses matters." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 is divisible by 2 only\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "if number % 2 == 0:\n", + " print(number, \"is divisible by 2 only\")\n", + "elif number % 3 == 0:\n", + " print(number, \"is divisible by 3 only\")\n", + "elif number % 2 == 0 and number % 3 == 0:\n", + " print(number, \"is divisible by 2 and 3\")\n", + "else:\n", + " print(number, \"is divisible by neither 2 nor 3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### \"Correct Logic\" Example: Is the number divisible by `2`, `3`, both, or none?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As a number divisible by both `2` and `3` is always a special (i.e., narrower) case of a number being divisible by either `2` or `3` on their own, we must check for that condition first. The order of the two latter cases is not important as they are mutually exclusive. Below is a correct implementation of the program." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 is divisible by 2 and 3\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "if number % 3 == 0 and number % 2 == 0:\n", + " print(number, \"is divisible by 2 and 3\")\n", + "elif number % 3 == 0:\n", + " print(number, \"is divisible by 3 only\")\n", + "elif number % 2 == 0:\n", + " print(number, \"is divisible by 2 only\")\n", + "else:\n", + " print(number, \"is divisible by neither 2 nor 3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### \"Concise Logic\" Example: Is the number divisible by `2`, `3`, both, or none?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A minor improvement could be to replace `number % 3 == 0 and number % 2 == 0` with the conciser `number % 6 == 0`. However, this has no effect on the order that is still essential for the code's correctness." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 is divisible by 2 and 3\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "if number % 6 == 0:\n", + " print(number, \"is divisible by 2 and 3\")\n", + "elif number % 3 == 0:\n", + " print(number, \"is divisible by 3 only\")\n", + "elif number % 2 == 0:\n", + " print(number, \"is divisible by 2 only\")\n", + "else:\n", + " print(number, \"is divisible by neither 2 nor 3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Only the `if`-clause is mandatory" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Often, we only need a reduced form of the `if` statement.\n", + "\n", + "For example, below we **inject** code to print a message at random." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You read this as often as you see heads when tossing a coin\n" + ] + } + ], + "source": [ + "if random.random() > 0.5:\n", + " print(\"You read this as often as you see heads when tossing a coin\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Common Use Case: A binary Choice" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "More often than not, we model a **binary choice**. Then, we only need to write an `if`- and an `else`-clause." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 is even\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "if number % 2 == 0:\n", + " print(number, \"is even\")\n", + "else:\n", + " print(number, \"is odd\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To write the condition even conciser, we may take advantage of Python's implicit casting and leave out the `== 0`. However, then we *must* exchange the two suits! The `if`-clause below means \"If the `number` is odd\" in plain English. That is the opposite of the `if`-clause above." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 is even\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "if number % 2: # Note the opposite meaning!\n", + " print(number, \"is odd\")\n", + "else:\n", + " print(number, \"is even\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### \"Hard to read\" Example: Nesting `if` Statements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may **nest** `if` statements to control the flow of execution in a more granular way. Every additional layer, however, makes the code *less* readable, in particular, if we have more than one line per code block.\n", + "\n", + "For example, the code cell below implements an [A/B Testing ](https://en.wikipedia.org/wiki/A/B_testing) strategy where half the time a \"complex\" message is shown to a \"user\" while in the remaining times an \"easy\" message is shown. To do so, the code first \"tosses a coin\" and then checks a randomly drawn `number`." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 can be divided by 2 without a rest\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "# Coin is heads.\n", + "if random.random() > 0.5:\n", + " if number % 2 == 0:\n", + " print(number, \"can be divided by 2 without a rest\")\n", + " else:\n", + " print(number, \"divided by 2 results in a non-zero rest\")\n", + "# Coin is tails.\n", + "else:\n", + " if number % 2 == 0:\n", + " print(number, \"is even\")\n", + " else:\n", + " print(number, \"is odd\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### \"Easy to read\" Example: Flattening nested `if` Statements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A way to make this code more readable is to introduce **temporary variables** *in combination* with the `and` operator to **flatten** the branching logic. The `if` statement then reads almost like plain English. In contrast to many other languages, creating variables is a computationally *cheap* operation in Python (i.e., only a reference is created) and also helps to document the code *inline* with meaningful variable names.\n", + "\n", + "Flattening the logic *without* temporary variables could lead to *more* sub-expressions in the conditions be evaluated than necessary. Do you see why?" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(789)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6 can be divided by 2 without a rest\n" + ] + } + ], + "source": [ + "number = random.choice(numbers)\n", + "\n", + "coin_is_heads = random.random() > 0.5\n", + "number_is_even = number % 2 == 0\n", + "\n", + "if coin_is_heads and number_is_even:\n", + " print(number, \"can be divided by 2 without a rest\")\n", + "elif coin_is_heads and not number_is_even:\n", + " print(number, \"divided by 2 results in a non-zero rest\")\n", + "elif not coin_is_heads and number_is_even:\n", + " print(number, \"is even\")\n", + "else:\n", + " print(number, \"is odd\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `if` Expression" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When an `if` statement assigns an object to a variable according to a true-or-false condition (i.e., a binary choice), there is a shortcut: We assign the variable the result of a so-called **[conditional expression ](https://docs.python.org/3/reference/expressions.html#conditional-expressions)**, or `if` expression for short, instead.\n", + "\n", + "Think of a situation where we evaluate a piece-wise functional relationship $y = f(x)$ at a given $x$, for example:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$\n", + "y = f(x) =\n", + "\\begin{cases}\n", + "0, \\text{ if } x \\le 0 \\\\\n", + "x, \\text{ otherwise}\n", + "\\end{cases}\n", + "$" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, we could use an `if` statement as above to do the job. Yet, this is rather lengthy." + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 3\n", + "\n", + "if x <= 0:\n", + " y = 0\n", + "else:\n", + " y = x\n", + "\n", + "y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, the `if` expression fits into one line. The main downside is a potential loss in readability, in particular, if the functional relationship is not that simple. Also, some practitioners do *not* like that the condition is in the middle of the expression." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 3\n", + "\n", + "y = 0 if x <= 0 else x\n", + "\n", + "y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this example, however, the most elegant solution is to use the built-in [max() ](https://docs.python.org/3/library/functions.html#max) function." + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 3\n", + "\n", + "y = max(0, x)\n", + "\n", + "y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `try` Statement" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the previous two chapters, we encountered a couple of *runtime* errors. A natural urge we might have after reading about conditional statements is to write code that somehow reacts to the occurrence of such exceptions.\n", + "\n", + "Consider a situation where we are given some user input that may contain values that cause problems. To illustrate this, we draw a random integer between `0` and `5`, and then divide by this number. Naturally, we see a `ZeroDivisionError` in 16.6% of the cases." + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(123)" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ZeroDivisionError", + "evalue": "division by zero", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[78], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m user_input \u001b[38;5;241m=\u001b[39m random\u001b[38;5;241m.\u001b[39mchoice([\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m4\u001b[39m, \u001b[38;5;241m5\u001b[39m])\n\u001b[0;32m----> 3\u001b[0m \u001b[38;5;241;43m1\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[43m \u001b[49m\u001b[43muser_input\u001b[49m\n", + "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" + ] + } + ], + "source": [ + "user_input = random.choice([0, 1, 2, 3, 4, 5])\n", + "\n", + "1 / user_input" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the compound `try` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement)), we can **handle** any *runtime* error.\n", + "\n", + "In its simplest form, it comes with just two clauses: `try` and `except`. The following tells Python to execute the code in the `try`-clause, and if *anything* goes wrong, continue in the `except`-clause instead of **raising** an error to us. Of course, if nothing goes wrong, the `except`-clause is *not* executed." + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(123)" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Something went wrong\n" + ] + } + ], + "source": [ + "user_input = random.choice([0, 1, 2, 3, 4, 5])\n", + "\n", + "try:\n", + " print(\"The result is\", 1 / user_input)\n", + "except:\n", + " print(\"Something went wrong\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, it is good practice *not* to handle *any* possible exception but only the ones we may *expect* from the code in the `try`-clause. The reason for that is that we do not want to risk *suppressing* an exception that we do *not* expect. Also, the code base becomes easier to understand as we communicate what could go wrong during execution in an *explicit* way to the (human) reader. Python comes with a lot of [built-in exceptions ](https://docs.python.org/3/library/exceptions.html#concrete-exceptions) that we should familiarize ourselves with.\n", + "\n", + "Another good practice is to always keep the code in the `try`-clause short to not *accidentally* handle an exception we do *not* want to handle.\n", + "\n", + "In the example, we are dividing numbers and may expect a `ZeroDivisionError`." + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(123)" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Something went wrong\n" + ] + } + ], + "source": [ + "user_input = random.choice([0, 1, 2, 3, 4, 5])\n", + "\n", + "try:\n", + " print(\"The result is\", 1 / user_input)\n", + "except ZeroDivisionError:\n", + " print(\"Something went wrong\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Often, we may have to run some code *independent* of an exception occurring, for example, to close a connection to a database. To achieve that, we add a `finally`-clause to the `try` statement.\n", + "\n", + "Similarly, we may have to run some code *only if* no exception occurs, but we do not want to put it in the `try`-clause as per the good practice mentioned above. To achieve that, we add an `else`-clause to the `try` statement.\n", + "\n", + "To showcase everything together, we look at one last example. It is randomized: So, run the cell several times and see for yourself." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(123)" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Oops. Division by 0. How does that work?\n", + "I am always printed\n" + ] + } + ], + "source": [ + "user_input = random.choice([0, 1, 2, 3, 4, 5])\n", + "\n", + "try:\n", + " result = 1 / user_input\n", + "except ZeroDivisionError:\n", + " print(\"Oops. Division by 0. How does that work?\")\n", + "else:\n", + " print(\"The result is\", result)\n", + "finally:\n", + " print(\"I am always printed\")" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/03_conditionals/01_exercises_discounts.ipynb b/03_conditionals/01_exercises_discounts.ipynb new file mode 100644 index 0000000..9764fd6 --- /dev/null +++ b/03_conditionals/01_exercises_discounts.ipynb @@ -0,0 +1,205 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/03_conditionals/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 3: Conditionals & Exceptions (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb).\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Discounting Customer Orders" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Write a function `discounted_price()` that takes the positional arguments `unit_price` (of type `float`) and `quantity` (of type `int`) and implements a discount scheme for a line item in a customer order as follows:\n", + "\n", + "- if the unit price is over 100 dollars, grant 10% relative discount\n", + "- if a customer orders more than 10 items, one in every five items is for free\n", + "\n", + "Only one of the two discounts is granted, whichever is better for the customer.\n", + "\n", + "The function should then return the overall price for the line item. Do not forget to round appropriately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def discounted_price(unit_price, quantity):\n", + " \"\"\"Calculate the price of a line item in an order.\n", + "\n", + " Args:\n", + " unit_price (float): price of one ordered item\n", + " quantity (int): number of items ordered\n", + "\n", + " Returns:\n", + " line_item_price (float)\n", + " \"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Calculate the final price for the following line items of an order:\n", + "- $7$ smartphones @ $99.00$ USD\n", + "- $3$ workstations @ $999.00$ USD\n", + "- $19$ GPUs @ $879.95$ USD\n", + "- $14$ Raspberry Pis @ $35.00$ USD" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discounted_price(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discounted_price(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discounted_price(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discounted_price(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Calculate the last two line items with order quantities of $20$ and $15$. What do you observe?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discounted_price(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "discounted_price(...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Looking at the `if`-`else`-logic in the function, why do you think the four example line items in **Q2** were chosen as they were?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/03_conditionals/02_exercises_fizz-buzz.ipynb b/03_conditionals/02_exercises_fizz-buzz.ipynb new file mode 100644 index 0000000..5b0ce28 --- /dev/null +++ b/03_conditionals/02_exercises_fizz-buzz.ipynb @@ -0,0 +1,140 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/03_conditionals/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 3: Conditionals & Exceptions (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb).\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fizz Buzz" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The kids game [Fizz Buzz ](https://en.wikipedia.org/wiki/Fizz_buzz) is said to be often used in job interviews for entry-level positions. However, opinions vary as to how good of a test it is (cf., [source ](https://news.ycombinator.com/item?id=16446774)).\n", + "\n", + "In its simplest form, a group of people starts counting upwards in an alternating fashion. Whenever a number is divisible by $3$, the person must say \"Fizz\" instead of the number. The same holds for numbers divisible by $5$ when the person must say \"Buzz.\" If a number is divisible by both numbers, one must say \"FizzBuzz.\" Probably, this game would also make a good drinking game with the \"right\" beverages." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: First, create a list `numbers` with the numbers from 1 through 100. You could type all numbers manually, but there is, of course, a smarter way. The built-in [range() ](https://docs.python.org/3/library/functions.html#func-range) may be useful here. Read how it works in the documentation. To make the output of [range() ](https://docs.python.org/3/library/functions.html#func-range) a `list` object, you have to wrap it with the [list() ](https://docs.python.org/3/library/functions.html#func-list) built-in (i.e., `list(range(...))`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numbers = ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Loop over the `numbers` list and *replace* numbers for which one of the two (or both) conditions apply with text strings `\"Fizz\"`, `\"Buzz\"`, or `\"FizzBuzz\"` using the indexing operator `[]` and the assignment statement `=`.\n", + "\n", + "In [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Who-am-I?-And-how-many?), we saw that Python starts indexing with `0` as the first element. Keep that in mind.\n", + "\n", + "So in each iteration of the `for`-loop, you have to determine an `index` variable as well as check the actual `number` for its divisors.\n", + "\n", + "Hint: the order of the conditions is important!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in numbers:\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Create a loop that prints out either the number or any of the Fizz Buzz substitutes in `numbers`! Do it in such a way that the output is concise!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for number in numbers:\n", + " print(...)" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/03_conditionals/03_summary.ipynb b/03_conditionals/03_summary.ipynb new file mode 100644 index 0000000..7d7b12d --- /dev/null +++ b/03_conditionals/03_summary.ipynb @@ -0,0 +1,76 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 3: Conditionals & Exceptions (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "- **boolean expressions** evaluate to either `True` or `False`\n", + "- **relational operators** (e.g., `==` or `!=`) compare operands according to \"human\" interpretations\n", + "- **logical operators** (e.g., `and` )combine boolean sub-expressions to more \"complex\" expressions\n", + "- the **conditional statement** (i.e., the `if` statement) allows **controlling** the **flow of execution** depending on some **conditions**\n", + "- a **conditional expression** is a short form of a conditional statement\n", + "- **exception handling** is also a common way of **controlling** the **flow of execution**, in particular, if we have to be prepared for bad input data" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/03_conditionals/04_review.ipynb b/03_conditionals/04_review.ipynb new file mode 100644 index 0000000..67e9dd3 --- /dev/null +++ b/03_conditionals/04_review.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 3: Conditionals & Exceptions (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb).\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: What is the **singleton** design pattern? How many objects does the expression `[True, False, True, False]` generate in memory?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What do we mean when we talk about **truthy** and **falsy** expressions?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Describe in your own words the concept of **short-circuiting**! What does it imply for an individual sub-expression in a boolean expression?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Explain how the conceptual difference between a **statement** and an **expression** relates to the difference between a **conditional statement** and a **conditional expression**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Why is the use of **temporary variables** encouraged in Python?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: What does the `finally`-clause enforce in this code snippet? How can a `try` statement be useful *without* an `except`-clause?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "try:\n", + " print(\"Make a request to a service on the internet\")\n", + "finally:\n", + " print(\"This could be clean-up code\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: The objects `True`, `False`, and `None` represent the idea of *yes*, *no*, and *maybe* answers in a natural language.\n", + "\n", + "Hint: you also respond with a code cell." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: The `try` statement is useful for handling **syntax** errors." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/04_iteration/00_content.ipynb b/04_iteration/00_content.ipynb new file mode 100644 index 0000000..362583b --- /dev/null +++ b/04_iteration/00_content.ipynb @@ -0,0 +1,4942 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 4: Recursion & Looping" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While controlling the flow of execution with an `if` statement is a must-have building block in any programming language, it alone does not allow us to run a block of code repetitively, and we need to be able to do precisely that to write useful software.\n", + "\n", + "The `for` statement shown in some examples before might be the missing piece in the puzzle. However, we can live without it and postpone its official introduction until the second half of this chapter.\n", + "\n", + "Instead, we dive into the big idea of **iteration** by studying the concept of **recursion** first. This order is opposite to many other introductory books that only treat the latter as a nice-to-have artifact, if at all. Yet, understanding recursion sharpens one's mind and contributes to seeing problems from a different angle." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Recursion" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A popular joke among programmers by an unknown author goes like this (cf., [discussion](https://www.quora.com/What-does-the-phrase-in-order-to-understand-recursion-you-must-first-understand-recursion-mean-to-you)):" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "> \"In order to understand **recursion**, you must first understand **recursion**.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A function that calls itself is **recursive**, and the process of executing such a function is called **recursion**.\n", + "\n", + "Recursive functions contain some form of a conditional check (e.g., `if` statement) to identify a **base case** that ends the recursion *when* reached. Otherwise, the function would keep calling itself forever.\n", + "\n", + "The meaning of the word *recursive* is similar to *circular*. However, a truly circular definition is not very helpful, and we think of a recursive function as kind of a \"circular function with a way out at the end.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Trivial Example: Countdown" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A rather trivial toy example illustrates the important aspects concretely: If called with any positive integer as its `n` argument, `countdown()` just prints that number and calls itself with the *new* `n` being the *old* `n` minus `1`. This continues until `countdown()` is called with `n=0`. Then, the flow of execution hits the base case, and the function calls stop." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def countdown(n):\n", + " \"\"\"Print a countdown until the party starts.\n", + "\n", + " Args:\n", + " n (int): seconds until the party begins\n", + " \"\"\"\n", + " if n == 0:\n", + " print(\"Happy New Year!\")\n", + " else:\n", + " print(n)\n", + " countdown(n - 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n", + "2\n", + "1\n", + "Happy New Year!\n" + ] + } + ], + "source": [ + "countdown(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As trivial as this seems, a lot of complexity is hidden in this implementation. In particular, the order in which objects are created and de-referenced in memory might not be apparent right away as [PythonTutor ](http://pythontutor.com/visualize.html#code=def%20countdown%28n%29%3A%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20print%28%22Happy%20new%20Year!%22%29%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20print%28n%29%0A%20%20%20%20%20%20%20%20countdown%28n%20-%201%29%0A%0Acountdown%283%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows: Each time `countdown()` is called, Python creates a *new* frame in the part of the memory where it manages all the names. This way, Python *isolates* all the different `n` variables from each other. As new frames are created until we reach the base case, after which the frames are destroyed in the *reversed* order, this is called a **[stack ](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))** of frames in computer science terminology. In simple words, a stack is a last-in-first-out (LIFO) task queue. Each frame has a single parent frame, namely the one whose recursive function call created it." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Recursion in Mathematics" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Recursion plays a vital role in mathematics as well, and we likely know about it from some introductory course, for example, in [combinatorics ](https://en.wikipedia.org/wiki/Combinatorics)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Easy Example: [Factorial ](https://en.wikipedia.org/wiki/Factorial)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The factorial function, denoted with the $!$ symbol, is defined as follows for all non-negative integers:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$$0! = 1$$\n", + "$$n! = n*(n-1)!$$" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whenever we find a recursive way of formulating an idea, we can immediately translate it into Python in a *naive* way (i.e., we create a *correct* program that may *not* be an *efficient* implementation yet).\n", + "\n", + "Below is a first version of `factorial()`: The `return` statement does not have to be a function's last code line, and we may indeed have several `return` statements as well." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for\n", + "\n", + " Returns:\n", + " factorial (int)\n", + " \"\"\"\n", + " if n == 0:\n", + " return 1\n", + " else:\n", + " recurse = factorial(n - 1)\n", + " result = n * recurse\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When we read such code, it is often easier not to follow every function call (i.e., `factorial(n - 1)` here) in one's mind but assume we receive a return value as specified in the documentation. Some call this approach a **[leap of faith](http://greenteapress.com/thinkpython2/html/thinkpython2007.html#sec75)**. We practice this already whenever we call built-in functions (e.g., [print() ](https://docs.python.org/3/library/functions.html#print) or [len() ](https://docs.python.org/3/library/functions.html#len)) where we would have to read C code in many cases.\n", + "\n", + "To visualize *all* the computational steps of the exemplary `factorial(3)`, we use [PythonTutor ](http://pythontutor.com/visualize.html#code=def%20factorial%28n%29%3A%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20recurse%20%3D%20factorial%28n%20-%201%29%0A%20%20%20%20%20%20%20%20result%20%3D%20n%20*%20recurse%0A%20%20%20%20%20%20%20%20return%20result%0A%0Asolution%20%3D%20factorial%283%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false): The recursion again creates a stack of frames in memory. In contrast to the previous trivial example, each frame leaves a return value in memory after it is destroyed. This return value is then assigned to the `recurse` variable within the parent frame and used to compute `result`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3628800" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A Pythonista would formulate `factorial()` in a more concise way using the so-called **early exit** pattern: No `else`-clause is needed as reaching a `return` statement ends a function call *immediately*. Furthermore, we do not need the temporary variables `recurse` and `result`.\n", + "\n", + "As [PythonTutor ](http://pythontutor.com/visualize.html#code=def%20factorial%28n%29%3A%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20return%20n%20*%20factorial%28n%20-%201%29%0A%0Asolution%20%3D%20factorial%283%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows, this implementation is more efficient as it only requires 18 computational steps instead of 24 to calculate `factorial(3)`, an improvement of 25 percent! " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for\n", + "\n", + " Returns:\n", + " factorial (int)\n", + " \"\"\"\n", + " if n == 0:\n", + " return 1\n", + " return n * factorial(n - 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3628800" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Note that the [math ](https://docs.python.org/3/library/math.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides a [factorial() ](https://docs.python.org/3/library/math.html#math.factorial) function as well, and we should, therefore, *never* implement it ourselves in a real codebase." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function factorial in module math:\n", + "\n", + "factorial(n, /)\n", + " Find n!.\n", + "\n", + " Raise a ValueError if x is negative or non-integral.\n", + "\n" + ] + } + ], + "source": [ + "help(math.factorial)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.factorial(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3628800" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.factorial(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### \"Involved\" Example: [Euclid's Algorithm ](https://en.wikipedia.org/wiki/Euclidean_algorithm)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As famous philosopher Euclid already shows in his \"Elements\" (ca. 300 BC), the greatest common divisor of two integers, i.e., the largest number that divides both integers without a remainder, can be efficiently computed with the following code. This example illustrates that a recursive solution to a problem is not always easy to understand." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def gcd(a, b):\n", + " \"\"\"Calculate the greatest common divisor of two numbers.\n", + "\n", + " Args:\n", + " a (int): first number\n", + " b (int): second number\n", + "\n", + " Returns:\n", + " gcd (int)\n", + " \"\"\"\n", + " if b == 0:\n", + " return a \n", + " return gcd(b, a % b)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gcd(12, 4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Euclid's algorithm is stunningly fast, even for large numbers. Its speed comes from the use of the modulo operator `%`. However, this is *not* true for recursion in general, which may result in slow programs if not applied correctly." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gcd(112233445566778899, 987654321)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As expected, for two [prime numbers ](https://en.wikipedia.org/wiki/List_of_prime_numbers) the greatest common divisor is of course $1$." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gcd(7, 7919)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [math ](https://docs.python.org/3/library/math.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides a [gcd() ](https://docs.python.org/3/library/math.html#math.gcd) function as well, and, therefore, we should again *never* implement it on our own." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on built-in function gcd in module math:\n", + "\n", + "gcd(*integers)\n", + " Greatest Common Divisor.\n", + "\n" + ] + } + ], + "source": [ + "help(math.gcd)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.gcd(12, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.gcd(112233445566778899, 987654321)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "math.gcd(7, 7919)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### \"Easy at first Glance\" Example: [Fibonacci Numbers ](https://en.wikipedia.org/wiki/Fibonacci_number)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The Fibonacci numbers are an infinite sequence of non-negative integers that are calculated such that every number is the sum of its two predecessors where the first two numbers of the sequence are defined to be $0$ and $1$. For example, the first 13 numbers are:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144$" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's write a function `fibonacci()` that calculates the $i$th Fibonacci number where $0$ is the $0$th number. Looking at the numbers in a **backward** fashion (i.e., from right to left), we realize that the return value for `fibonacci(i)` can be reduced to the sum of the return values for `fibonacci(i - 1)` and `fibonacci(i - 2)` disregarding the *two* base cases." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def fibonacci(i):\n", + " \"\"\"Calculate the ith Fibonacci number.\n", + "\n", + " Args:\n", + " i (int): index of the Fibonacci number to calculate\n", + "\n", + " Returns:\n", + " ith_fibonacci (int)\n", + " \"\"\"\n", + " if i == 0:\n", + " return 0\n", + " elif i == 1:\n", + " return 1\n", + " return fibonacci(i - 1) + fibonacci(i - 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "144" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12) # = 13th number" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Efficiency of Algorithms" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This implementation is *highly* **inefficient** as small Fibonacci numbers already take a very long time to compute. The reason for this is **exponential growth** in the number of function calls. As [PythonTutor ](http://pythontutor.com/visualize.html#code=def%20fibonacci%28i%29%3A%0A%20%20%20%20if%20i%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20elif%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20return%20fibonacci%28i%20-%201%29%20%2B%20fibonacci%28i%20-%202%29%0A%0Arv%20%3D%20fibonacci%285%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows, `fibonacci()` is called again and again with the same `i` arguments.\n", + "\n", + "To understand this in detail, we have to study algorithms and data structures (e.g., with [this book](https://www.amazon.de/Introduction-Algorithms-Press-Thomas-Cormen/dp/0262033844/ref=sr_1_1?__mk_de_DE=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=1JNE8U0VZGU0O&qid=1569837169&s=gateway&sprefix=algorithms+an%2Caps%2C180&sr=8-1)), a discipline within computer science, and dive into the analysis of **[time complexity of algorithms ](https://en.wikipedia.org/wiki/Time_complexity)**.\n", + "\n", + "Luckily, in the Fibonacci case, the inefficiency can be resolved with a **caching** (i.e., \"reuse\") strategy from the field of **[dynamic programming ](https://en.wikipedia.org/wiki/Dynamic_programming)**, namely **[memoization ](https://en.wikipedia.org/wiki/Memoization)**. We do so in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb#Memoization), after introducing the `dict` data type.\n", + "\n", + "Let's measure the average run times for `fibonacci()` and varying `i` arguments with the `%%timeit` [cell magic](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-timeit) that comes with Jupyter." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "38.9 µs ± 1.48 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%%timeit -n 100\n", + "fibonacci(12)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12.2 ms ± 77.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%%timeit -n 100\n", + "fibonacci(24)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.89 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1 -r 1\n", + "fibonacci(36)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6.28 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1 -r 1\n", + "fibonacci(37)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Infinite Recursion" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If a recursion does not reach its base case, it theoretically runs forever. Luckily, Python detects that and saves the computer from crashing by raising a `RecursionError`.\n", + "\n", + "The simplest possible infinite recursion is generated like so." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def run_forever():\n", + " \"\"\"Also a pointless function should have a docstring.\"\"\"\n", + " run_forever()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "RecursionError", + "evalue": "maximum recursion depth exceeded", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[28], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mrun_forever\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[27], line 3\u001b[0m, in \u001b[0;36mrun_forever\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_forever\u001b[39m():\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Also a pointless function should have a docstring.\"\"\"\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m \u001b[43mrun_forever\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[27], line 3\u001b[0m, in \u001b[0;36mrun_forever\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_forever\u001b[39m():\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Also a pointless function should have a docstring.\"\"\"\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m \u001b[43mrun_forever\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + " \u001b[0;31m[... skipping similar frames: run_forever at line 3 (2974 times)]\u001b[0m\n", + "Cell \u001b[0;32mIn[27], line 3\u001b[0m, in \u001b[0;36mrun_forever\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mrun_forever\u001b[39m():\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Also a pointless function should have a docstring.\"\"\"\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m \u001b[43mrun_forever\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded" + ] + } + ], + "source": [ + "run_forever()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, even the trivial `countdown()` function from above is not immune to infinite recursion. Let's call it with `3.1` instead of `3`. What goes wrong here?" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.1\n", + "2.1\n", + "1.1\n", + "0.10000000000000009\n", + "-0.8999999999999999\n", + "-1.9\n", + "-2.9\n", + "-3.9\n", + "-4.9\n", + "-5.9\n", + "-6.9\n", + "-7.9\n", + "-8.9\n", + "-9.9\n", + "-10.9\n", + "-11.9\n", + "-12.9\n", + "-13.9\n", + "-14.9\n", + "-15.9\n", + "-16.9\n", + "-17.9\n", + "-18.9\n", + "-19.9\n", + "-20.9\n", + "-21.9\n", + "-22.9\n", + "-23.9\n", + "-24.9\n", + "-25.9\n", + "-26.9\n", + "-27.9\n", + "-28.9\n", + "-29.9\n", + "-30.9\n", + "-31.9\n", + "-32.9\n", + "-33.9\n", + "-34.9\n", + "-35.9\n", + "-36.9\n", + "-37.9\n", + "-38.9\n", + "-39.9\n", + "-40.9\n", + "-41.9\n", + "-42.9\n", + "-43.9\n", + "-44.9\n", + "-45.9\n", + "-46.9\n", + "-47.9\n", + "-48.9\n", + "-49.9\n", + "-50.9\n", + "-51.9\n", + "-52.9\n", + "-53.9\n", + "-54.9\n", + "-55.9\n", + "-56.9\n", + "-57.9\n", + "-58.9\n", + "-59.9\n", + "-60.9\n", + "-61.9\n", + "-62.9\n", + "-63.9\n", + "-64.9\n", + "-65.9\n", + "-66.9\n", + "-67.9\n", + "-68.9\n", + "-69.9\n", + "-70.9\n", + "-71.9\n", + "-72.9\n", + "-73.9\n", + "-74.9\n", + "-75.9\n", + "-76.9\n", + "-77.9\n", + "-78.9\n", + "-79.9\n", + "-80.9\n", + "-81.9\n", + "-82.9\n", + "-83.9\n", + "-84.9\n", + "-85.9\n", + "-86.9\n", + "-87.9\n", + "-88.9\n", + "-89.9\n", + "-90.9\n", + "-91.9\n", + "-92.9\n", + "-93.9\n", + "-94.9\n", + "-95.9\n", + "-96.9\n", + "-97.9\n", + "-98.9\n", + "-99.9\n", + "-100.9\n", + "-101.9\n", + "-102.9\n", + "-103.9\n", + "-104.9\n", + "-105.9\n", + "-106.9\n", + "-107.9\n", + "-108.9\n", + "-109.9\n", + "-110.9\n", + "-111.9\n", + "-112.9\n", + "-113.9\n", + "-114.9\n", + "-115.9\n", + "-116.9\n", + "-117.9\n", + "-118.9\n", + "-119.9\n", + "-120.9\n", + "-121.9\n", + "-122.9\n", + "-123.9\n", + "-124.9\n", + "-125.9\n", + "-126.9\n", + "-127.9\n", + "-128.9\n", + "-129.9\n", + "-130.9\n", + "-131.9\n", + "-132.9\n", + "-133.9\n", + "-134.9\n", + "-135.9\n", + "-136.9\n", + "-137.9\n", + "-138.9\n", + "-139.9\n", + "-140.9\n", + "-141.9\n", + "-142.9\n", + "-143.9\n", + "-144.9\n", + "-145.9\n", + "-146.9\n", + "-147.9\n", + "-148.9\n", + "-149.9\n", + "-150.9\n", + "-151.9\n", + "-152.9\n", + "-153.9\n", + "-154.9\n", + "-155.9\n", + "-156.9\n", + "-157.9\n", + "-158.9\n", + "-159.9\n", + "-160.9\n", + "-161.9\n", + "-162.9\n", + "-163.9\n", + "-164.9\n", + "-165.9\n", + "-166.9\n", + "-167.9\n", + "-168.9\n", + "-169.9\n", + "-170.9\n", + "-171.9\n", + "-172.9\n", + "-173.9\n", + "-174.9\n", + "-175.9\n", + "-176.9\n", + "-177.9\n", + "-178.9\n", + "-179.9\n", + "-180.9\n", + "-181.9\n", + "-182.9\n", + "-183.9\n", + "-184.9\n", + "-185.9\n", + "-186.9\n", + "-187.9\n", + "-188.9\n", + "-189.9\n", + "-190.9\n", + "-191.9\n", + "-192.9\n", + "-193.9\n", + "-194.9\n", + "-195.9\n", + "-196.9\n", + "-197.9\n", + "-198.9\n", + "-199.9\n", + "-200.9\n", + "-201.9\n", + "-202.9\n", + "-203.9\n", + "-204.9\n", + "-205.9\n", + "-206.9\n", + "-207.9\n", + "-208.9\n", + "-209.9\n", + "-210.9\n", + "-211.9\n", + "-212.9\n", + "-213.9\n", + "-214.9\n", + "-215.9\n", + "-216.9\n", + "-217.9\n", + "-218.9\n", + "-219.9\n", + "-220.9\n", + "-221.9\n", + "-222.9\n", + "-223.9\n", + "-224.9\n", + "-225.9\n", + "-226.9\n", + "-227.9\n", + "-228.9\n", + "-229.9\n", + "-230.9\n", + "-231.9\n", + "-232.9\n", + "-233.9\n", + "-234.9\n", + "-235.9\n", + "-236.9\n", + "-237.9\n", + "-238.9\n", + "-239.9\n", + "-240.9\n", + "-241.9\n", + "-242.9\n", + "-243.9\n", + "-244.9\n", + "-245.9\n", + "-246.9\n", + "-247.9\n", + "-248.9\n", + "-249.9\n", + "-250.9\n", + "-251.9\n", + "-252.9\n", + "-253.9\n", + "-254.9\n", + "-255.9\n", + "-256.9\n", + "-257.9\n", + "-258.9\n", + "-259.9\n", + "-260.9\n", + "-261.9\n", + "-262.9\n", + "-263.9\n", + "-264.9\n", + "-265.9\n", + "-266.9\n", + "-267.9\n", + "-268.9\n", + "-269.9\n", + "-270.9\n", + "-271.9\n", + "-272.9\n", + "-273.9\n", + "-274.9\n", + "-275.9\n", + "-276.9\n", + "-277.9\n", + "-278.9\n", + "-279.9\n", + "-280.9\n", + "-281.9\n", + "-282.9\n", + "-283.9\n", + "-284.9\n", + "-285.9\n", + "-286.9\n", + "-287.9\n", + "-288.9\n", + "-289.9\n", + "-290.9\n", + "-291.9\n", + "-292.9\n", + "-293.9\n", + "-294.9\n", + "-295.9\n", + "-296.9\n", + "-297.9\n", + "-298.9\n", + "-299.9\n", + "-300.9\n", + "-301.9\n", + "-302.9\n", + "-303.9\n", + "-304.9\n", + "-305.9\n", + "-306.9\n", + "-307.9\n", + "-308.9\n", + "-309.9\n", + "-310.9\n", + "-311.9\n", + "-312.9\n", + "-313.9\n", + "-314.9\n", + "-315.9\n", + "-316.9\n", + "-317.9\n", + "-318.9\n", + "-319.9\n", + "-320.9\n", + "-321.9\n", + "-322.9\n", + "-323.9\n", + "-324.9\n", + "-325.9\n", + "-326.9\n", + "-327.9\n", + "-328.9\n", + "-329.9\n", + "-330.9\n", + "-331.9\n", + "-332.9\n", + "-333.9\n", + "-334.9\n", + "-335.9\n", + "-336.9\n", + "-337.9\n", + "-338.9\n", + "-339.9\n", + "-340.9\n", + "-341.9\n", + "-342.9\n", + "-343.9\n", + "-344.9\n", + "-345.9\n", + "-346.9\n", + "-347.9\n", + "-348.9\n", + "-349.9\n", + "-350.9\n", + "-351.9\n", + "-352.9\n", + "-353.9\n", + "-354.9\n", + "-355.9\n", + "-356.9\n", + "-357.9\n", + "-358.9\n", + "-359.9\n", + "-360.9\n", + "-361.9\n", + "-362.9\n", + "-363.9\n", + "-364.9\n", + "-365.9\n", + "-366.9\n", + "-367.9\n", + "-368.9\n", + "-369.9\n", + "-370.9\n", + "-371.9\n", + "-372.9\n", + "-373.9\n", + "-374.9\n", + "-375.9\n", + "-376.9\n", + "-377.9\n", + "-378.9\n", + "-379.9\n", + "-380.9\n", + "-381.9\n", + "-382.9\n", + "-383.9\n", + "-384.9\n", + "-385.9\n", + "-386.9\n", + "-387.9\n", + "-388.9\n", + "-389.9\n", + "-390.9\n", + "-391.9\n", + "-392.9\n", + "-393.9\n", + "-394.9\n", + "-395.9\n", + "-396.9\n", + "-397.9\n", + "-398.9\n", + "-399.9\n", + "-400.9\n", + "-401.9\n", + "-402.9\n", + "-403.9\n", + "-404.9\n", + "-405.9\n", + "-406.9\n", + "-407.9\n", + "-408.9\n", + "-409.9\n", + "-410.9\n", + "-411.9\n", + "-412.9\n", + "-413.9\n", + "-414.9\n", + "-415.9\n", + "-416.9\n", + "-417.9\n", + "-418.9\n", + "-419.9\n", + "-420.9\n", + "-421.9\n", + "-422.9\n", + "-423.9\n", + "-424.9\n", + "-425.9\n", + "-426.9\n", + "-427.9\n", + "-428.9\n", + "-429.9\n", + "-430.9\n", + "-431.9\n", + "-432.9\n", + "-433.9\n", + "-434.9\n", + "-435.9\n", + "-436.9\n", + "-437.9\n", + "-438.9\n", + "-439.9\n", + "-440.9\n", + "-441.9\n", + "-442.9\n", + "-443.9\n", + "-444.9\n", + "-445.9\n", + "-446.9\n", + "-447.9\n", + "-448.9\n", + "-449.9\n", + "-450.9\n", + "-451.9\n", + "-452.9\n", + "-453.9\n", + "-454.9\n", + "-455.9\n", + "-456.9\n", + "-457.9\n", + "-458.9\n", + "-459.9\n", + "-460.9\n", + "-461.9\n", + "-462.9\n", + "-463.9\n", + "-464.9\n", + "-465.9\n", + "-466.9\n", + "-467.9\n", + "-468.9\n", + "-469.9\n", + "-470.9\n", + "-471.9\n", + "-472.9\n", + "-473.9\n", + "-474.9\n", + "-475.9\n", + "-476.9\n", + "-477.9\n", + "-478.9\n", + "-479.9\n", + "-480.9\n", + "-481.9\n", + "-482.9\n", + "-483.9\n", + "-484.9\n", + "-485.9\n", + "-486.9\n", + "-487.9\n", + "-488.9\n", + "-489.9\n", + "-490.9\n", + "-491.9\n", + "-492.9\n", + "-493.9\n", + "-494.9\n", + "-495.9\n", + "-496.9\n", + "-497.9\n", + "-498.9\n", + "-499.9\n", + "-500.9\n", + "-501.9\n", + "-502.9\n", + "-503.9\n", + "-504.9\n", + "-505.9\n", + "-506.9\n", + "-507.9\n", + "-508.9\n", + "-509.9\n", + "-510.9\n", + "-511.9\n", + "-512.9\n", + "-513.9\n", + "-514.9\n", + "-515.9\n", + "-516.9\n", + "-517.9\n", + "-518.9\n", + "-519.9\n", + "-520.9\n", + "-521.9\n", + "-522.9\n", + "-523.9\n", + "-524.9\n", + "-525.9\n", + "-526.9\n", + "-527.9\n", + "-528.9\n", + "-529.9\n", + "-530.9\n", + "-531.9\n", + "-532.9\n", + "-533.9\n", + "-534.9\n", + "-535.9\n", + "-536.9\n", + "-537.9\n", + "-538.9\n", + "-539.9\n", + "-540.9\n", + "-541.9\n", + "-542.9\n", + "-543.9\n", + "-544.9\n", + "-545.9\n", + "-546.9\n", + "-547.9\n", + "-548.9\n", + "-549.9\n", + "-550.9\n", + "-551.9\n", + "-552.9\n", + "-553.9\n", + "-554.9\n", + "-555.9\n", + "-556.9\n", + "-557.9\n", + "-558.9\n", + "-559.9\n", + "-560.9\n", + "-561.9\n", + "-562.9\n", + "-563.9\n", + "-564.9\n", + "-565.9\n", + "-566.9\n", + "-567.9\n", + "-568.9\n", + "-569.9\n", + "-570.9\n", + "-571.9\n", + "-572.9\n", + "-573.9\n", + "-574.9\n", + "-575.9\n", + "-576.9\n", + "-577.9\n", + "-578.9\n", + "-579.9\n", + "-580.9\n", + "-581.9\n", + "-582.9\n", + "-583.9\n", + "-584.9\n", + "-585.9\n", + "-586.9\n", + "-587.9\n", + "-588.9\n", + "-589.9\n", + "-590.9\n", + "-591.9\n", + "-592.9\n", + "-593.9\n", + "-594.9\n", + "-595.9\n", + "-596.9\n", + "-597.9\n", + "-598.9\n", + "-599.9\n", + "-600.9\n", + "-601.9\n", + "-602.9\n", + "-603.9\n", + "-604.9\n", + "-605.9\n", + "-606.9\n", + "-607.9\n", + "-608.9\n", + "-609.9\n", + "-610.9\n", + "-611.9\n", + "-612.9\n", + "-613.9\n", + "-614.9\n", + "-615.9\n", + "-616.9\n", + "-617.9\n", + "-618.9\n", + "-619.9\n", + "-620.9\n", + "-621.9\n", + "-622.9\n", + "-623.9\n", + "-624.9\n", + "-625.9\n", + "-626.9\n", + "-627.9\n", + "-628.9\n", + "-629.9\n", + "-630.9\n", + "-631.9\n", + "-632.9\n", + "-633.9\n", + "-634.9\n", + "-635.9\n", + "-636.9\n", + "-637.9\n", + "-638.9\n", + "-639.9\n", + "-640.9\n", + "-641.9\n", + "-642.9\n", + "-643.9\n", + "-644.9\n", + "-645.9\n", + "-646.9\n", + "-647.9\n", + "-648.9\n", + "-649.9\n", + "-650.9\n", + "-651.9\n", + "-652.9\n", + "-653.9\n", + "-654.9\n", + "-655.9\n", + "-656.9\n", + "-657.9\n", + "-658.9\n", + "-659.9\n", + "-660.9\n", + "-661.9\n", + "-662.9\n", + "-663.9\n", + "-664.9\n", + "-665.9\n", + "-666.9\n", + "-667.9\n", + "-668.9\n", + "-669.9\n", + "-670.9\n", + "-671.9\n", + "-672.9\n", + "-673.9\n", + "-674.9\n", + "-675.9\n", + "-676.9\n", + "-677.9\n", + "-678.9\n", + "-679.9\n", + "-680.9\n", + "-681.9\n", + "-682.9\n", + "-683.9\n", + "-684.9\n", + "-685.9\n", + "-686.9\n", + "-687.9\n", + "-688.9\n", + "-689.9\n", + "-690.9\n", + "-691.9\n", + "-692.9\n", + "-693.9\n", + "-694.9\n", + "-695.9\n", + "-696.9\n", + "-697.9\n", + "-698.9\n", + "-699.9\n", + "-700.9\n", + "-701.9\n", + "-702.9\n", + "-703.9\n", + "-704.9\n", + "-705.9\n", + "-706.9\n", + "-707.9\n", + "-708.9\n", + "-709.9\n", + "-710.9\n", + "-711.9\n", + "-712.9\n", + "-713.9\n", + "-714.9\n", + "-715.9\n", + "-716.9\n", + "-717.9\n", + "-718.9\n", + "-719.9\n", + "-720.9\n", + "-721.9\n", + "-722.9\n", + "-723.9\n", + "-724.9\n", + "-725.9\n", + "-726.9\n", + "-727.9\n", + "-728.9\n", + "-729.9\n", + "-730.9\n", + "-731.9\n", + "-732.9\n", + "-733.9\n", + "-734.9\n", + "-735.9\n", + "-736.9\n", + "-737.9\n", + "-738.9\n", + "-739.9\n", + "-740.9\n", + "-741.9\n", + "-742.9\n", + "-743.9\n", + "-744.9\n", + "-745.9\n", + "-746.9\n", + "-747.9\n", + "-748.9\n", + "-749.9\n", + "-750.9\n", + "-751.9\n", + "-752.9\n", + "-753.9\n", + "-754.9\n", + "-755.9\n", + "-756.9\n", + "-757.9\n", + "-758.9\n", + "-759.9\n", + "-760.9\n", + "-761.9\n", + "-762.9\n", + "-763.9\n", + "-764.9\n", + "-765.9\n", + "-766.9\n", + "-767.9\n", + "-768.9\n", + "-769.9\n", + "-770.9\n", + "-771.9\n", + "-772.9\n", + "-773.9\n", + "-774.9\n", + "-775.9\n", + "-776.9\n", + "-777.9\n", + "-778.9\n", + "-779.9\n", + "-780.9\n", + "-781.9\n", + "-782.9\n", + "-783.9\n", + "-784.9\n", + "-785.9\n", + "-786.9\n", + "-787.9\n", + "-788.9\n", + "-789.9\n", + "-790.9\n", + "-791.9\n", + "-792.9\n", + "-793.9\n", + "-794.9\n", + "-795.9\n", + "-796.9\n", + "-797.9\n", + "-798.9\n", + "-799.9\n", + "-800.9\n", + "-801.9\n", + "-802.9\n", + "-803.9\n", + "-804.9\n", + "-805.9\n", + "-806.9\n", + "-807.9\n", + "-808.9\n", + "-809.9\n", + "-810.9\n", + "-811.9\n", + "-812.9\n", + "-813.9\n", + "-814.9\n", + "-815.9\n", + "-816.9\n", + "-817.9\n", + "-818.9\n", + "-819.9\n", + "-820.9\n", + "-821.9\n", + "-822.9\n", + "-823.9\n", + "-824.9\n", + "-825.9\n", + "-826.9\n", + "-827.9\n", + "-828.9\n", + "-829.9\n", + "-830.9\n", + "-831.9\n", + "-832.9\n", + "-833.9\n", + "-834.9\n", + "-835.9\n", + "-836.9\n", + "-837.9\n", + "-838.9\n", + "-839.9\n", + "-840.9\n", + "-841.9\n", + "-842.9\n", + "-843.9\n", + "-844.9\n", + "-845.9\n", + "-846.9\n", + "-847.9\n", + "-848.9\n", + "-849.9\n", + "-850.9\n", + "-851.9\n", + "-852.9\n", + "-853.9\n", + "-854.9\n", + "-855.9\n", + "-856.9\n", + "-857.9\n", + "-858.9\n", + "-859.9\n", + "-860.9\n", + "-861.9\n", + "-862.9\n", + "-863.9\n", + "-864.9\n", + "-865.9\n", + "-866.9\n", + "-867.9\n", + "-868.9\n", + "-869.9\n", + "-870.9\n", + "-871.9\n", + "-872.9\n", + "-873.9\n", + "-874.9\n", + "-875.9\n", + "-876.9\n", + "-877.9\n", + "-878.9\n", + "-879.9\n", + "-880.9\n", + "-881.9\n", + "-882.9\n", + "-883.9\n", + "-884.9\n", + "-885.9\n", + "-886.9\n", + "-887.9\n", + "-888.9\n", + "-889.9\n", + "-890.9\n", + "-891.9\n", + "-892.9\n", + "-893.9\n", + "-894.9\n", + "-895.9\n", + "-896.9\n", + "-897.9\n", + "-898.9\n", + "-899.9\n", + "-900.9\n", + "-901.9\n", + "-902.9\n", + "-903.9\n", + "-904.9\n", + "-905.9\n", + "-906.9\n", + "-907.9\n", + "-908.9\n", + "-909.9\n", + "-910.9\n", + "-911.9\n", + "-912.9\n", + "-913.9\n", + "-914.9\n", + "-915.9\n", + "-916.9\n", + "-917.9\n", + "-918.9\n", + "-919.9\n", + "-920.9\n", + "-921.9\n", + "-922.9\n", + "-923.9\n", + "-924.9\n", + "-925.9\n", + "-926.9\n", + "-927.9\n", + "-928.9\n", + "-929.9\n", + "-930.9\n", + "-931.9\n", + "-932.9\n", + "-933.9\n", + "-934.9\n", + "-935.9\n", + "-936.9\n", + "-937.9\n", + "-938.9\n", + "-939.9\n", + "-940.9\n", + "-941.9\n", + "-942.9\n", + "-943.9\n", + "-944.9\n", + "-945.9\n", + "-946.9\n", + "-947.9\n", + "-948.9\n", + "-949.9\n", + "-950.9\n", + "-951.9\n", + "-952.9\n", + "-953.9\n", + "-954.9\n", + "-955.9\n", + "-956.9\n", + "-957.9\n", + "-958.9\n", + "-959.9\n", + "-960.9\n", + "-961.9\n", + "-962.9\n", + "-963.9\n", + "-964.9\n", + "-965.9\n", + "-966.9\n", + "-967.9\n", + "-968.9\n", + "-969.9\n", + "-970.9\n", + "-971.9\n", + "-972.9\n", + "-973.9\n", + "-974.9\n", + "-975.9\n", + "-976.9\n", + "-977.9\n", + "-978.9\n", + "-979.9\n", + "-980.9\n", + "-981.9\n", + "-982.9\n", + "-983.9\n", + "-984.9\n", + "-985.9\n", + "-986.9\n", + "-987.9\n", + "-988.9\n", + "-989.9\n", + "-990.9\n", + "-991.9\n", + "-992.9\n", + "-993.9\n", + "-994.9\n", + "-995.9\n", + "-996.9\n", + "-997.9\n", + "-998.9\n", + "-999.9\n", + "-1000.9\n", + "-1001.9\n", + "-1002.9\n", + "-1003.9\n", + "-1004.9\n", + "-1005.9\n", + "-1006.9\n", + "-1007.9\n", + "-1008.9\n", + "-1009.9\n", + "-1010.9\n", + "-1011.9\n", + "-1012.9\n", + "-1013.9\n", + "-1014.9\n", + "-1015.9\n", + "-1016.9\n", + "-1017.9\n", + "-1018.9\n", + "-1019.9\n", + "-1020.9\n", + "-1021.9\n", + "-1022.9\n", + "-1023.9\n", + "-1024.9\n", + "-1025.9\n", + "-1026.9\n", + "-1027.9\n", + "-1028.9\n", + "-1029.9\n", + "-1030.9\n", + "-1031.9\n", + "-1032.9\n", + "-1033.9\n", + "-1034.9\n", + "-1035.9\n", + "-1036.9\n", + "-1037.9\n", + "-1038.9\n", + "-1039.9\n", + "-1040.9\n", + "-1041.9\n", + "-1042.9\n", + "-1043.9\n", + "-1044.9\n", + "-1045.9\n", + "-1046.9\n", + "-1047.9\n", + "-1048.9\n", + "-1049.9\n", + "-1050.9\n", + "-1051.9\n", + "-1052.9\n", + "-1053.9\n", + "-1054.9\n", + "-1055.9\n", + "-1056.9\n", + "-1057.9\n", + "-1058.9\n", + "-1059.9\n", + "-1060.9\n", + "-1061.9\n", + "-1062.9\n", + "-1063.9\n", + "-1064.9\n", + "-1065.9\n", + "-1066.9\n", + "-1067.9\n", + "-1068.9\n", + "-1069.9\n", + "-1070.9\n", + "-1071.9\n", + "-1072.9\n", + "-1073.9\n", + "-1074.9\n", + "-1075.9\n", + "-1076.9\n", + "-1077.9\n", + "-1078.9\n", + "-1079.9\n", + "-1080.9\n", + "-1081.9\n", + "-1082.9\n", + "-1083.9\n", + "-1084.9\n", + "-1085.9\n", + "-1086.9\n", + "-1087.9\n", + "-1088.9\n", + "-1089.9\n", + "-1090.9\n", + "-1091.9\n", + "-1092.9\n", + "-1093.9\n", + "-1094.9\n", + "-1095.9\n", + "-1096.9\n", + "-1097.9\n", + "-1098.9\n", + "-1099.9\n", + "-1100.9\n", + "-1101.9\n", + "-1102.9\n", + "-1103.9\n", + "-1104.9\n", + "-1105.9\n", + "-1106.9\n", + "-1107.9\n", + "-1108.9\n", + "-1109.9\n", + "-1110.9\n", + "-1111.9\n", + "-1112.9\n", + "-1113.9\n", + "-1114.9\n", + "-1115.9\n", + "-1116.9\n", + "-1117.9\n", + "-1118.9\n", + "-1119.9\n", + "-1120.9\n", + "-1121.9\n", + "-1122.9\n", + "-1123.9\n", + "-1124.9\n", + "-1125.9\n", + "-1126.9\n", + "-1127.9\n", + "-1128.9\n", + "-1129.9\n", + "-1130.9\n", + "-1131.9\n", + "-1132.9\n", + "-1133.9\n", + "-1134.9\n", + "-1135.9\n", + "-1136.9\n", + "-1137.9\n", + "-1138.9\n", + "-1139.9\n", + "-1140.9\n", + "-1141.9\n", + "-1142.9\n", + "-1143.9\n", + "-1144.9\n", + "-1145.9\n", + "-1146.9\n", + "-1147.9\n", + "-1148.9\n", + "-1149.9\n", + "-1150.9\n", + "-1151.9\n", + "-1152.9\n", + "-1153.9\n", + "-1154.9\n", + "-1155.9\n", + "-1156.9\n", + "-1157.9\n", + "-1158.9\n", + "-1159.9\n", + "-1160.9\n", + "-1161.9\n", + "-1162.9\n", + "-1163.9\n", + "-1164.9\n", + "-1165.9\n", + "-1166.9\n", + "-1167.9\n", + "-1168.9\n", + "-1169.9\n", + "-1170.9\n", + "-1171.9\n", + "-1172.9\n", + "-1173.9\n", + "-1174.9\n", + "-1175.9\n", + "-1176.9\n", + "-1177.9\n", + "-1178.9\n", + "-1179.9\n", + "-1180.9\n", + "-1181.9\n", + "-1182.9\n", + "-1183.9\n", + "-1184.9\n", + "-1185.9\n", + "-1186.9\n", + "-1187.9\n", + "-1188.9\n", + "-1189.9\n", + "-1190.9\n", + "-1191.9\n", + "-1192.9\n", + "-1193.9\n", + "-1194.9\n", + "-1195.9\n", + "-1196.9\n", + "-1197.9\n", + "-1198.9\n", + "-1199.9\n", + "-1200.9\n", + "-1201.9\n", + "-1202.9\n", + "-1203.9\n", + "-1204.9\n", + "-1205.9\n", + "-1206.9\n", + "-1207.9\n", + "-1208.9\n", + "-1209.9\n", + "-1210.9\n", + "-1211.9\n", + "-1212.9\n", + "-1213.9\n", + "-1214.9\n", + "-1215.9\n", + "-1216.9\n", + "-1217.9\n", + "-1218.9\n", + "-1219.9\n", + "-1220.9\n", + "-1221.9\n", + "-1222.9\n", + "-1223.9\n", + "-1224.9\n", + "-1225.9\n", + "-1226.9\n", + "-1227.9\n", + "-1228.9\n", + "-1229.9\n", + "-1230.9\n", + "-1231.9\n", + "-1232.9\n", + "-1233.9\n", + "-1234.9\n", + "-1235.9\n", + "-1236.9\n", + "-1237.9\n", + "-1238.9\n", + "-1239.9\n", + "-1240.9\n", + "-1241.9\n", + "-1242.9\n", + "-1243.9\n", + "-1244.9\n", + "-1245.9\n", + "-1246.9\n", + "-1247.9\n", + "-1248.9\n", + "-1249.9\n", + "-1250.9\n", + "-1251.9\n", + "-1252.9\n", + "-1253.9\n", + "-1254.9\n", + "-1255.9\n", + "-1256.9\n", + "-1257.9\n", + "-1258.9\n", + "-1259.9\n", + "-1260.9\n", + "-1261.9\n", + "-1262.9\n", + "-1263.9\n", + "-1264.9\n", + "-1265.9\n", + "-1266.9\n", + "-1267.9\n", + "-1268.9\n", + "-1269.9\n", + "-1270.9\n", + "-1271.9\n", + "-1272.9\n", + "-1273.9\n", + "-1274.9\n", + "-1275.9\n", + "-1276.9\n", + "-1277.9\n", + "-1278.9\n", + "-1279.9\n", + "-1280.9\n", + "-1281.9\n", + "-1282.9\n", + "-1283.9\n", + "-1284.9\n", + "-1285.9\n", + "-1286.9\n", + "-1287.9\n", + "-1288.9\n", + "-1289.9\n", + "-1290.9\n", + "-1291.9\n", + "-1292.9\n", + "-1293.9\n", + "-1294.9\n", + "-1295.9\n", + "-1296.9\n", + "-1297.9\n", + "-1298.9\n", + "-1299.9\n", + "-1300.9\n", + "-1301.9\n", + "-1302.9\n", + "-1303.9\n", + "-1304.9\n", + "-1305.9\n", + "-1306.9\n", + "-1307.9\n", + "-1308.9\n", + "-1309.9\n", + "-1310.9\n", + "-1311.9\n", + "-1312.9\n", + "-1313.9\n", + "-1314.9\n", + "-1315.9\n", + "-1316.9\n", + "-1317.9\n", + "-1318.9\n", + "-1319.9\n", + "-1320.9\n", + "-1321.9\n", + "-1322.9\n", + "-1323.9\n", + "-1324.9\n", + "-1325.9\n", + "-1326.9\n", + "-1327.9\n", + "-1328.9\n", + "-1329.9\n", + "-1330.9\n", + "-1331.9\n", + "-1332.9\n", + "-1333.9\n", + "-1334.9\n", + "-1335.9\n", + "-1336.9\n", + "-1337.9\n", + "-1338.9\n", + "-1339.9\n", + "-1340.9\n", + "-1341.9\n", + "-1342.9\n", + "-1343.9\n", + "-1344.9\n", + "-1345.9\n", + "-1346.9\n", + "-1347.9\n", + "-1348.9\n", + "-1349.9\n", + "-1350.9\n", + "-1351.9\n", + "-1352.9\n", + "-1353.9\n", + "-1354.9\n", + "-1355.9\n", + "-1356.9\n", + "-1357.9\n", + "-1358.9\n", + "-1359.9\n", + "-1360.9\n", + "-1361.9\n", + "-1362.9\n", + "-1363.9\n", + "-1364.9\n", + "-1365.9\n", + "-1366.9\n", + "-1367.9\n", + "-1368.9\n", + "-1369.9\n", + "-1370.9\n", + "-1371.9\n", + "-1372.9\n", + "-1373.9\n", + "-1374.9\n", + "-1375.9\n", + "-1376.9\n", + "-1377.9\n", + "-1378.9\n", + "-1379.9\n", + "-1380.9\n", + "-1381.9\n", + "-1382.9\n", + "-1383.9\n", + "-1384.9\n", + "-1385.9\n", + "-1386.9\n", + "-1387.9\n", + "-1388.9\n", + "-1389.9\n", + "-1390.9\n", + "-1391.9\n", + "-1392.9\n", + "-1393.9\n", + "-1394.9\n", + "-1395.9\n", + "-1396.9\n", + "-1397.9\n", + "-1398.9\n", + "-1399.9\n", + "-1400.9\n", + "-1401.9\n", + "-1402.9\n", + "-1403.9\n", + "-1404.9\n", + "-1405.9\n", + "-1406.9\n", + "-1407.9\n", + "-1408.9\n", + "-1409.9\n", + "-1410.9\n", + "-1411.9\n", + "-1412.9\n", + "-1413.9\n", + "-1414.9\n", + "-1415.9\n", + "-1416.9\n", + "-1417.9\n", + "-1418.9\n", + "-1419.9\n", + "-1420.9\n", + "-1421.9\n", + "-1422.9\n", + "-1423.9\n", + "-1424.9\n", + "-1425.9\n", + "-1426.9\n", + "-1427.9\n", + "-1428.9\n", + "-1429.9\n", + "-1430.9\n", + "-1431.9\n", + "-1432.9\n", + "-1433.9\n", + "-1434.9\n", + "-1435.9\n", + "-1436.9\n", + "-1437.9\n", + "-1438.9\n", + "-1439.9\n", + "-1440.9\n", + "-1441.9\n", + "-1442.9\n", + "-1443.9\n", + "-1444.9\n", + "-1445.9\n", + "-1446.9\n", + "-1447.9\n", + "-1448.9\n", + "-1449.9\n", + "-1450.9\n", + "-1451.9\n", + "-1452.9\n", + "-1453.9\n", + "-1454.9\n", + "-1455.9\n", + "-1456.9\n", + "-1457.9\n", + "-1458.9\n", + "-1459.9\n", + "-1460.9\n", + "-1461.9\n", + "-1462.9\n", + "-1463.9\n", + "-1464.9\n", + "-1465.9\n", + "-1466.9\n", + "-1467.9\n", + "-1468.9\n", + "-1469.9\n", + "-1470.9\n", + "-1471.9\n", + "-1472.9\n", + "-1473.9\n", + "-1474.9\n", + "-1475.9\n", + "-1476.9\n", + "-1477.9\n", + "-1478.9\n", + "-1479.9\n", + "-1480.9\n", + "-1481.9\n", + "-1482.9\n", + "-1483.9\n", + "-1484.9\n", + "-1485.9\n", + "-1486.9\n", + "-1487.9\n", + "-1488.9\n", + "-1489.9\n", + "-1490.9\n", + "-1491.9\n", + "-1492.9\n", + "-1493.9\n", + "-1494.9\n", + "-1495.9\n", + "-1496.9\n", + "-1497.9\n", + "-1498.9\n", + "-1499.9\n", + "-1500.9\n", + "-1501.9\n", + "-1502.9\n", + "-1503.9\n", + "-1504.9\n", + "-1505.9\n", + "-1506.9\n", + "-1507.9\n", + "-1508.9\n", + "-1509.9\n", + "-1510.9\n", + "-1511.9\n", + "-1512.9\n", + "-1513.9\n", + "-1514.9\n", + "-1515.9\n", + "-1516.9\n", + "-1517.9\n", + "-1518.9\n", + "-1519.9\n", + "-1520.9\n", + "-1521.9\n", + "-1522.9\n", + "-1523.9\n", + "-1524.9\n", + "-1525.9\n", + "-1526.9\n", + "-1527.9\n", + "-1528.9\n", + "-1529.9\n", + "-1530.9\n", + "-1531.9\n", + "-1532.9\n", + "-1533.9\n", + "-1534.9\n", + "-1535.9\n", + "-1536.9\n", + "-1537.9\n", + "-1538.9\n", + "-1539.9\n", + "-1540.9\n", + "-1541.9\n", + "-1542.9\n", + "-1543.9\n", + "-1544.9\n", + "-1545.9\n", + "-1546.9\n", + "-1547.9\n", + "-1548.9\n", + "-1549.9\n", + "-1550.9\n", + "-1551.9\n", + "-1552.9\n", + "-1553.9\n", + "-1554.9\n", + "-1555.9\n", + "-1556.9\n", + "-1557.9\n", + "-1558.9\n", + "-1559.9\n", + "-1560.9\n", + "-1561.9\n", + "-1562.9\n", + "-1563.9\n", + "-1564.9\n", + "-1565.9\n", + "-1566.9\n", + "-1567.9\n", + "-1568.9\n", + "-1569.9\n", + "-1570.9\n", + "-1571.9\n", + "-1572.9\n", + "-1573.9\n", + "-1574.9\n", + "-1575.9\n", + "-1576.9\n", + "-1577.9\n", + "-1578.9\n", + "-1579.9\n", + "-1580.9\n", + "-1581.9\n", + "-1582.9\n", + "-1583.9\n", + "-1584.9\n", + "-1585.9\n", + "-1586.9\n", + "-1587.9\n", + "-1588.9\n", + "-1589.9\n", + "-1590.9\n", + "-1591.9\n", + "-1592.9\n", + "-1593.9\n", + "-1594.9\n", + "-1595.9\n", + "-1596.9\n", + "-1597.9\n", + "-1598.9\n", + "-1599.9\n", + "-1600.9\n", + "-1601.9\n", + "-1602.9\n", + "-1603.9\n", + "-1604.9\n", + "-1605.9\n", + "-1606.9\n", + "-1607.9\n", + "-1608.9\n", + "-1609.9\n", + "-1610.9\n", + "-1611.9\n", + "-1612.9\n", + "-1613.9\n", + "-1614.9\n", + "-1615.9\n", + "-1616.9\n", + "-1617.9\n", + "-1618.9\n", + "-1619.9\n", + "-1620.9\n", + "-1621.9\n", + "-1622.9\n", + "-1623.9\n", + "-1624.9\n", + "-1625.9\n", + "-1626.9\n", + "-1627.9\n", + "-1628.9\n", + "-1629.9\n", + "-1630.9\n", + "-1631.9\n", + "-1632.9\n", + "-1633.9\n", + "-1634.9\n", + "-1635.9\n", + "-1636.9\n", + "-1637.9\n", + "-1638.9\n", + "-1639.9\n", + "-1640.9\n", + "-1641.9\n", + "-1642.9\n", + "-1643.9\n", + "-1644.9\n", + "-1645.9\n", + "-1646.9\n", + "-1647.9\n", + "-1648.9\n", + "-1649.9\n", + "-1650.9\n", + "-1651.9\n", + "-1652.9\n", + "-1653.9\n", + "-1654.9\n", + "-1655.9\n", + "-1656.9\n", + "-1657.9\n", + "-1658.9\n", + "-1659.9\n", + "-1660.9\n", + "-1661.9\n", + "-1662.9\n", + "-1663.9\n", + "-1664.9\n", + "-1665.9\n", + "-1666.9\n", + "-1667.9\n", + "-1668.9\n", + "-1669.9\n", + "-1670.9\n", + "-1671.9\n", + "-1672.9\n", + "-1673.9\n", + "-1674.9\n", + "-1675.9\n", + "-1676.9\n", + "-1677.9\n", + "-1678.9\n", + "-1679.9\n", + "-1680.9\n", + "-1681.9\n", + "-1682.9\n", + "-1683.9\n", + "-1684.9\n", + "-1685.9\n", + "-1686.9\n", + "-1687.9\n", + "-1688.9\n", + "-1689.9\n", + "-1690.9\n", + "-1691.9\n", + "-1692.9\n", + "-1693.9\n", + "-1694.9\n", + "-1695.9\n", + "-1696.9\n", + "-1697.9\n", + "-1698.9\n", + "-1699.9\n", + "-1700.9\n", + "-1701.9\n", + "-1702.9\n", + "-1703.9\n", + "-1704.9\n", + "-1705.9\n", + "-1706.9\n", + "-1707.9\n", + "-1708.9\n", + "-1709.9\n", + "-1710.9\n", + "-1711.9\n", + "-1712.9\n", + "-1713.9\n", + "-1714.9\n", + "-1715.9\n", + "-1716.9\n", + "-1717.9\n", + "-1718.9\n", + "-1719.9\n", + "-1720.9\n", + "-1721.9\n", + "-1722.9\n", + "-1723.9\n", + "-1724.9\n", + "-1725.9\n", + "-1726.9\n", + "-1727.9\n", + "-1728.9\n", + "-1729.9\n", + "-1730.9\n", + "-1731.9\n", + "-1732.9\n", + "-1733.9\n", + "-1734.9\n", + "-1735.9\n", + "-1736.9\n", + "-1737.9\n", + "-1738.9\n", + "-1739.9\n", + "-1740.9\n", + "-1741.9\n", + "-1742.9\n", + "-1743.9\n", + "-1744.9\n", + "-1745.9\n", + "-1746.9\n", + "-1747.9\n", + "-1748.9\n", + "-1749.9\n", + "-1750.9\n", + "-1751.9\n", + "-1752.9\n", + "-1753.9\n", + "-1754.9\n", + "-1755.9\n", + "-1756.9\n", + "-1757.9\n", + "-1758.9\n", + "-1759.9\n", + "-1760.9\n", + "-1761.9\n", + "-1762.9\n", + "-1763.9\n", + "-1764.9\n", + "-1765.9\n", + "-1766.9\n", + "-1767.9\n", + "-1768.9\n", + "-1769.9\n", + "-1770.9\n", + "-1771.9\n", + "-1772.9\n", + "-1773.9\n", + "-1774.9\n", + "-1775.9\n", + "-1776.9\n", + "-1777.9\n", + "-1778.9\n", + "-1779.9\n", + "-1780.9\n", + "-1781.9\n", + "-1782.9\n", + "-1783.9\n", + "-1784.9\n", + "-1785.9\n", + "-1786.9\n", + "-1787.9\n", + "-1788.9\n", + "-1789.9\n", + "-1790.9\n", + "-1791.9\n", + "-1792.9\n", + "-1793.9\n", + "-1794.9\n", + "-1795.9\n", + "-1796.9\n", + "-1797.9\n", + "-1798.9\n", + "-1799.9\n", + "-1800.9\n", + "-1801.9\n", + "-1802.9\n", + "-1803.9\n", + "-1804.9\n", + "-1805.9\n", + "-1806.9\n", + "-1807.9\n", + "-1808.9\n", + "-1809.9\n", + "-1810.9\n", + "-1811.9\n", + "-1812.9\n", + "-1813.9\n", + "-1814.9\n", + "-1815.9\n", + "-1816.9\n", + "-1817.9\n", + "-1818.9\n", + "-1819.9\n", + "-1820.9\n", + "-1821.9\n", + "-1822.9\n", + "-1823.9\n", + "-1824.9\n", + "-1825.9\n", + "-1826.9\n", + "-1827.9\n", + "-1828.9\n", + "-1829.9\n", + "-1830.9\n", + "-1831.9\n", + "-1832.9\n", + "-1833.9\n", + "-1834.9\n", + "-1835.9\n", + "-1836.9\n", + "-1837.9\n", + "-1838.9\n", + "-1839.9\n", + "-1840.9\n", + "-1841.9\n", + "-1842.9\n", + "-1843.9\n", + "-1844.9\n", + "-1845.9\n", + "-1846.9\n", + "-1847.9\n", + "-1848.9\n", + "-1849.9\n", + "-1850.9\n", + "-1851.9\n", + "-1852.9\n", + "-1853.9\n", + "-1854.9\n", + "-1855.9\n", + "-1856.9\n", + "-1857.9\n", + "-1858.9\n", + "-1859.9\n", + "-1860.9\n", + "-1861.9\n", + "-1862.9\n", + "-1863.9\n", + "-1864.9\n", + "-1865.9\n", + "-1866.9\n", + "-1867.9\n", + "-1868.9\n", + "-1869.9\n", + "-1870.9\n", + "-1871.9\n", + "-1872.9\n", + "-1873.9\n", + "-1874.9\n", + "-1875.9\n", + "-1876.9\n", + "-1877.9\n", + "-1878.9\n", + "-1879.9\n", + "-1880.9\n", + "-1881.9\n", + "-1882.9\n", + "-1883.9\n", + "-1884.9\n", + "-1885.9\n", + "-1886.9\n", + "-1887.9\n", + "-1888.9\n", + "-1889.9\n", + "-1890.9\n", + "-1891.9\n", + "-1892.9\n", + "-1893.9\n", + "-1894.9\n", + "-1895.9\n", + "-1896.9\n", + "-1897.9\n", + "-1898.9\n", + "-1899.9\n", + "-1900.9\n", + "-1901.9\n", + "-1902.9\n", + "-1903.9\n", + "-1904.9\n", + "-1905.9\n", + "-1906.9\n", + "-1907.9\n", + "-1908.9\n", + "-1909.9\n", + "-1910.9\n", + "-1911.9\n", + "-1912.9\n", + "-1913.9\n", + "-1914.9\n", + "-1915.9\n", + "-1916.9\n", + "-1917.9\n", + "-1918.9\n", + "-1919.9\n", + "-1920.9\n", + "-1921.9\n", + "-1922.9\n", + "-1923.9\n", + "-1924.9\n", + "-1925.9\n", + "-1926.9\n", + "-1927.9\n", + "-1928.9\n", + "-1929.9\n", + "-1930.9\n", + "-1931.9\n", + "-1932.9\n", + "-1933.9\n", + "-1934.9\n", + "-1935.9\n", + "-1936.9\n", + "-1937.9\n", + "-1938.9\n", + "-1939.9\n", + "-1940.9\n", + "-1941.9\n", + "-1942.9\n", + "-1943.9\n", + "-1944.9\n", + "-1945.9\n", + "-1946.9\n", + "-1947.9\n", + "-1948.9\n", + "-1949.9\n", + "-1950.9\n", + "-1951.9\n", + "-1952.9\n", + "-1953.9\n", + "-1954.9\n", + "-1955.9\n", + "-1956.9\n", + "-1957.9\n", + "-1958.9\n", + "-1959.9\n", + "-1960.9\n", + "-1961.9\n", + "-1962.9\n", + "-1963.9\n", + "-1964.9\n", + "-1965.9\n", + "-1966.9\n", + "-1967.9\n", + "-1968.9\n", + "-1969.9\n", + "-1970.9\n", + "-1971.9\n", + "-1972.9\n", + "-1973.9\n", + "-1974.9\n", + "-1975.9\n", + "-1976.9\n", + "-1977.9\n", + "-1978.9\n", + "-1979.9\n", + "-1980.9\n", + "-1981.9\n", + "-1982.9\n", + "-1983.9\n", + "-1984.9\n", + "-1985.9\n", + "-1986.9\n", + "-1987.9\n", + "-1988.9\n", + "-1989.9\n", + "-1990.9\n", + "-1991.9\n", + "-1992.9\n", + "-1993.9\n", + "-1994.9\n", + "-1995.9\n", + "-1996.9\n", + "-1997.9\n", + "-1998.9\n", + "-1999.9\n", + "-2000.9\n", + "-2001.9\n", + "-2002.9\n", + "-2003.9\n", + "-2004.9\n", + "-2005.9\n", + "-2006.9\n", + "-2007.9\n", + "-2008.9\n", + "-2009.9\n", + "-2010.9\n", + "-2011.9\n", + "-2012.9\n", + "-2013.9\n", + "-2014.9\n", + "-2015.9\n", + "-2016.9\n", + "-2017.9\n", + "-2018.9\n", + "-2019.9\n", + "-2020.9\n", + "-2021.9\n", + "-2022.9\n", + "-2023.9\n", + "-2024.9\n", + "-2025.9\n", + "-2026.9\n", + "-2027.9\n", + "-2028.9\n", + "-2029.9\n", + "-2030.9\n", + "-2031.9\n", + "-2032.9\n", + "-2033.9\n", + "-2034.9\n", + "-2035.9\n", + "-2036.9\n", + "-2037.9\n", + "-2038.9\n", + "-2039.9\n", + "-2040.9\n", + "-2041.9\n", + "-2042.9\n", + "-2043.9\n", + "-2044.9\n", + "-2045.9\n", + "-2046.9\n", + "-2047.9\n", + "-2048.9\n", + "-2049.9\n", + "-2050.9\n", + "-2051.9\n", + "-2052.9\n", + "-2053.9\n", + "-2054.9\n", + "-2055.9\n", + "-2056.9\n", + "-2057.9\n", + "-2058.9\n", + "-2059.9\n", + "-2060.9\n", + "-2061.9\n", + "-2062.9\n", + "-2063.9\n", + "-2064.9\n", + "-2065.9\n", + "-2066.9\n", + "-2067.9\n", + "-2068.9\n", + "-2069.9\n", + "-2070.9\n", + "-2071.9\n", + "-2072.9\n", + "-2073.9\n", + "-2074.9\n", + "-2075.9\n", + "-2076.9\n", + "-2077.9\n", + "-2078.9\n", + "-2079.9\n", + "-2080.9\n", + "-2081.9\n", + "-2082.9\n", + "-2083.9\n", + "-2084.9\n", + "-2085.9\n", + "-2086.9\n", + "-2087.9\n", + "-2088.9\n", + "-2089.9\n", + "-2090.9\n", + "-2091.9\n", + "-2092.9\n", + "-2093.9\n", + "-2094.9\n", + "-2095.9\n", + "-2096.9\n", + "-2097.9\n", + "-2098.9\n", + "-2099.9\n", + "-2100.9\n", + "-2101.9\n", + "-2102.9\n", + "-2103.9\n", + "-2104.9\n", + "-2105.9\n", + "-2106.9\n", + "-2107.9\n", + "-2108.9\n", + "-2109.9\n", + "-2110.9\n", + "-2111.9\n", + "-2112.9\n", + "-2113.9\n", + "-2114.9\n", + "-2115.9\n", + "-2116.9\n", + "-2117.9\n", + "-2118.9\n", + "-2119.9\n", + "-2120.9\n", + "-2121.9\n", + "-2122.9\n", + "-2123.9\n", + "-2124.9\n", + "-2125.9\n", + "-2126.9\n", + "-2127.9\n", + "-2128.9\n", + "-2129.9\n", + "-2130.9\n", + "-2131.9\n", + "-2132.9\n", + "-2133.9\n", + "-2134.9\n", + "-2135.9\n", + "-2136.9\n", + "-2137.9\n", + "-2138.9\n", + "-2139.9\n", + "-2140.9\n", + "-2141.9\n", + "-2142.9\n", + "-2143.9\n", + "-2144.9\n", + "-2145.9\n", + "-2146.9\n", + "-2147.9\n", + "-2148.9\n", + "-2149.9\n", + "-2150.9\n", + "-2151.9\n", + "-2152.9\n", + "-2153.9\n", + "-2154.9\n", + "-2155.9\n", + "-2156.9\n", + "-2157.9\n", + "-2158.9\n", + "-2159.9\n", + "-2160.9\n", + "-2161.9\n", + "-2162.9\n", + "-2163.9\n", + "-2164.9\n", + "-2165.9\n", + "-2166.9\n", + "-2167.9\n", + "-2168.9\n", + "-2169.9\n", + "-2170.9\n", + "-2171.9\n", + "-2172.9\n", + "-2173.9\n", + "-2174.9\n", + "-2175.9\n", + "-2176.9\n", + "-2177.9\n", + "-2178.9\n", + "-2179.9\n", + "-2180.9\n", + "-2181.9\n", + "-2182.9\n", + "-2183.9\n", + "-2184.9\n", + "-2185.9\n", + "-2186.9\n", + "-2187.9\n", + "-2188.9\n", + "-2189.9\n", + "-2190.9\n", + "-2191.9\n", + "-2192.9\n", + "-2193.9\n", + "-2194.9\n", + "-2195.9\n", + "-2196.9\n", + "-2197.9\n", + "-2198.9\n", + "-2199.9\n", + "-2200.9\n", + "-2201.9\n", + "-2202.9\n", + "-2203.9\n", + "-2204.9\n", + "-2205.9\n", + "-2206.9\n", + "-2207.9\n", + "-2208.9\n", + "-2209.9\n", + "-2210.9\n", + "-2211.9\n", + "-2212.9\n", + "-2213.9\n", + "-2214.9\n", + "-2215.9\n", + "-2216.9\n", + "-2217.9\n", + "-2218.9\n", + "-2219.9\n", + "-2220.9\n", + "-2221.9\n", + "-2222.9\n", + "-2223.9\n", + "-2224.9\n", + "-2225.9\n", + "-2226.9\n", + "-2227.9\n", + "-2228.9\n", + "-2229.9\n", + "-2230.9\n", + "-2231.9\n", + "-2232.9\n", + "-2233.9\n", + "-2234.9\n", + "-2235.9\n", + "-2236.9\n", + "-2237.9\n", + "-2238.9\n", + "-2239.9\n", + "-2240.9\n", + "-2241.9\n", + "-2242.9\n", + "-2243.9\n", + "-2244.9\n", + "-2245.9\n", + "-2246.9\n", + "-2247.9\n", + "-2248.9\n", + "-2249.9\n", + "-2250.9\n", + "-2251.9\n", + "-2252.9\n", + "-2253.9\n", + "-2254.9\n", + "-2255.9\n", + "-2256.9\n", + "-2257.9\n", + "-2258.9\n", + "-2259.9\n", + "-2260.9\n", + "-2261.9\n", + "-2262.9\n", + "-2263.9\n", + "-2264.9\n", + "-2265.9\n", + "-2266.9\n", + "-2267.9\n", + "-2268.9\n", + "-2269.9\n", + "-2270.9\n", + "-2271.9\n", + "-2272.9\n", + "-2273.9\n", + "-2274.9\n", + "-2275.9\n", + "-2276.9\n", + "-2277.9\n", + "-2278.9\n", + "-2279.9\n", + "-2280.9\n", + "-2281.9\n", + "-2282.9\n", + "-2283.9\n", + "-2284.9\n", + "-2285.9\n", + "-2286.9\n", + "-2287.9\n", + "-2288.9\n", + "-2289.9\n", + "-2290.9\n", + "-2291.9\n", + "-2292.9\n", + "-2293.9\n", + "-2294.9\n", + "-2295.9\n", + "-2296.9\n", + "-2297.9\n", + "-2298.9\n", + "-2299.9\n", + "-2300.9\n", + "-2301.9\n", + "-2302.9\n", + "-2303.9\n", + "-2304.9\n", + "-2305.9\n", + "-2306.9\n", + "-2307.9\n", + "-2308.9\n", + "-2309.9\n", + "-2310.9\n", + "-2311.9\n", + "-2312.9\n", + "-2313.9\n", + "-2314.9\n", + "-2315.9\n", + "-2316.9\n", + "-2317.9\n", + "-2318.9\n", + "-2319.9\n", + "-2320.9\n", + "-2321.9\n", + "-2322.9\n", + "-2323.9\n", + "-2324.9\n", + "-2325.9\n", + "-2326.9\n", + "-2327.9\n", + "-2328.9\n", + "-2329.9\n", + "-2330.9\n", + "-2331.9\n", + "-2332.9\n", + "-2333.9\n", + "-2334.9\n", + "-2335.9\n", + "-2336.9\n", + "-2337.9\n", + "-2338.9\n", + "-2339.9\n", + "-2340.9\n", + "-2341.9\n", + "-2342.9\n", + "-2343.9\n", + "-2344.9\n", + "-2345.9\n", + "-2346.9\n", + "-2347.9\n", + "-2348.9\n", + "-2349.9\n", + "-2350.9\n", + "-2351.9\n", + "-2352.9\n", + "-2353.9\n", + "-2354.9\n", + "-2355.9\n", + "-2356.9\n", + "-2357.9\n", + "-2358.9\n", + "-2359.9\n", + "-2360.9\n", + "-2361.9\n", + "-2362.9\n", + "-2363.9\n", + "-2364.9\n", + "-2365.9\n", + "-2366.9\n", + "-2367.9\n", + "-2368.9\n", + "-2369.9\n", + "-2370.9\n", + "-2371.9\n", + "-2372.9\n", + "-2373.9\n", + "-2374.9\n", + "-2375.9\n", + "-2376.9\n", + "-2377.9\n", + "-2378.9\n", + "-2379.9\n", + "-2380.9\n", + "-2381.9\n", + "-2382.9\n", + "-2383.9\n", + "-2384.9\n", + "-2385.9\n", + "-2386.9\n", + "-2387.9\n", + "-2388.9\n", + "-2389.9\n", + "-2390.9\n", + "-2391.9\n", + "-2392.9\n", + "-2393.9\n", + "-2394.9\n", + "-2395.9\n", + "-2396.9\n", + "-2397.9\n", + "-2398.9\n", + "-2399.9\n", + "-2400.9\n", + "-2401.9\n", + "-2402.9\n", + "-2403.9\n", + "-2404.9\n", + "-2405.9\n", + "-2406.9\n", + "-2407.9\n", + "-2408.9\n", + "-2409.9\n", + "-2410.9\n", + "-2411.9\n", + "-2412.9\n", + "-2413.9\n", + "-2414.9\n", + "-2415.9\n", + "-2416.9\n", + "-2417.9\n", + "-2418.9\n", + "-2419.9\n", + "-2420.9\n", + "-2421.9\n", + "-2422.9\n", + "-2423.9\n", + "-2424.9\n", + "-2425.9\n", + "-2426.9\n", + "-2427.9\n", + "-2428.9\n", + "-2429.9\n", + "-2430.9\n", + "-2431.9\n", + "-2432.9\n", + "-2433.9\n", + "-2434.9\n", + "-2435.9\n", + "-2436.9\n", + "-2437.9\n", + "-2438.9\n", + "-2439.9\n", + "-2440.9\n", + "-2441.9\n", + "-2442.9\n", + "-2443.9\n", + "-2444.9\n", + "-2445.9\n", + "-2446.9\n", + "-2447.9\n", + "-2448.9\n", + "-2449.9\n", + "-2450.9\n", + "-2451.9\n", + "-2452.9\n", + "-2453.9\n", + "-2454.9\n", + "-2455.9\n", + "-2456.9\n", + "-2457.9\n", + "-2458.9\n", + "-2459.9\n", + "-2460.9\n", + "-2461.9\n", + "-2462.9\n", + "-2463.9\n", + "-2464.9\n", + "-2465.9\n", + "-2466.9\n", + "-2467.9\n", + "-2468.9\n", + "-2469.9\n", + "-2470.9\n", + "-2471.9\n", + "-2472.9\n", + "-2473.9\n", + "-2474.9\n", + "-2475.9\n", + "-2476.9\n", + "-2477.9\n", + "-2478.9\n", + "-2479.9\n", + "-2480.9\n", + "-2481.9\n", + "-2482.9\n", + "-2483.9\n", + "-2484.9\n", + "-2485.9\n", + "-2486.9\n", + "-2487.9\n", + "-2488.9\n", + "-2489.9\n", + "-2490.9\n", + "-2491.9\n", + "-2492.9\n", + "-2493.9\n", + "-2494.9\n", + "-2495.9\n", + "-2496.9\n", + "-2497.9\n", + "-2498.9\n", + "-2499.9\n", + "-2500.9\n", + "-2501.9\n", + "-2502.9\n", + "-2503.9\n", + "-2504.9\n", + "-2505.9\n", + "-2506.9\n", + "-2507.9\n", + "-2508.9\n", + "-2509.9\n", + "-2510.9\n", + "-2511.9\n", + "-2512.9\n", + "-2513.9\n", + "-2514.9\n", + "-2515.9\n", + "-2516.9\n", + "-2517.9\n", + "-2518.9\n", + "-2519.9\n", + "-2520.9\n", + "-2521.9\n", + "-2522.9\n", + "-2523.9\n", + "-2524.9\n", + "-2525.9\n", + "-2526.9\n", + "-2527.9\n", + "-2528.9\n", + "-2529.9\n", + "-2530.9\n", + "-2531.9\n", + "-2532.9\n", + "-2533.9\n", + "-2534.9\n", + "-2535.9\n", + "-2536.9\n", + "-2537.9\n", + "-2538.9\n", + "-2539.9\n", + "-2540.9\n", + "-2541.9\n", + "-2542.9\n", + "-2543.9\n", + "-2544.9\n", + "-2545.9\n", + "-2546.9\n", + "-2547.9\n", + "-2548.9\n", + "-2549.9\n", + "-2550.9\n", + "-2551.9\n", + "-2552.9\n", + "-2553.9\n", + "-2554.9\n", + "-2555.9\n", + "-2556.9\n", + "-2557.9\n", + "-2558.9\n", + "-2559.9\n", + "-2560.9\n", + "-2561.9\n", + "-2562.9\n", + "-2563.9\n", + "-2564.9\n", + "-2565.9\n", + "-2566.9\n", + "-2567.9\n", + "-2568.9\n", + "-2569.9\n", + "-2570.9\n", + "-2571.9\n", + "-2572.9\n", + "-2573.9\n", + "-2574.9\n", + "-2575.9\n", + "-2576.9\n", + "-2577.9\n", + "-2578.9\n", + "-2579.9\n", + "-2580.9\n", + "-2581.9\n", + "-2582.9\n", + "-2583.9\n", + "-2584.9\n", + "-2585.9\n", + "-2586.9\n", + "-2587.9\n", + "-2588.9\n", + "-2589.9\n", + "-2590.9\n", + "-2591.9\n", + "-2592.9\n", + "-2593.9\n", + "-2594.9\n", + "-2595.9\n", + "-2596.9\n", + "-2597.9\n", + "-2598.9\n", + "-2599.9\n", + "-2600.9\n", + "-2601.9\n", + "-2602.9\n", + "-2603.9\n", + "-2604.9\n", + "-2605.9\n", + "-2606.9\n", + "-2607.9\n", + "-2608.9\n", + "-2609.9\n", + "-2610.9\n", + "-2611.9\n", + "-2612.9\n", + "-2613.9\n", + "-2614.9\n", + "-2615.9\n", + "-2616.9\n", + "-2617.9\n", + "-2618.9\n", + "-2619.9\n", + "-2620.9\n", + "-2621.9\n", + "-2622.9\n", + "-2623.9\n", + "-2624.9\n", + "-2625.9\n", + "-2626.9\n", + "-2627.9\n", + "-2628.9\n", + "-2629.9\n", + "-2630.9\n", + "-2631.9\n", + "-2632.9\n", + "-2633.9\n", + "-2634.9\n", + "-2635.9\n", + "-2636.9\n", + "-2637.9\n", + "-2638.9\n", + "-2639.9\n", + "-2640.9\n", + "-2641.9\n", + "-2642.9\n", + "-2643.9\n", + "-2644.9\n", + "-2645.9\n", + "-2646.9\n", + "-2647.9\n", + "-2648.9\n", + "-2649.9\n", + "-2650.9\n", + "-2651.9\n", + "-2652.9\n", + "-2653.9\n", + "-2654.9\n", + "-2655.9\n", + "-2656.9\n", + "-2657.9\n", + "-2658.9\n", + "-2659.9\n", + "-2660.9\n", + "-2661.9\n", + "-2662.9\n", + "-2663.9\n", + "-2664.9\n", + "-2665.9\n", + "-2666.9\n", + "-2667.9\n", + "-2668.9\n", + "-2669.9\n", + "-2670.9\n", + "-2671.9\n", + "-2672.9\n", + "-2673.9\n", + "-2674.9\n", + "-2675.9\n", + "-2676.9\n", + "-2677.9\n", + "-2678.9\n", + "-2679.9\n", + "-2680.9\n", + "-2681.9\n", + "-2682.9\n", + "-2683.9\n", + "-2684.9\n", + "-2685.9\n", + "-2686.9\n", + "-2687.9\n", + "-2688.9\n", + "-2689.9\n", + "-2690.9\n", + "-2691.9\n", + "-2692.9\n", + "-2693.9\n", + "-2694.9\n", + "-2695.9\n", + "-2696.9\n", + "-2697.9\n", + "-2698.9\n", + "-2699.9\n", + "-2700.9\n", + "-2701.9\n", + "-2702.9\n", + "-2703.9\n", + "-2704.9\n", + "-2705.9\n", + "-2706.9\n", + "-2707.9\n", + "-2708.9\n", + "-2709.9\n", + "-2710.9\n", + "-2711.9\n", + "-2712.9\n", + "-2713.9\n", + "-2714.9\n", + "-2715.9\n", + "-2716.9\n", + "-2717.9\n", + "-2718.9\n", + "-2719.9\n", + "-2720.9\n", + "-2721.9\n", + "-2722.9\n", + "-2723.9\n", + "-2724.9\n", + "-2725.9\n", + "-2726.9\n", + "-2727.9\n", + "-2728.9\n", + "-2729.9\n", + "-2730.9\n", + "-2731.9\n", + "-2732.9\n", + "-2733.9\n", + "-2734.9\n", + "-2735.9\n", + "-2736.9\n", + "-2737.9\n", + "-2738.9\n", + "-2739.9\n", + "-2740.9\n", + "-2741.9\n", + "-2742.9\n", + "-2743.9\n", + "-2744.9\n", + "-2745.9\n", + "-2746.9\n", + "-2747.9\n", + "-2748.9\n", + "-2749.9\n", + "-2750.9\n", + "-2751.9\n", + "-2752.9\n", + "-2753.9\n", + "-2754.9\n", + "-2755.9\n", + "-2756.9\n", + "-2757.9\n", + "-2758.9\n", + "-2759.9\n", + "-2760.9\n", + "-2761.9\n", + "-2762.9\n", + "-2763.9\n", + "-2764.9\n", + "-2765.9\n", + "-2766.9\n", + "-2767.9\n", + "-2768.9\n", + "-2769.9\n", + "-2770.9\n", + "-2771.9\n", + "-2772.9\n", + "-2773.9\n", + "-2774.9\n", + "-2775.9\n", + "-2776.9\n", + "-2777.9\n", + "-2778.9\n", + "-2779.9\n", + "-2780.9\n", + "-2781.9\n", + "-2782.9\n", + "-2783.9\n", + "-2784.9\n", + "-2785.9\n", + "-2786.9\n", + "-2787.9\n", + "-2788.9\n", + "-2789.9\n", + "-2790.9\n", + "-2791.9\n", + "-2792.9\n", + "-2793.9\n", + "-2794.9\n", + "-2795.9\n", + "-2796.9\n", + "-2797.9\n", + "-2798.9\n", + "-2799.9\n", + "-2800.9\n", + "-2801.9\n", + "-2802.9\n", + "-2803.9\n", + "-2804.9\n", + "-2805.9\n", + "-2806.9\n", + "-2807.9\n", + "-2808.9\n", + "-2809.9\n", + "-2810.9\n", + "-2811.9\n", + "-2812.9\n", + "-2813.9\n", + "-2814.9\n", + "-2815.9\n", + "-2816.9\n", + "-2817.9\n", + "-2818.9\n", + "-2819.9\n", + "-2820.9\n", + "-2821.9\n", + "-2822.9\n", + "-2823.9\n", + "-2824.9\n", + "-2825.9\n", + "-2826.9\n", + "-2827.9\n", + "-2828.9\n", + "-2829.9\n", + "-2830.9\n", + "-2831.9\n", + "-2832.9\n", + "-2833.9\n", + "-2834.9\n", + "-2835.9\n", + "-2836.9\n", + "-2837.9\n", + "-2838.9\n", + "-2839.9\n", + "-2840.9\n", + "-2841.9\n", + "-2842.9\n", + "-2843.9\n", + "-2844.9\n", + "-2845.9\n", + "-2846.9\n", + "-2847.9\n", + "-2848.9\n", + "-2849.9\n", + "-2850.9\n", + "-2851.9\n", + "-2852.9\n", + "-2853.9\n", + "-2854.9\n", + "-2855.9\n", + "-2856.9\n", + "-2857.9\n", + "-2858.9\n", + "-2859.9\n", + "-2860.9\n", + "-2861.9\n", + "-2862.9\n", + "-2863.9\n", + "-2864.9\n", + "-2865.9\n", + "-2866.9\n", + "-2867.9\n", + "-2868.9\n", + "-2869.9\n", + "-2870.9\n", + "-2871.9\n", + "-2872.9\n", + "-2873.9\n", + "-2874.9\n", + "-2875.9\n", + "-2876.9\n", + "-2877.9\n", + "-2878.9\n", + "-2879.9\n", + "-2880.9\n", + "-2881.9\n", + "-2882.9\n", + "-2883.9\n", + "-2884.9\n", + "-2885.9\n", + "-2886.9\n", + "-2887.9\n", + "-2888.9\n", + "-2889.9\n", + "-2890.9\n", + "-2891.9\n", + "-2892.9\n", + "-2893.9\n", + "-2894.9\n", + "-2895.9\n", + "-2896.9\n", + "-2897.9\n", + "-2898.9\n", + "-2899.9\n", + "-2900.9\n", + "-2901.9\n", + "-2902.9\n", + "-2903.9\n", + "-2904.9\n", + "-2905.9\n", + "-2906.9\n", + "-2907.9\n", + "-2908.9\n", + "-2909.9\n", + "-2910.9\n", + "-2911.9\n", + "-2912.9\n", + "-2913.9\n", + "-2914.9\n", + "-2915.9\n", + "-2916.9\n", + "-2917.9\n", + "-2918.9\n", + "-2919.9\n", + "-2920.9\n", + "-2921.9\n", + "-2922.9\n", + "-2923.9\n", + "-2924.9\n", + "-2925.9\n", + "-2926.9\n", + "-2927.9\n", + "-2928.9\n", + "-2929.9\n", + "-2930.9\n", + "-2931.9\n", + "-2932.9\n", + "-2933.9\n", + "-2934.9\n", + "-2935.9\n", + "-2936.9\n", + "-2937.9\n", + "-2938.9\n", + "-2939.9\n", + "-2940.9\n", + "-2941.9\n", + "-2942.9\n", + "-2943.9\n", + "-2944.9\n", + "-2945.9\n", + "-2946.9\n", + "-2947.9\n", + "-2948.9\n", + "-2949.9\n", + "-2950.9\n", + "-2951.9\n", + "-2952.9\n", + "-2953.9\n", + "-2954.9\n", + "-2955.9\n", + "-2956.9\n", + "-2957.9\n", + "-2958.9\n", + "-2959.9\n", + "-2960.9\n", + "-2961.9\n", + "-2962.9\n", + "-2963.9\n", + "-2964.9\n", + "-2965.9\n", + "-2966.9\n", + "-2967.9\n", + "-2968.9\n", + "-2969.9\n", + "-2970.9\n" + ] + }, + { + "ename": "RecursionError", + "evalue": "maximum recursion depth exceeded", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[29], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mcountdown\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[1], line 11\u001b[0m, in \u001b[0;36mcountdown\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(n)\n\u001b[0;32m---> 11\u001b[0m \u001b[43mcountdown\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[1], line 11\u001b[0m, in \u001b[0;36mcountdown\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(n)\n\u001b[0;32m---> 11\u001b[0m \u001b[43mcountdown\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + " \u001b[0;31m[... skipping similar frames: countdown at line 11 (2972 times)]\u001b[0m\n", + "Cell \u001b[0;32mIn[1], line 11\u001b[0m, in \u001b[0;36mcountdown\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28mprint\u001b[39m(n)\n\u001b[0;32m---> 11\u001b[0m \u001b[43mcountdown\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[1], line 10\u001b[0m, in \u001b[0;36mcountdown\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mHappy New Year!\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 10\u001b[0m \u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 11\u001b[0m countdown(n \u001b[38;5;241m-\u001b[39m \u001b[38;5;241m1\u001b[39m)\n", + "File \u001b[0;32m~/Repositories/intro-to-python/.venv/lib/python3.12/site-packages/ipykernel/iostream.py:664\u001b[0m, in \u001b[0;36mOutStream.write\u001b[0;34m(self, string)\u001b[0m\n\u001b[1;32m 655\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrite\u001b[39m(\u001b[38;5;28mself\u001b[39m, string: \u001b[38;5;28mstr\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Optional[\u001b[38;5;28mint\u001b[39m]: \u001b[38;5;66;03m# type:ignore[override]\u001b[39;00m\n\u001b[1;32m 656\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Write to current stream after encoding if necessary\u001b[39;00m\n\u001b[1;32m 657\u001b[0m \n\u001b[1;32m 658\u001b[0m \u001b[38;5;124;03m Returns\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 662\u001b[0m \n\u001b[1;32m 663\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 664\u001b[0m parent \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mparent_header\u001b[49m\n\u001b[1;32m 666\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(string, \u001b[38;5;28mstr\u001b[39m):\n\u001b[1;32m 667\u001b[0m msg \u001b[38;5;241m=\u001b[39m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwrite() argument must be str, not \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(string)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;66;03m# type:ignore[unreachable]\u001b[39;00m\n", + "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded" + ] + } + ], + "source": [ + "countdown(3.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the same way, a `RecursionError` occurs if we call `factorial()` with `3.1` instead of `3`." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "RecursionError", + "evalue": "maximum recursion depth exceeded", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[30], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[6], line 12\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m---> 12\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[6], line 12\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m---> 12\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + " \u001b[0;31m[... skipping similar frames: factorial at line 12 (2974 times)]\u001b[0m\n", + "Cell \u001b[0;32mIn[6], line 12\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m---> 12\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m n \u001b[38;5;241m*\u001b[39m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded" + ] + } + ], + "source": [ + "factorial(3.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The infinite recursions could easily be avoided by replacing `n == 0` with `n <= 0` in both functions and thereby **generalizing** them. However, even then, calling either `countdown()` or `factorial()` with a non-integer number is still *semantically* wrong.\n", + "\n", + "Errors as above are a symptom of missing **type checking**: By design, Python allows us to pass in not only integers but objects of any type as arguments to the `countdown()` and `factorial()` functions. As long as the arguments \"behave\" like integers, we do not encounter any *runtime* errors. This is the case here as the two example functions only use the `-` and `*` operators internally, and, in the context of arithmetic, a `float` object behaves like an `int` object. So, the functions keep calling themselves until Python decides with a built-in heuristic that the recursion is likely not going to end and aborts the computations with a `RecursionError`. Strictly speaking, a `RecursionError` is, of course, a *runtime* error as well." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Duck Typing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The missing type checking is *100% intentional* and considered a **[feature of rather than a bug](https://www.urbandictionary.com/define.php?term=It%27s%20not%20a%20bug%2C%20it%27s%20a%20feature)** in Python!\n", + "\n", + "Pythonistas use the \"technical\" term **[duck typing ](https://en.wikipedia.org/wiki/Duck_typing)** to express the idea of two objects of *different* types behaving in the *same* way in a given context. The colloquial saying goes, \"If it walks like a duck and it quacks like a duck, it must be a duck.\"\n", + "\n", + "For example, we could call `factorial()` with the `float` object `3.0`, and the recursion works out fine. So, because the `3.0` \"walks\" and \"quacks\" like a `3`, it \"must be\" a `3`." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6.0" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We see similar behavior when we mix objects of types `int` and `float` with arithmetic operators. For example, `1 + 2.0` works because Python implicitly views the `1` as a `1.0` at runtime and then knows how to do floating-point arithmetic: Here, the `int` \"walks\" and \"quacks\" like a `float`." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 + 2.0" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1.0 + 2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The important lesson is that we must expect our functions to be called with objects of *any* type at runtime, as opposed to the one type we had in mind when defining the function.\n", + "\n", + "Duck typing is possible because Python is a dynamically typed language. On the contrary, in statically typed languages like C, we *must* declare (i.e., \"specify\") the data type of every parameter in a function definition. Then, a `RecursionError` as for `countdown(3.1)` or `factorial(3.1)` above could not occur. For example, if we declared the `countdown()` and `factorial()` functions to only accept `int` objects, calling the functions with a `float` argument would immediately fail *syntactically*. As a downside, we would then lose the ability to call `factorial()` with `3.0`, which is *semantically* correct nevertheless.\n", + "\n", + "So, there is no black or white answer as to which of the two language designs is better. Yet, most professional programmers have strong opinions concerning duck typing, reaching from \"love\" to \"hate.\" This is another example of how programming is a subjective art rather than \"objective\" science. Python's design is probably more appealing to beginners who intuitively regard `3` and `3.0` as interchangeable." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Type Checking & Input Validation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We use the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function to make sure `factorial()` is called with an `int` object as the argument. We further **validate** the **input** by verifying that the integer is non-negative.\n", + "\n", + "Meanwhile, we also see how we manually raise exceptions with the `raise` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)), another way of controlling the flow of execution.\n", + "\n", + "The first two branches in the revised `factorial()` function act as **guardians** ensuring that the code does not produce *unexpected* runtime errors: Errors may be expected when mentioned in the docstring.\n", + "\n", + "So, in essence, we are doing *two* things here: Besides checking the type, we also enforce **domain-specific** (i.e., mathematical here) rules concerning the non-negativity of `n`." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for; must be positive\n", + "\n", + " Returns:\n", + " factorial (int)\n", + "\n", + " Raises:\n", + " TypeError: if n is not an integer\n", + " ValueError: if n is negative\n", + " \"\"\"\n", + " if not isinstance(n, int):\n", + " raise TypeError(\"Factorial is only defined for integers\")\n", + " elif n < 0:\n", + " raise ValueError(\"Factorial is not defined for negative integers\")\n", + " elif n == 0:\n", + " return 1\n", + " return n * factorial(n - 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The revised `factorial()` function works like the old one." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Instead of running into a situation of infinite recursion, we now receive specific error messages." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Factorial is only defined for integers", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[38], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[35], line 15\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Calculate the factorial of a number.\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \n\u001b[1;32m 4\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;124;03m ValueError: if n is negative\u001b[39;00m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(n, \u001b[38;5;28mint\u001b[39m):\n\u001b[0;32m---> 15\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is only defined for integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m n \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is not defined for negative integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mTypeError\u001b[0m: Factorial is only defined for integers" + ] + } + ], + "source": [ + "factorial(3.1)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Factorial is not defined for negative integers", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[39], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m42\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[35], line 17\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is only defined for integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m n \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m---> 17\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is not defined for negative integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m n \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;241m1\u001b[39m\n", + "\u001b[0;31mValueError\u001b[0m: Factorial is not defined for negative integers" + ] + } + ], + "source": [ + "factorial(-42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Forcing `n` to be an `int` is a very puritan way of handling the issues discussed above. So, we can *not* call `factorial()` with, for example, `3.0`." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Factorial is only defined for integers", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[40], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.0\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[35], line 15\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Calculate the factorial of a number.\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \n\u001b[1;32m 4\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;124;03m ValueError: if n is negative\u001b[39;00m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(n, \u001b[38;5;28mint\u001b[39m):\n\u001b[0;32m---> 15\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is only defined for integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m n \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is not defined for negative integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mTypeError\u001b[0m: Factorial is only defined for integers" + ] + } + ], + "source": [ + "factorial(3.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Type Casting" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A similar way to prevent an infinite recursion is to **cast** the **type** of the `n` argument with the built-in [int() ](https://docs.python.org/3/library/functions.html#int) constructor." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for; must be positive\n", + "\n", + " Returns:\n", + " factorial (int)\n", + "\n", + " Raises:\n", + " TypeError: if n cannot be cast as an integer\n", + " ValueError: if n is negative\n", + " \"\"\"\n", + " n = int(n)\n", + " if n < 0:\n", + " raise ValueError(\"Factorial is not defined for negative integers\")\n", + " elif n == 0:\n", + " return 1\n", + " return n * factorial(n - 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The not so strict type casting implements duck typing for `factorial()` as, for example, `3.0` \"walks\" and \"quacks\" like a `3`." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, if we now call `factorial()` with a non-integer `float` object like `3.1`, *no* error is raised. This is a potential source for *semantic* errors as the function runs for invalid input." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We could adjust the type casting logic such that a `TypeError` is raised for `n` arguments with non-zero decimals. " + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for; must be positive\n", + "\n", + " Returns:\n", + " factorial (int)\n", + "\n", + " Raises:\n", + " TypeError: if n cannot be cast as an integer\n", + " ValueError: if n is negative\n", + " \"\"\"\n", + " if n != int(n):\n", + " raise TypeError(\"n is not integer-like; it has non-zero decimals\")\n", + " n = int(n)\n", + "\n", + " if n < 0:\n", + " raise ValueError(\"Factorial is not defined for negative integers\")\n", + " elif n == 0:\n", + " return 1\n", + " return n * factorial(n - 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "n is not integer-like; it has non-zero decimals", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[47], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[45], line 15\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Calculate the factorial of a number.\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \n\u001b[1;32m 4\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;124;03m ValueError: if n is negative\u001b[39;00m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mint\u001b[39m(n):\n\u001b[0;32m---> 15\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mn is not integer-like; it has non-zero decimals\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 16\u001b[0m n \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(n)\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n", + "\u001b[0;31mTypeError\u001b[0m: n is not integer-like; it has non-zero decimals" + ] + } + ], + "source": [ + "factorial(3.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, using built-in constructors for type casting leads to another subtle inconsistency. As constructors are designed to take *any* object as their argument, they do not raise a `TypeError` when called with invalid input but a `ValueError` instead. So, if we, for example, called `factorial()` with `\"text\"` as the `n` argument, we see the `ValueError` raised by [int() ](https://docs.python.org/3/library/functions.html#int) in a situation where a `TypeError` would be more appropriate." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "invalid literal for int() with base 10: 'text'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[48], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtext\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[45], line 14\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mfactorial\u001b[39m(n):\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Calculate the factorial of a number.\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \n\u001b[1;32m 4\u001b[0m \u001b[38;5;124;03m Args:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;124;03m ValueError: if n is negative\u001b[39;00m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m---> 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28;43mint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mn is not integer-like; it has non-zero decimals\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 16\u001b[0m n \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(n)\n", + "\u001b[0;31mValueError\u001b[0m: invalid literal for int() with base 10: 'text'" + ] + } + ], + "source": [ + "factorial(\"text\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We could, of course, use a `try` statement to suppress the exceptions raised by [int() ](https://docs.python.org/3/library/functions.html#int) and replace them with a custom `TypeError`. However, now the implementation as a whole is more about type checking than about the actual logic solving the problem. We took this example to the extreme on purpose. In practice, we rarely see such code!" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for; must be positive\n", + "\n", + " Returns:\n", + " factorial (int)\n", + "\n", + " Raises:\n", + " TypeError: if n cannot be cast as an integer\n", + " ValueError: if n is negative\n", + " \"\"\"\n", + " try:\n", + " casted_n = int(n)\n", + " except ValueError:\n", + " raise TypeError(\"n cannot be casted as an integer\") from None\n", + " else:\n", + " if n != casted_n:\n", + " raise TypeError(\"n is not integer-like; it has non-zero decimals\")\n", + " n = casted_n\n", + "\n", + " if n < 0:\n", + " raise ValueError(\"Factorial is not defined for negative integers\")\n", + " elif n == 0:\n", + " return 1\n", + " return n * factorial(n - 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "n cannot be casted as an integer", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[50], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mtext\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[49], line 17\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 15\u001b[0m casted_n \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(n)\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m:\n\u001b[0;32m---> 17\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mn cannot be casted as an integer\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m!=\u001b[39m casted_n:\n", + "\u001b[0;31mTypeError\u001b[0m: n cannot be casted as an integer" + ] + } + ], + "source": [ + "factorial(\"text\")" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "n is not integer-like; it has non-zero decimals", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[51], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[49], line 20\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n)\u001b[0m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m!=\u001b[39m casted_n:\n\u001b[0;32m---> 20\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mn is not integer-like; it has non-zero decimals\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 21\u001b[0m n \u001b[38;5;241m=\u001b[39m casted_n\n\u001b[1;32m 23\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n", + "\u001b[0;31mTypeError\u001b[0m: n is not integer-like; it has non-zero decimals" + ] + } + ], + "source": [ + "factorial(3.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Which way we choose for **hardening** the `factorial()` function depends on the concrete circumstances. If we are the main caller of the function ourselves, we may choose to *not* do any of the approaches at all. After all, we should be able to call our own function in the correct way. The lesson is that just because Python has no static typing, this does *not* mean that we cannot do this \"manually.\" Yet, the idea behind a dynamically typed language is to *not* deal with the types too much at all." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Theory of Computation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With everything *officially* introduced so far, Python would be what is called **[Turing complete ](https://en.wikipedia.org/wiki/Turing_completeness)**. That means that anything that could be formulated as an algorithm could be expressed with all the language features we have seen. Note that, in particular, we have *not* yet formally *introduced* the `for` and `while` statements!" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/04_iteration/01_exercises_hanoi-towers.ipynb b/04_iteration/01_exercises_hanoi-towers.ipynb new file mode 100644 index 0000000..b29212e --- /dev/null +++ b/04_iteration/01_exercises_hanoi-towers.ipynb @@ -0,0 +1,610 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 4: Recursion & Looping (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb) of Chapter 4.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Towers of Hanoi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A popular example of a problem that is solved by recursion art the **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)**.\n", + "\n", + "In its basic version, a tower consisting of, for example, four disks with increasing radii, is placed on the left-most of **three** adjacent spots. In the following, we refer to the number of disks as $n$, so here $n = 4$.\n", + "\n", + "The task is to move the entire tower to the right-most spot whereby **two rules** must be obeyed:\n", + "\n", + "1. Disks can only be moved individually, and\n", + "2. a disk with a larger radius must *never* be placed on a disk with a smaller one.\n", + "\n", + "Although the **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** are a **classic** example, introduced by the mathematician [Édouard Lucas ](https://en.wikipedia.org/wiki/%C3%89douard_Lucas) already in 1883, it is still **actively** researched as this scholarly [article](https://www.worldscientific.com/doi/abs/10.1142/S1793830919300017?journalCode=dmaa&) published in January 2019 shows.\n", + "\n", + "Despite being so easy to formulate, the game is quite hard to solve.\n", + "\n", + "Below is an interactive illustration of the solution with the minimal number of moves for $n = 4$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Watch the following video by [MIT](https://www.mit.edu/)'s professor [Richard Larson](https://idss.mit.edu/staff/richard-larson/) for a comprehensive introduction.\n", + "\n", + "The [MIT Blossoms Initiative](https://blossoms.mit.edu/) is primarily aimed at high school students and does not have any prerequisites.\n", + "\n", + "The video consists of three segments, the last of which is *not* necessary to have watched to solve the tasks below. So, watch the video until 37:55." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"UuIneNBbscc\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Video Review Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Explain for the $n = 3$ case why it can be solved as a **recursion**!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: How does the number of minimal moves needed to solve a problem with three spots and $n$ disks grow as a function of $n$? How does this relate to the answer to **Q1**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: The **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** problem is of **exponential growth**. What does that mean? What does that imply for large $n$?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: The video introduces the recursive relationship $Sol(4, 1, 3) = Sol(3, 1, 2) ~ \\bigoplus ~ Sol(1, 1, 3) ~ \\bigoplus ~ Sol(3, 2, 3)$. The $\\bigoplus$ is to be interpreted as some sort of \"plus\" operation. How does this \"plus\" operation work? How does this way of expressing the problem relate to the answer to **Q1**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Naive Translation to Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As most likely the first couple of tries will result in *semantic* errors, it is advisable to have some sort of **visualization tool** for the program's output: For example, an online version of the game can be found [here](https://www.mathsisfun.com/games/towerofhanoi.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first **generalize** the mathematical relationship from above and then introduce the variable names used in our `sol()` implementation below.\n", + "\n", + "Unsurprisingly, the recursive relationship in the video may be generalized into:\n", + "\n", + "$Sol(n, o, d) = Sol(n-1, o, i) ~ \\bigoplus ~ Sol(1, o, d) ~ \\bigoplus ~ Sol(n-1, i, d)$\n", + "\n", + "$Sol(\\cdot)$ takes three \"arguments\" $n$, $o$, and $d$ and is defined with *three* references to itself that take modified versions of $n$, $o$, and $d$ in different orders. The middle reference, Sol(1, o, d), constitutes the \"end\" of the recursive definition: It is the problem of solving Towers of Hanoi for a \"tower\" of only one disk.\n", + "\n", + "While the first \"argument\" of $Sol(\\cdot)$ is a number that we refer to as `n_disks` below, the second and third \"arguments\" are merely **labels** for the spots, and we refer to the **roles** they take in a given problem as `origin` and `destination` below. Instead of labeling individual spots with the numbers `1`, `2`, and `3` as in the video, we may also call them `\"left\"`, `\"center\"`, and `\"right\"`. Both ways are equally correct! So, only the first \"argument\" of $Sol(\\cdot)$ is really a number!\n", + "\n", + "As an example, the notation $Sol(4, 1, 3)$ from above can then be \"translated\" into Python as either the function call `sol(4, 1, 3)` or `sol(4, \"left\", \"right\")`. This describes the problem of moving a tower consisting of `n_disks=4` disks from either the `origin=1` spot to the `destination=3` spot or from the `origin=\"left\"` spot to the `destination=\"right\"` spot.\n", + "\n", + "To adhere to the rules, an `intermediate` spot $i$ is needed. In `sol()` below, this is a temporary variable within a function call and *not* a parameter of the function itself." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In summary, to move a tower consisting of `n_disks` (= $n$) disks from an `origin` (= $o$) to a `destination` (= $d$), three steps must be executed:\n", + "\n", + "1. Move the tower's topmost `n_disks - 1` (= $n - 1$) disks from the `origin` (= $o$) to an `intermediate` (= $i$) spot (= **Sub-Problem 1**),\n", + "2. move the remaining and largest disk from the `origin` (= $o$) to the `destination` (= $d$), and\n", + "3. move the `n_disks - 1` (= $n - 1$) disks from the `intermediate` (= $i$) spot to the `destination` (= $d$) spot (= **Sub-Problem 2**).\n", + "\n", + "The two sub-problems themselves are solved via the same *recursive* logic." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Write your answers to **Q5** to **Q7** into the skeleton of `sol()` below.\n", + "\n", + "`sol()` takes three arguments `n_disks`, `origin`, and `destination` that mirror $n$, $o$, and $d$ above.\n", + "\n", + "For now, assume that all arguments to `sol()` are `int` objects! We generalize this into real labels further below in the `hanoi()` function.\n", + "\n", + "Once completed, `sol()` should *print* out all the moves in the correct order. For example, *print* `\"1 -> 3\"` to mean \"Move the top-most `n_disks - 1` disks from spot `1` to spot `3`.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def sol(n_disks, origin, destination):\n", + " \"\"\"A naive implementation of Towers of Hanoi.\n", + "\n", + " This function prints out the moves to solve a Towers of Hanoi problem.\n", + "\n", + " Args:\n", + " n_disks (int): number of disks in the tower\n", + " origin (int): spot of the tower at the start; 1, 2, or 3\n", + " destination (int): spot of the tower at the end; 1, 2, or 3\n", + " \"\"\"\n", + " # answer to Q5\n", + " ...\n", + " ...\n", + "\n", + " # answer to Q6\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " # answer to Q7\n", + " ...\n", + " ...\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What is the `n_disks` argument when the function reaches its **base case**? Check for the base case with a simple `if` statement and return from the function using the **early exit** pattern!\n", + "\n", + "Hint: The base case in the Python implementation may be slightly different than the one shown in the generalized mathematical relationship above!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: If not in the base case, `sol()` determines the `intermediate` spot given concrete `origin` and `destination` arguments. For example, if called with `origin=1` and `destination=2`, `intermediate` must be `3`.\n", + "\n", + "Add *one* compound `if` statement to `sol()` that has a branch for *every* possible `origin`-`destination`-pair that assigns the correct temporary spot to a variable `intermediate`.\n", + "\n", + "Hint: How many 2-tuples of 3 elements can there be if the order matters?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: `sol()` calls itself *two* more times with the correct 2-tuples chosen from the three available spots `origin`, `intermediate`, and `destination`.\n", + "\n", + "*In between* the two recursive function calls, use [print() ](https://docs.python.org/3/library/functions.html#print) to print out from where to where the \"remaining and largest\" disk has to be moved!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Execute the code cells below and confirm that the moves are correct!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sol(1, 1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sol(2, 1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sol(3, 1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sol(4, 1, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pythonic Refactoring" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The previous `sol()` implementation does the job, but the conditional statement is unnecessarily tedious. \n", + "\n", + "Let's create a concise `hanoi()` function that, in addition to a positional `n_disks` argument, takes three keyword-only arguments `origin`, `intermediate`, and `destination` with default values `\"left\"`, `\"center\"`, and `\"right\"`.\n", + "\n", + "Write your answers to **Q9** and **Q10** into the subsequent code cell and finalize `hanoi()`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def hanoi(n_disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\"):\n", + " \"\"\"A Pythonic implementation of Towers of Hanoi.\n", + "\n", + " This function prints out the moves to solve a Towers of Hanoi problem.\n", + "\n", + " Args:\n", + " n_disks (int): number of disks in the tower\n", + " origin (str, optional): label for the spot of the tower at the start\n", + " intermediate (str, optional): label for the intermediate spot\n", + " destination (str, optional): label for the spot of the tower at the end\n", + " \"\"\"\n", + " # answer to Q9\n", + " ...\n", + " ...\n", + "\n", + " # answer to Q10\n", + " ...\n", + " ...\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Copy the base case from `sol()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: Instead of conditional logic, `hanoi()` calls itself *two* times with the *three* arguments `origin`, `intermediate`, and `destination` passed on in a *different* order.\n", + "\n", + "Figure out how the arguments are passed on in the two recursive `hanoi()` calls and finish `hanoi()`.\n", + "\n", + "Hint: Do not forget to use [print() ](https://docs.python.org/3/library/functions.html#print) to print out the moves!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Execute the code cells below and confirm that the moves are correct!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We could, of course, also use *numeric* labels for the three steps like so." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi(3, origin=1, intermediate=2, destination=3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Passing a Value \"down\" the Recursion Tree" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above `hanoi()` prints the optimal solution's moves in the correct order but fails to label each move with an order number. This is built in the `hanoi_ordered()` function below by passing on a \"private\" `_offset` argument \"down\" the recursion tree. The leading underscore `_` in the parameter name indicates that it is *not* to be used by the caller of the function. That is also why the parameter is *not* mentioned in the docstring.\n", + "\n", + "Write your answers to **Q12** and **Q13** into the subsequent code cell and finalize `hanoi_ordered()`! As the logic gets a bit \"involved,\" `hanoi_ordered()` below is almost finished." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def hanoi_ordered(n_disks, *, origin=\"left\", intermediate=\"center\", destination=\"right\", _offset=None):\n", + " \"\"\"A Pythonic implementation of Towers of Hanoi.\n", + "\n", + " This function prints out the moves to solve a Towers of Hanoi problem.\n", + " Each move is labeled with an order number.\n", + "\n", + " Args:\n", + " n_disks (int): number of disks in the tower\n", + " origin (str, optional): label for the spot of the tower at the start\n", + " intermediate (str, optional): label for the intermediate spot\n", + " destination (str, optional): label for the spot of the tower at the end\n", + " \"\"\"\n", + " # answer to Q12\n", + " ...\n", + " ...\n", + "\n", + " total = (2 ** n_disks - 1)\n", + " half = (2 ** (n_disks - 1) - 1)\n", + " count = total - half\n", + "\n", + " if _offset is not None:\n", + " count += _offset\n", + "\n", + " # answer to Q18\n", + " hanoi_ordered(..., _offset=_offset)\n", + " ...\n", + " hanoi_ordered(..., _offset=count)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Copy the base case from the original `hanoi()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: Complete the two recursive function calls with the same arguments as in `hanoi()`! Do *not* change the already filled in `offset` arguments!\n", + "\n", + "Then, adjust the use of [print() ](https://docs.python.org/3/library/functions.html#print) from above to print out the moves with their order number!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Execute the code cells below and confirm that the order numbers are correct!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi_ordered(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi_ordered(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi_ordered(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hanoi_ordered(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, it is to be mentioned that for problem instances with a small `n_disks` argument, it is easier to collect all the moves first in a `list` object and then add the order number with the [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) built-in." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Open Question" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q15**: Conducting your own research on the internet, what can you say about generalizing the **[Towers of Hanoi ](https://en.wikipedia.org/wiki/Tower_of_Hanoi)** problem to a setting with *more than three* landing spots?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/04_iteration/02_content.ipynb b/04_iteration/02_content.ipynb new file mode 100644 index 0000000..1d41ed7 --- /dev/null +++ b/04_iteration/02_content.ipynb @@ -0,0 +1,1300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/02_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 4: Recursion & Looping (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After learning about the concept of **recursion** in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb) of this chapter, we look at other ways of running code repeatedly, namely **looping** with the `for` and `while` statements. We start with the latter as it is more generic. Throughout this second part of the chapter, we revisit the same examples from the first part to show how recursion and looping are really two sides of the same coin." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `while` Statement" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whereas functions combined with `if` statements suffice to model any repetitive logic, Python comes with a compound `while` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#the-while-statement)) that often makes it easier to implement iterative ideas.\n", + "\n", + "It consists of a header line with a boolean expression followed by an indented code block. Before the first and after every execution of the code block, the boolean expression is evaluated, and if it is (still) equal to `True`, the code block runs (again). Eventually, some variable referenced in the boolean expression is changed in the code block such that the condition becomes `False`.\n", + "\n", + "If the condition is `False` before the first iteration, the entire code block is *never* executed. As the flow of control keeps \"looping\" (i.e., more formally, **iterating**) back to the beginning of the code block, this concept is also called a `while`-loop and each pass through the loop an **iteration**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Trivial Example: Countdown (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's rewrite the `countdown()` example in an iterative style. We also build in **input validation** by allowing the function only to be called with strictly positive integers. As any positive integer hits $0$ at some point when iteratively decremented by $1$, `countdown()` is guaranteed to **terminate**. Also, the base case is now handled at the end of the function, which commonly happens with iterative solutions to problems." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def countdown(n):\n", + " \"\"\"Print a countdown until the party starts.\n", + "\n", + " Args:\n", + " n (int): seconds until the party begins; must be positive\n", + " \"\"\"\n", + " while n != 0:\n", + " print(n)\n", + " n -= 1\n", + "\n", + " print(\"Happy new Year!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n", + "2\n", + "1\n", + "Happy new Year!\n" + ] + } + ], + "source": [ + "countdown(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As [PythonTutor ](http://pythontutor.com/visualize.html#code=def%20countdown%28n%29%3A%0A%20%20%20%20while%20n%20!%3D%200%3A%0A%20%20%20%20%20%20%20%20print%28n%29%0A%20%20%20%20%20%20%20%20n%20-%3D%201%0A%0A%20%20%20%20print%28%22Happy%20new%20Year!%22%29%0A%0Acountdown%283%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows, there is a subtle but essential difference in the way a `while` statement is treated in memory: In short, `while` statements can *not* run into a `RecursionError` as only *one* frame is needed to manage the names. After all, there is only *one* function call to be made. For typical day-to-day applications, this difference is, however, not so important *unless* a problem instance becomes so big that a large (i.e., $> 3.000$) number of recursive calls must be made." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### \"Still involved\" Example: [Euclid's Algorithm ](https://en.wikipedia.org/wiki/Euclidean_algorithm) (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Finding the greatest common divisor of two numbers is still not so obvious when using a `while`-loop instead of a recursive formulation.\n", + "\n", + "The iterative implementation of `gcd()` below accepts any two strictly positive integers. As in any iteration through the loop, the smaller number is subtracted from the larger one, the two decremented values of `a` and `b` eventually become equal. Thus, this algorithm is also guaranteed to terminate. If one of the two numbers were negative or $0$ in the first place, `gcd()` would run forever, and not even Python could detect this. Try this out by removing the input validation and running the function with negative arguments!" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def gcd(a, b):\n", + " \"\"\"Calculate the greatest common divisor of two numbers.\n", + "\n", + " Args:\n", + " a (int): first number; must be positive\n", + " b (int): second number; must be positive\n", + "\n", + " Returns:\n", + " gcd (int)\n", + " \"\"\"\n", + " while a != b:\n", + " if a > b:\n", + " a -= b\n", + " else:\n", + " b -= a\n", + "\n", + " return a" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gcd(12, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gcd(7, 7919)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Efficiency of Algorithms (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We also see that this implementation is a lot *less* efficient than its recursive counterpart which solves `gcd()` for the same two numbers `112233445566778899` and `987654321` within microseconds." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5.32 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1 -r 1\n", + "gcd(112233445566778899, 987654321)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Infinite Loops" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with recursion, we must ensure that the iteration ends. For the above `countdown()` and `gcd()` examples, we could \"prove\" (i.e., at least argue in favor) that some pre-defined **termination criterion** is reached eventually. However, this cannot be done in all cases, as the following example shows." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### \"Mystery\" Example: [Collatz Conjecture ](https://en.wikipedia.org/wiki/Collatz_conjecture)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Let's play the following game:\n", + "- Think of any positive integer $n$.\n", + "- If $n$ is even, the next $n$ is half the old $n$.\n", + "- If $n$ is odd, multiply the old $n$ by $3$ and add $1$ to obtain the next $n$.\n", + "- Repeat these steps until you reach $1$.\n", + "\n", + "**Do we always reach the final $1$?**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The function below implements this game. Does it always reach $1$? No one has proven it so far! We include some input validation as before because `collatz()` would for sure not terminate if we called it with a negative number. Further, the Collatz sequence also works for real numbers, but then we would have to study fractals (cf., [this ](https://en.wikipedia.org/wiki/Collatz_conjecture#Iterating_on_real_or_complex_numbers)). So we restrict our example to integers only." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def collatz(n):\n", + " \"\"\"Print a Collatz sequence in descending order.\n", + "\n", + " Given a positive integer n, modify it according to these rules:\n", + " - if n is even, the next n is half the previous one\n", + " - if n is odd, the next n is 3 times the previous one plus 1\n", + " - if n is 1, stop the iteration\n", + "\n", + " Args:\n", + " n (int): a positive number to start the Collatz sequence at\n", + " \"\"\"\n", + " while n != 1:\n", + " print(n, end=\" \")\n", + " if n % 2 == 0:\n", + " n //= 2 # //= to preserve the int type\n", + " else:\n", + " n = 3 * n + 1\n", + "\n", + " print(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Collatz sequences do not necessarily become longer with a larger initial `n`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100 50 25 76 38 19 58 29 88 44 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1\n" + ] + } + ], + "source": [ + "collatz(100)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1000 500 250 125 376 188 94 47 142 71 214 107 322 161 484 242 121 364 182 91 274 137 412 206 103 310 155 466 233 700 350 175 526 263 790 395 1186 593 1780 890 445 1336 668 334 167 502 251 754 377 1132 566 283 850 425 1276 638 319 958 479 1438 719 2158 1079 3238 1619 4858 2429 7288 3644 1822 911 2734 1367 4102 2051 6154 3077 9232 4616 2308 1154 577 1732 866 433 1300 650 325 976 488 244 122 61 184 92 46 23 70 35 106 53 160 80 40 20 10 5 16 8 4 2 1\n" + ] + } + ], + "source": [ + "collatz(1000)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10000 5000 2500 1250 625 1876 938 469 1408 704 352 176 88 44 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1\n" + ] + } + ], + "source": [ + "collatz(10000)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100000 50000 25000 12500 6250 3125 9376 4688 2344 1172 586 293 880 440 220 110 55 166 83 250 125 376 188 94 47 142 71 214 107 322 161 484 242 121 364 182 91 274 137 412 206 103 310 155 466 233 700 350 175 526 263 790 395 1186 593 1780 890 445 1336 668 334 167 502 251 754 377 1132 566 283 850 425 1276 638 319 958 479 1438 719 2158 1079 3238 1619 4858 2429 7288 3644 1822 911 2734 1367 4102 2051 6154 3077 9232 4616 2308 1154 577 1732 866 433 1300 650 325 976 488 244 122 61 184 92 46 23 70 35 106 53 160 80 40 20 10 5 16 8 4 2 1\n" + ] + } + ], + "source": [ + "collatz(100000)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `for` Statement" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Recursion and the `while` statement are two sides of the same coin. Disregarding that in the case of recursion Python internally faces some additional burden for managing the stack of frames in memory, both approaches lead to the *same* computational steps in memory. More importantly, we can formulate any recursive implementation in an iterative way and vice versa despite one of the two ways often \"feeling\" a lot more natural given a particular problem.\n", + "\n", + "So how does the compound `for` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#the-for-statement)) in this book's very first example fit into this picture? It is a *redundant* language construct to provide a *shorter* and more *convenient* syntax for common applications of the `while` statement. In programming, such additions to a language are called **syntactic sugar**. A cup of tea tastes better with sugar, but we may drink tea without sugar too.\n", + "\n", + "Consider `elements` below. Without the `for` statement, we must manage a temporary **index variable**, `index`, to loop over all the elements and also obtain the individual elements with the `[]` operator in each iteration of the loop." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "elements = [0, 1, 2, 3, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 " + ] + } + ], + "source": [ + "index = 0\n", + "\n", + "while index < len(elements):\n", + " element = elements[index]\n", + " print(element, end=\" \")\n", + " index += 1\n", + "\n", + "del index" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `for` statement, on the contrary, makes the actual business logic more apparent by stripping all the **[boilerplate code ](https://en.wikipedia.org/wiki/Boilerplate_code)** away. The variable that is automatically set by Python in each iteration of the loop (i.e., `element` in the example) is called the **target variable**." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 " + ] + } + ], + "source": [ + "for element in elements:\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For sequences of integers, the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in makes the `for` statement even more convenient: It creates a `list`-like object of type `range` that generates integers \"on the fly,\" and we look closely at the underlying effects in memory in [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb#Mapping)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 " + ] + } + ], + "source": [ + "for element in range(5):\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "range" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(range(5))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[range() ](https://docs.python.org/3/library/functions.html#func-range) takes optional `start` and `step` arguments that we use to customize the sequence of integers even more." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 3 5 7 9 " + ] + } + ], + "source": [ + "for element in [1, 3, 5, 7, 9]:\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 3 5 7 9 " + ] + } + ], + "source": [ + "for element in range(1, 10, 2):\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Containers vs. Iterables" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The essential difference between the above `list` objects, `[0, 1, 2, 3, 4]` and `[1, 3, 5, 7, 9]`, and the `range` objects, `range(5)` and `range(1, 10, 2)`, is that in the former case *six* objects are created in memory *before* the `for` statement starts running, *one* `list` holding references to *five* `int` objects, whereas in the latter case only *one* `range` object is created that **generates** `int` objects one at a time *while* the `for`-loop runs.\n", + "\n", + "However, we can loop over both of them. So a natural question to ask is why Python treats objects of *different* types in the *same* way when used with a `for` statement.\n", + "\n", + "So far, the overarching storyline in this book goes like this: In Python, *everything* is an object. Besides its *identity* and *value*, every object is characterized by \"belonging\" to *one* data type that determines how the object behaves and what we may do with it.\n", + "\n", + "Now, just as we classify objects by data type, we also classify these data types (e.g., `int`, `float`, `str`, or `list`) into **abstract concepts**.\n", + "\n", + "We did this already in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Who-am-I?-And-how-many?) when we described a `list` object as \"some sort of container that holds [...] references to other objects\". So, abstractly speaking, **containers** are any objects that are \"composed\" of other objects and also \"manage\" how these objects are organized. `list` objects, for example, have the property that they model an order associated with their elements. There exist, however, other container types, many of which do *not* come with an order. So, containers primarily \"contain\" other objects and have *nothing* to do with looping.\n", + "\n", + "On the contrary, the abstract concept of **iterables** is all about looping: Any object that we can loop over is, by definition, an iterable. So, `range` objects, for example, are iterables, even though they hold no references to other objects. Moreover, looping does *not* have to occur in a *predictable* order, although this is the case for both `list` and `range` objects.\n", + "\n", + "Typically, containers are iterables, and iterables are containers. Yet, only because these two concepts coincide often, we must not think of them as the same. In [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Collections-vs.-Sequences), we formalize these two concepts and introduce many more. Finally, [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) gives an explanation how abstract concepts are implemented and play together.\n", + "\n", + "Let's continue with `first_names` below as an example an illustrate what iterable containers are." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "first_names = [\"Achim\", \"Berthold\", \"Carl\", \"Diedrich\", \"Eckardt\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The characteristic operator associated with container types is the `in` operator: It checks if a given object evaluates equal to at least one of the objects in the container. Colloquially, it checks if an object is \"contained\" in the container. Formally, this operation is called **membership testing**." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Achim\" in first_names" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Alexander\" in first_names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The cell below shows the *exact* workings of the `in` operator: Although `3.0` is *not* contained in `elements`, it evaluates equal to the `3` that is, which is why the following expression evaluates to `True`. So, while we could colloquially say that `elements` \"contains\" `3.0`, it actually does not." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 1, 2, 3, 4]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "elements" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "3.0 in elements" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, the characteristic operation of an iterable type is that it supports being looped over, for example, with the `for` statement." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Achim Berthold Carl Diedrich Eckardt " + ] + } + ], + "source": [ + "for name in first_names:\n", + " print(name, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we must have an index variable in the loop's body, we use the [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) built-in that takes an *iterable* as its argument and then generates a \"stream\" of \"pairs\" of an index variable, `i` below, and an object provided by the iterable, `name`, separated by a `,`. There is *no* need to ever revert to the `while` statement with an explicitly managed index variable to loop over an iterable object." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 > Achim 2 > Berthold 3 > Carl 4 > Diedrich 5 > Eckardt " + ] + } + ], + "source": [ + "for i, name in enumerate(first_names, start=1):\n", + " print(i, \">\", name, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) takes an optional `start` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 > Achim 2 > Berthold 3 > Carl 4 > Diedrich 5 > Eckardt " + ] + } + ], + "source": [ + "for i, name in enumerate(first_names, start=1):\n", + " print(i, \">\", name, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [zip() ](https://docs.python.org/3/library/functions.html#zip) built-in allows us to combine the elements of two or more iterables in a *pairwise* fashion: It conceptually works like a zipper for a jacket." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "last_names = [\"Müller\", \"Meyer\", \"Mayer\", \"Schmitt\", \"Schmidt\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Achim Müller Berthold Meyer Carl Mayer Diedrich Schmitt Eckardt Schmidt " + ] + } + ], + "source": [ + "for first_name, last_name in zip(first_names, last_names):\n", + " print(first_name, last_name, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### \"Hard at first Glance\" Example: [Fibonacci Numbers ](https://en.wikipedia.org/wiki/Fibonacci_number) (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In contrast to its recursive counterpart, the iterative `fibonacci()` function below is somewhat harder to read. For example, it is not so obvious as to how many iterations through the `for`-loop we need to make when implementing it. There is an increased risk of making an *off-by-one* error. Moreover, we need to track a `temp` variable along.\n", + "\n", + "However, one advantage of calculating Fibonacci numbers in a **forward** fashion with a `for` statement is that we could list the entire sequence in ascending order as we calculate the desired number. To show this, we added `print()` statements in `fibonacci()` below.\n", + "\n", + "We do *not* need to store the index variable in the `for`-loop's header line: That is what the underscore variable `_` indicates; we \"throw it away.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def fibonacci(i):\n", + " \"\"\"Calculate the ith Fibonacci number.\n", + "\n", + " Args:\n", + " i (int): index of the Fibonacci number to calculate\n", + "\n", + " Returns:\n", + " ith_fibonacci (int)\n", + " \"\"\"\n", + " a = 0\n", + " b = 1\n", + " print(a, b, sep=\" \", end=\" \") # added for didactical purposes\n", + " for _ in range(i - 1):\n", + " temp = a + b\n", + " a = b\n", + " b = temp\n", + " print(b, end=\" \") # added for didactical purposes\n", + "\n", + " return b" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 1 2 3 5 8 13 21 34 55 89 144 " + ] + }, + { + "data": { + "text/plain": [ + "144" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12) # = 13th number" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "##### Efficiency of Algorithms (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another more important advantage is that now we may calculate even big Fibonacci numbers *efficiently*." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 14930352 24157817 39088169 63245986 102334155 165580141 267914296 433494437 701408733 1134903170 1836311903 2971215073 4807526976 7778742049 12586269025 20365011074 32951280099 53316291173 86267571272 139583862445 225851433717 365435296162 591286729879 956722026041 1548008755920 2504730781961 4052739537881 6557470319842 10610209857723 17167680177565 27777890035288 44945570212853 72723460248141 117669030460994 190392490709135 308061521170129 498454011879264 806515533049393 1304969544928657 2111485077978050 3416454622906707 5527939700884757 8944394323791464 14472334024676221 23416728348467685 37889062373143906 61305790721611591 99194853094755497 160500643816367088 259695496911122585 420196140727489673 679891637638612258 1100087778366101931 1779979416004714189 2880067194370816120 4660046610375530309 7540113804746346429 12200160415121876738 19740274219868223167 31940434634990099905 51680708854858323072 83621143489848422977 135301852344706746049 218922995834555169026 " + ] + }, + { + "data": { + "text/plain": [ + "218922995834555169026" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(99) # = 100th number" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Easy Example: [Factorial ](https://en.wikipedia.org/wiki/Factorial) (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The iterative `factorial()` implementation is comparable to its recursive counterpart when it comes to readability. One advantage of calculating the factorial in a forward fashion is that we could track the intermediate `product` as it grows." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def factorial(n):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for, must be positive\n", + "\n", + " Returns:\n", + " factorial (int)\n", + " \"\"\"\n", + " product = 1 # because 0! = 1\n", + " for i in range(1, n + 1):\n", + " product *= i\n", + " print(product, end=\" \") # added for didactical purposes\n", + "\n", + " return product" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2 6 " + ] + }, + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2 6 24 120 720 5040 40320 362880 3628800 " + ] + }, + { + "data": { + "text/plain": [ + "3628800" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(10)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/04_iteration/03_content.ipynb b/04_iteration/03_content.ipynb new file mode 100644 index 0000000..0eb9d25 --- /dev/null +++ b/04_iteration/03_content.ipynb @@ -0,0 +1,991 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/03_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 4: Recursion & Looping (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While what we learned about the `for` and `while` statements in the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb) of this chapter suffices to translate any iterative algorithm into code, both come with some syntactic sugar to make life easier for the developer. This last part of the chapter shows how we can further customize the looping logic and introduces as \"trick\" for situations where we cannot come up with a stopping criterion in a `while`-loop." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Stopping Loops prematurely" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This section introduces additional syntax to customize `for` and `while` statements in our code even further. They are mostly syntactic sugar in that they do not change how a program runs but make its code more readable. We illustrate them for the `for` statement only. However, everything presented in this section also works for the `while` statement." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Is the square of a number in `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]` greater than `100`?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's say we have a list of `numbers` and want to check if the square of at least one of its elements is greater than `100`. So, conceptually, we are asking the question if a list of numbers as a whole satisfies a certain condition." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A first naive implementation could look like this: We loop over *every* element in `numbers` and set an **indicator variable** `is_above`, initialized as `False`, to `True` once we encounter an element satisfying the condition.\n", + "\n", + "This implementation is *inefficient* as even if the *first* element in `numbers` has a square greater than `100`, we loop until the last element: This could take a long time for a big list.\n", + "\n", + "Moreover, we must initialize `is_above` *before* the `for`-loop and write an `if`-`else`-logic *after* it to check for the result. The actual business logic is *not* conveyed in a clear way." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 8 5 3 12 2 6 9 10 1 4 => at least one number satisfies the condition\n" + ] + } + ], + "source": [ + "is_above = False\n", + "\n", + "for number in numbers:\n", + " print(number, end=\" \") # added for didactical purposes\n", + " if number ** 2 > 100:\n", + " is_above = True\n", + "\n", + "if is_above:\n", + " print(\"=> at least one number satisfies the condition\")\n", + "else:\n", + " print(\"=> no number satisfies the condition\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### The `break` Statement" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python provides the `break` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-break-statement)) that lets us stop a loop prematurely at any iteration. It is yet another means of controlling the flow of execution, and we say that we \"break out of a loop.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 => at least one number satisfies the condition\n" + ] + } + ], + "source": [ + "is_above = False\n", + "\n", + "for number in numbers:\n", + " print(number, end=\" \") # added for didactical purposes\n", + " if number ** 2 > 100:\n", + " is_above = True\n", + " break\n", + "\n", + "if is_above:\n", + " print(\"=> at least one number satisfies the condition\")\n", + "else:\n", + " print(\"=> no number satisfies the condition\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This is a computational improvement. However, the code still consists of *three* sections: Some initialization *before* the `for`-loop, the loop itself, and some finalizing logic. We prefer to convey the program's idea in *one* compound statement instead." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### The `else`-clause" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To express the logic in a prettier way, we add an `else`-clause at the end of the `for`-loop (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#the-for-statement)). The `else`-clause is executed *only if* the `for`-loop is *not* stopped with a `break` statement *prematurely* (i.e., *before* reaching the *last* iteration in the loop). The word \"else\" implies a somewhat unintuitive meaning and may have better been named a `then`-clause. In most use cases, however, the `else`-clause logically goes together with some `if` statement in the loop's body.\n", + "\n", + "Overall, the code's expressive power increases. Not many programming languages support an optional `else`-branching for the `for` and `while` statements, which turns out to be very useful in practice." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 => at least one number satisfies the condition\n" + ] + } + ], + "source": [ + "for number in numbers:\n", + " print(number, end=\" \") # added for didactical purposes\n", + " if number ** 2 > 100:\n", + " is_above = True\n", + " break\n", + "else:\n", + " is_above = False\n", + "\n", + "if is_above:\n", + " print(\"=> at least one number satisfies the condition\")\n", + "else:\n", + " print(\"=> no number satisfies the condition\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, we incorporate the finalizing `if`-`else` logic into the `for`-loop, avoiding the `is_above` variable altogether." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 => at least one number satisfies the condition\n" + ] + } + ], + "source": [ + "for number in numbers:\n", + " print(number, end=\" \") # added for didactical purposes\n", + " if number ** 2 > 100:\n", + " print(\"=> at least one number satisfies the condition\")\n", + " break\n", + "else:\n", + " print(\"=> no number satisfies the condition\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, if we choose the number an element's square has to pass to be larger, for example, to `200`, we have to loop over all `numbers`. There is *no way* to optimize this **[linear search ](https://en.wikipedia.org/wiki/Linear_search)** further." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 8 5 3 12 2 6 9 10 1 4 => no number satisfies the condition\n" + ] + } + ], + "source": [ + "for number in numbers:\n", + " print(number, end=\" \") # added for didactical purposes\n", + " if number ** 2 > 200:\n", + " print(\"=> at least one number satisfies the condition\")\n", + " break\n", + "else:\n", + " print(\"=> no number satisfies the condition\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## A first Glance at the **Map-Filter-Reduce** Paradigm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Often, we process some iterable with numeric data, for example, a list of `numbers` as in this book's introductory example in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List) or, more realistically, data from a CSV file with many rows and columns.\n", + "\n", + "Processing numeric data usually comes down to operations that may be grouped into one of the following three categories:\n", + "\n", + "- **mapping**: transform a number according to some functional relationship $y = f(x)$\n", + "- **filtering**: throw away individual numbers (e.g., statistical outliers in a sample)\n", + "- **reducing**: collect individual numbers into summary statistics\n", + "\n", + "We study this **map-filter-reduce** paradigm extensively in [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb) after introducing more advanced data types that are needed to work with \"big\" data.\n", + "\n", + "Here, we focus on *filtering out* some numbers in a `for`-loop." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: A simple Filter" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Calculate the sum of all even numbers in `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]` after squaring them and adding `1` to the squares:\n", + "\n", + "- \"*all*\" => **loop** over an iterable\n", + "- \"*even*\" => **filter** out the odd numbers\n", + "- \"*square and add $1$*\" => apply the **map** $y = f(x) = x^2 + 1$\n", + "- \"*sum*\" => **reduce** the remaining and mapped numbers to their sum" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8 -> 65 12 -> 145 2 -> 5 6 -> 37 10 -> 101 4 -> 17 " + ] + }, + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "total = 0\n", + "\n", + "for number in numbers:\n", + " if number % 2 == 0: # only keep even numbers\n", + " square = (number ** 2) + 1\n", + " print(number, \"->\", square, end=\" \") # added for didactical purposes\n", + " total += square\n", + "\n", + "total" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The above code is easy to read as it involves only two levels of indentation.\n", + "\n", + "In general, code gets harder to comprehend the more **horizontal space** it occupies. It is commonly considered good practice to grow a program **vertically** rather than horizontally. Code compliant with [PEP 8 ](https://www.python.org/dev/peps/pep-0008/#maximum-line-length) requires us to use *at most* 79 characters in a line!\n", + "\n", + "Consider the next example, whose implementation in code already starts to look unbalanced." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Several Filters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Calculate the sum of every third and even number in `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]` after squaring them and adding `1` to the squares:\n", + "\n", + "- \"*every*\" => **loop** over an iterable\n", + "- \"*third*\" => **filter** out all numbers except every third\n", + "- \"*even*\" => **filter** out the odd numbers\n", + "- \"*square and add $1$*\" => apply the **map** $y = f(x) = x^2 + 1$\n", + "- \"*sum*\" => **reduce** the remaining and mapped numbers to their sum" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8 -> 65 12 -> 145 4 -> 17 " + ] + }, + { + "data": { + "text/plain": [ + "227" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "total = 0\n", + "\n", + "for i, number in enumerate(numbers, start=1):\n", + " if i % 3 == 0: # only keep every third number\n", + " if number % 2 == 0: # only keep even numbers\n", + " square = (number ** 2) + 1\n", + " print(number, \"->\", square, end=\" \") # added for didactical purposes \n", + " total += square\n", + "\n", + "total" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With already three levels of indentation, less horizontal space is available for the actual code block. Of course, one could flatten the two `if` statements with the logical `and` operator, as shown in [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#The-if-Statement). Then, however, we trade off horizontal space against a more \"complex\" `if` logic, and this is *not* a real improvement." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### The `continue` Statement" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A Pythonista would instead make use of the `continue` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement)) that causes a loop to jump into the next iteration skipping the rest of the code block.\n", + "\n", + "The revised code fragment below occupies more vertical space and less horizontal space: A *good* trade-off.\n", + "\n", + "One caveat is that we need to negate the conditions in the `if` statements. Conceptually, we are now filtering \"out\" and not \"in.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8 -> 65 12 -> 145 4 -> 17 " + ] + }, + { + "data": { + "text/plain": [ + "227" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "total = 0\n", + "\n", + "for i, number in enumerate(numbers, start=1):\n", + " if i % 3 != 0: # only keep every third number\n", + " continue\n", + " elif number % 2 != 0: # only keep even numbers\n", + " continue\n", + "\n", + " square = (number ** 2) + 1\n", + " print(number, \"->\", square, end=\" \") # added for didactical purposes \n", + " total += square\n", + "\n", + "total" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This is yet another illustration of why programming is an art. The two preceding code cells do the *same* with *identical* time complexity. However, the latter is arguably easier to read for a human, even more so when the business logic grows beyond two filters." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Indefinite Loops" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sometimes we find ourselves in situations where we *cannot* know ahead of time how often or until which point in time a code block is to be executed." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Guessing a Coin Toss" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's consider a game where we randomly choose a variable to be either \"Heads\" or \"Tails\" and the user of our program has to guess it.\n", + "\n", + "Python provides the built-in [input() ](https://docs.python.org/3/library/functions.html#input) function that prints a message to the user, called the **prompt**, and reads in what was typed in response as a `str` object. We use it to process a user's \"unreliable\" input to our program (i.e., a user might type in some invalid response). Further, we use the [random() ](https://docs.python.org/3/library/random.html#random.random) function in the [random ](https://docs.python.org/3/library/random.html) module to model the coin toss.\n", + "\n", + "A popular pattern to approach such **indefinite loops** is to go with a `while True` statement, which on its own would cause Python to enter into an infinite loop. Then, once a particular event occurs, we `break` out of the loop.\n", + "\n", + "Let's look at a first and naive implementation." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess if the coin comes up as heads or tails: heads\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ooops, it was tails\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess if the coin comes up as heads or tails: heads\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Yes, it was heads\n" + ] + } + ], + "source": [ + "while True:\n", + " guess = input(\"Guess if the coin comes up as heads or tails: \")\n", + "\n", + " if random.random() < 0.5:\n", + " if guess == \"heads\":\n", + " print(\"Yes, it was heads\")\n", + " break\n", + " else:\n", + " print(\"Ooops, it was heads\")\n", + " else:\n", + " if guess == \"tails\":\n", + " print(\"Yes, it was tails\")\n", + " break\n", + " else:\n", + " print(\"Ooops, it was tails\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This version exhibits two *severe* issues where we should improve on:\n", + "\n", + "1. If a user enters *anything* other than `\"heads\"` or `\"tails\"`, for example, `\"Heads\"` or `\"Tails\"`, the program keeps running *without* the user knowing about the mistake!\n", + "2. The code *intermingles* the coin tossing with the processing of the user's input: Mixing *unrelated* business logic in the *same* code block makes a program harder to read and, more importantly, maintain in the long run." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Guessing a Coin Toss (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's refactor the code and make it *modular*.\n", + "\n", + "First, we divide the business logic into two functions `get_guess()` and `toss_coin()` that are controlled from within a `while`-loop.\n", + "\n", + "`get_guess()` not only reads in the user's input but also implements a simple input validation pattern in that the [.strip() ](https://docs.python.org/3/library/stdtypes.html?highlight=__contains__#str.strip) and [.lower() ](https://docs.python.org/3/library/stdtypes.html?highlight=__contains__#str.lower) methods remove preceding and trailing whitespace and lower case the input ensuring that the user may spell the input in any possible way (e.g., all upper or lower case). Also, `get_guess()` checks if the user entered one of the two valid options. If so, it returns either `\"heads\"` or `\"tails\"`; if not, it returns `None`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def get_guess():\n", + " \"\"\"Process the user's input.\n", + " \n", + " Returns:\n", + " guess (str / NoneType): either \"heads\" or \"tails\"\n", + " if the input can be parsed and None otherwise\n", + " \"\"\"\n", + " guess = input(\"Guess if the coin comes up as heads or tails: \")\n", + " # handle frequent cases of \"misspelled\" user input\n", + " guess = guess.strip().lower()\n", + "\n", + " if guess in [\"heads\", \"tails\"]:\n", + " return guess\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`toss_coin()` models a fair coin toss when called with default arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def toss_coin(p_heads=0.5):\n", + " \"\"\"Simulate the tossing of a coin.\n", + "\n", + " Args:\n", + " p_heads (optional, float): probability that the coin comes up \"heads\";\n", + " defaults to 0.5 resembling a fair coin\n", + "\n", + " Returns:\n", + " side_on_top (str): \"heads\" or \"tails\"\n", + " \"\"\"\n", + " if random.random() < p_heads:\n", + " return \"heads\"\n", + " return \"tails\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Second, we rewrite the `if`-`else`-logic to handle the case where `get_guess()` returns `None` explicitly: Whenever the user enters something invalid, a warning is shown, and another try is granted. We use the `is` operator and not the `==` operator as `None` is a singleton object.\n", + "\n", + "The `while`-loop takes on the role of **glue code** that manages how other parts of the program interact with each other." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess if the coin comes up as heads or tails: invalid\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Make sure to enter your guess correctly!\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Guess if the coin comes up as heads or tails: Heads\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Yes, it was heads\n" + ] + } + ], + "source": [ + "while True:\n", + " guess = get_guess()\n", + " result = toss_coin()\n", + "\n", + " if guess is None:\n", + " print(\"Make sure to enter your guess correctly!\")\n", + " elif guess == result:\n", + " print(\"Yes, it was\", result)\n", + " break\n", + " else:\n", + " print(\"Ooops, it was\", result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, the program's business logic is expressed in a clearer way. More importantly, we can now change it more easily. For example, we could make the `toss_coin()` function base the tossing on a probability distribution other than the uniform (i.e., replace the [random.random() ](https://docs.python.org/3/library/random.html#random.random) function with another one). In general, modular architecture leads to improved software maintenance." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/04_iteration/04_exercises_dice.ipynb b/04_iteration/04_exercises_dice.ipynb new file mode 100644 index 0000000..9c3feae --- /dev/null +++ b/04_iteration/04_exercises_dice.ipynb @@ -0,0 +1,403 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/04_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 4: Recursion & Looping (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [third part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb) of Chapter 4.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Throwing Dice" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this exercise, you will model the throwing of dice within the context of a guessing game similar to the one shown in the \"*Example: Guessing a Coin Toss*\" section in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-Guessing-a-Coin-Toss).\n", + "\n", + "As the game involves randomness, we import the [random ](https://docs.python.org/3/library/random.html) module from the [standard library ](https://docs.python.org/3/library/index.html). To follow best practices, we set the random seed as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A die has six sides that we labeled with integers `1` to `6` in this exercise. For a fair die, the probability for each side is the same.\n", + "\n", + "**Q1**: Model a `fair_die` as a `list` object!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fair_die = ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What function from the [random ](https://docs.python.org/3/library/random.html) module that we have seen already is useful for modeling a single throw of the `fair_die`? Write a simple expression (i.e., one function call) that draws one of the equally likely sides! Execute the cell a couple of times to \"see\" the probability distribution!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's check if the `fair_die` is indeed fair. To do so, we create a little numerical experiment and throw the `fair_die` `100000` times. We track the six different outcomes in a `list` object called `throws` that we initialize with all `0`s for each outcome.\n", + "\n", + "**Q3**: Complete the `for`-loop below such that it runs `100000` times! In the body, use your answer to **Q2** to simulate a single throw of the `fair_die` and update the corresponding count in `throws`!\n", + "\n", + "Hints: You need to use the indexing operator `[]` and calculate an `index` in each iteration of the loop. Do do not actually need the target variable provided by the `for`-loop and may want to indicate that with an underscore `_`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "throws = [0, 0, 0, 0, 0, 0]\n", + "\n", + "for ... in ...:\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + "throws" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`throws` contains the simulation results as absolute counts.\n", + "\n", + "**Q4**: Complete the `for`-loop below to convert the counts in `throws` to relative frequencies stored in a `list` called `frequencies`! Round the frequencies to three decimals with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function!\n", + "\n", + "Hints: Initialize `frequencies` just as `throws` above. How many iterations does the `for`-loop have? `6` or `100000`? You may want to obtain an `index` variable with the [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) built-in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "frequencies = [0, 0, 0, 0, 0, 0]\n", + "\n", + "for ... in ...:\n", + " ...\n", + "\n", + "frequencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: How could we adapt the `list` object used above to model an `unfair_die` where `1` is as likely as `2`, `2` is twice as likely as `3`, and `3` is twice as likely as `4`, `5`, or `6`, who are all equally likely?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "unfair_die = ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Copy your solution to **Q2** for the `unfair_die`! Execute the cell a couple of times to \"see\" the probability distribution!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Copy and adapt your solutions to **Q3** and **Q4** to calculate the `frequencies` for the `unfair_die`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "throws = [0, 0, 0, 0, 0, 0]\n", + "frequencies = [0, 0, 0, 0, 0, 0]\n", + "\n", + "for ... in ...:\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + "for ... in ...:\n", + " ...\n", + "\n", + "frequencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: The built-in [input() ](https://docs.python.org/3/library/functions.html#input) allows us to ask the user to enter a `guess`. What is the data type of the object returned by [input() ](https://docs.python.org/3/library/functions.html#input)? Assume the user enters the `guess` as a number (i.e., \"1\", \"2\", ...) and not as a text (e.g., \"one\")." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "guess = input(\"Guess the side of the die: \")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "guess" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Use a built-in constructor to cast `guess` as an `int` object!\n", + "\n", + "Hint: Simply wrap `guess` or `input(\"Guess the side of the die: \")` with the constructor you choose." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: What type of error is raised if `guess` cannot be cast as an `int` object?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Write a `try` statement that catches the type of error (i.e., your answer to **Q10**) raised if the user's input cannot be cast as an `int` object! Print out some nice error message notifying the user of the bad input!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " ...\n", + "except ...:\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Write a function `get_guess()` that takes a user's input and checks if it is a valid side of the die! The function should *return* either an `int` object between `1` and `6` or `None` if the user enters something invalid.\n", + "\n", + "Hints: You may want to re-use the `try` statement from **Q11**. Instead of printing out an error message, you can also `return` directly from the `except`-clause (i.e., early exit) with `None`. So, the user can make *two* kinds of input errors and maybe you want to model that with two *distinct* `return None` statements. Also, you may want to allow the user to enter leading and trailing whitespace that gets removed without an error message." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_guess():\n", + " \"\"\"Process the user's input.\n", + " \n", + " Returns:\n", + " guess (int / NoneType): either 1, 2, 3, 4, 5 or 6\n", + " if the input can be parsed and None otherwise\n", + " \"\"\"\n", + " ...\n", + "\n", + " # Check if the user entered an integer.\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " # Check if the user entered a valid side.\n", + " ...\n", + " ...\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13** Test your function for all *three* cases!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "get_guess()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Write an *indefinite* loop where in each iteration a `fair_die` is thrown and the user makes a guess! Print out an error message if the user does not enter something that can be understood as a number between `1` and `6`! The game should continue until the user makes a correct guess." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "...\n", + "...\n", + "...\n", + "\n", + "...\n", + "...\n", + "...\n", + "...\n", + "...\n", + "...\n", + "..." + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/04_iteration/05_summary.ipynb b/04_iteration/05_summary.ipynb new file mode 100644 index 0000000..18d287d --- /dev/null +++ b/04_iteration/05_summary.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 4: Recursion & Looping (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Iteration** is about **running blocks of code repeatedly**.\n", + "\n", + "There are two redundant approaches to achieving that.\n", + "\n", + "First, we combine functions that call themselves with conditional statements. This concept is known as **recursion** and suffices to control the flow of execution in *every* way we desire. For a beginner, this approach of **backward** reasoning might not be intuitive, but it turns out to be a handy tool to have in one's toolbox.\n", + "\n", + "Second, the `while` and `for` statements are alternative and potentially more intuitive ways to express iteration as they correspond to a **forward** reasoning. The `for` statement is **syntactic sugar** that allows rewriting common occurrences of the `while` statement concisely. Python provides the `break` and `continue` statements as well as an optional `else`-clause that make working with the `for` and `while` statements even more convenient.\n", + "\n", + "**Iterables** are any **concrete data types** that support being looped over, for example, with the `for` statement. The idea behind iterables is an **abstract concept** that may or may not be implemented by any given concrete data type. For example, both `list` and `range` objects can be looped over. The `list` type is also a **container** as any given `list` objects \"contains\" references to other objects in memory. On the contrary, the `range` type does not reference any other object but instead creates *new* `int` objects \"on the fly\" (i.e., when being looped over)." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/04_iteration/06_review.ipynb b/04_iteration/06_review.ipynb new file mode 100644 index 0000000..eeb19a5 --- /dev/null +++ b/04_iteration/06_review.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 4: Recursion & Looping (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb), [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb), and the [third ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb) part of Chapter 4.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Solving a problem by **recursion** is not only popular in computer science and math. Name some examples from the fields of business or economics where problems are also solved in a **backward** fashion!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Explain what **duck typing** means! Why can it cause problems? Why is it [not a bug but a feature](https://www.urbandictionary.com/define.php?term=It%27s%20not%20a%20bug%2C%20it%27s%20a%20feature)?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: What is **syntactic sugar**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Describe in your own words why the **recursive** version of `fibonacci()`, the \"Easy at first Glance\" example in the chapter, is computationally **inefficient**! Why does the **iterative** version of `fibonacci()`, the \"Hard at first Glance\" example, run so much faster?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What is the conceptual difference between a **container** and a **list**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: What is a good use case for the `for`-loop's optional `else`-clause?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: When a **recursion** does **not** reach the base case, this is an example of the **early exit** strategy." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Any programming language **without** looping constructs like the `for` or `while` statements is **not** Turing complete." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: A **recursive** formulation is the same as a **circular** one: The terms are **synonyms**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: Formulating a computational problem as a **recursion** results in an **efficient** implementation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Whereas a **recursion** may accidentally result in a **never-ending** program, `while`-loops and `for`-loops are guaranteed to **terminate**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Before writing **any** kind of **loop**, we **always** need to think about a **stopping criterion** ahead of time." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: **Container** types such as `list` objects are characterized by their **support** for **being looped over**, for example as in:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "for element in container:\n", + " # do something for every element\n", + " ...\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/04_iteration/static/towers_of_hanoi.gif b/04_iteration/static/towers_of_hanoi.gif new file mode 100644 index 0000000..7ab13d4 Binary files /dev/null and b/04_iteration/static/towers_of_hanoi.gif differ diff --git a/05_numbers/00_content.ipynb b/05_numbers/00_content.ipynb new file mode 100644 index 0000000..405c540 --- /dev/null +++ b/05_numbers/00_content.ipynb @@ -0,0 +1,2454 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 5: Numbers & Bits" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After learning about the basic building blocks of expressing and structuring the business logic in programs, we focus our attention on the **data types** Python offers us, both built-in and available via the [standard library ](https://docs.python.org/3/library/index.html) or third-party packages.\n", + "\n", + "We start with the \"simple\" ones: Numeric types in this chapter and textual data in [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb). An important fact that holds for all objects of these types is that they are **immutable**. To reuse the bag analogy from [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Objects-vs.-Types-vs.-Values), this means that the $0$s and $1$s making up an object's *value* cannot be changed once the bag is created in memory, implying that any operation with or method on the object creates a *new* object in a *different* memory location.\n", + "\n", + "[Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb), [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb), [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb), [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/10_arrarys/00_content.ipynb) then cover the more \"complex\" data types, including, for example, the `list` type. Finally, [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) completes the picture by introducing language constructs to create custom types.\n", + "\n", + "We have already seen many hints indicating that numbers are not as trivial to work with as it seems at first sight:\n", + "\n", + "- [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Data%29-Type-%2F-%22Behavior%22) reveals that numbers may come in *different* data types (i.e., `int` vs. `float` so far),\n", + "- [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#Boolean-Expressions) raises questions regarding the **limited precision** of `float` numbers (e.g., `42 == 42.000000000000001` evaluates to `True`), and\n", + "- [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb#Infinite-Recursion) shows that sometimes a `float` \"walks\" and \"quacks\" like an `int`, whereas the reverse is true in other cases.\n", + "\n", + "This chapter introduces all the [built-in numeric types ](https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex): `int`, `float`, and `complex`. To mitigate the limited precision of floating-point numbers, we also add an [Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/03_appendix.ipynb) where we look at two replacements for the `float` type in the [standard library ](https://docs.python.org/3/library/index.html), namely the `Decimal` type in the [decimals ](https://docs.python.org/3/library/decimal.html#decimal.Decimal) and the `Fraction` type in the [fractions ](https://docs.python.org/3/library/fractions.html#fractions.Fraction) module." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `int` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The simplest numeric type is the `int` type: It behaves like an [integer in ordinary math ](https://en.wikipedia.org/wiki/Integer) (i.e., the set $\\mathbb{Z}$) and supports operators in the way we saw in the section on arithmetic operators in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Arithmetic%29-Operators).\n", + "\n", + "One way to create `int` objects is by simply writing its value as a literal with the digits `0` to `9`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a = 42" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Just like any other object, the `42` has an identity, a type, and a value." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94857615440928" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(a)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "int" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(a)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A nice feature in newer Python versions is using underscores `_` as (thousands) separators in numeric literals. For example, `1_000_000` evaluates to `1000000` in memory; the `_`s are ignored by the interpreter." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1000000" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1_000_000" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may place the `_`s anywhere we want." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "123456789" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1_2_3_4_5_6_7_8_9" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is syntactically invalid to write out leading `0`s in numeric literals. The reason for that will become apparent in the next section." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers (1481240458.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[7], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m 042\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers\n" + ] + } + ], + "source": [ + "042" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another way to create `int` objects is with the [int() ](https://docs.python.org/3/library/functions.html#int) built-in that casts `float` or properly formatted `str` objects as integers. Then, decimals are truncated (i.e., \"cut off\")." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(42.11)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(42.87)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whereas the integer division operator `//` \"rounds\" towards negative infinity (cf., the \"*(Arithmetic) Operators*\" section in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Arithmetic%29-Operators)), the [int() ](https://docs.python.org/3/library/functions.html#int) built-in rounds towards `0`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-42" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(-42.87)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When casting `str` objects as `int`s, the [int() ](https://docs.python.org/3/library/functions.html#int) built-in is less forgiving. We must not include any decimals as shown by the `ValueError`. Yet, leading and trailing whitespace is gracefully ignored." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(\"42\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "invalid literal for int() with base 10: '42.0'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[12], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m42.0\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mValueError\u001b[0m: invalid literal for int() with base 10: '42.0'" + ] + } + ], + "source": [ + "int(\"42.0\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(\" 42 \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `int` type follows all rules we know from math, apart from one exception: Whereas mathematicians to this day argue what the term $0^0$ means (cf., this [article ](https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero)), programmers are pragmatic about this and simply define $0^0 = 1$." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 ** 0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Binary Representations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As computers can only store $0$s and $1$s, `int` objects are nothing but that in memory as well. Consequently, computer scientists and engineers developed conventions as to how $0$s and $1$s are \"translated\" into integers, and one such convention is the **[binary representation ](https://en.wikipedia.org/wiki/Binary_number)** of **non-negative integers**. Consider the integers from $0$ through $255$ that are encoded into $0$s and $1$s with the help of this table:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "|Bit $i$| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |\n", + "|:------|:----|:----|:----|:----|:----|:----|:----|:----|\n", + "| Digit |$2^7$|$2^6$|$2^5$|$2^4$|$2^3$|$2^2$|$2^1$|$2^0$|\n", + "| $=$ |$128$| $64$| $32$| $16$| $8$ | $4$ | $2$ | $1$ |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A number consists of exactly eight $0$s and $1$s that are read from right to left and referred to as the **bits** of the number. Each bit represents a distinct multiple of $2$, the **digit**. For sure, we start counting at $0$ again.\n", + "\n", + "To encode the integer $3$, for example, we need to find a combination of $0$s and $1$s such that the sum of digits marked with a $1$ is equal to the number we want to encode. In the example, we set all bits to $0$ except for the first ($i=0$) and second ($i=1$) as $2^0 + 2^1 = 1 + 2 = 3$. So the binary representation of $3$ is $00~00~00~11$. To borrow some terminology from linear algebra, the $3$ is a linear combination of the digits where the coefficients are either $0$ or $1$: $3 = 0*128 + 0*64 + 0*32 + 0*16 + 0*8 + 0*4 + 1*2 + 1*1$. It is *guaranteed* that there is exactly *one* such combination for each number between $0$ and $255$.\n", + "\n", + "As each bit in the binary representation is one of two values, we say that this representation has a base of $2$. Often, the base is indicated with a subscript to avoid confusion. For example, we write $3_{10} = 00000011_2$ or $3_{10} = 11_2$ for short omitting leading $0$s. A subscript of $10$ implies a decimal number as we know it from elementary school.\n", + "\n", + "We use the built-in [bin() ](https://docs.python.org/3/library/functions.html#bin) function to obtain an `int` object's binary representation: It returns a `str` object starting with `\"0b\"` indicating the binary format and as many $0$s and $1$s as are necessary to encode the integer omitting leading $0$s." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b11'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may pass a `str` object formatted this way as the argument to the [int() ](https://docs.python.org/3/library/functions.html#int) built-in, together with `base=2`, to create an `int` object, for example, with the value of `3`." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(\"0b11\", base=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Moreover, we may also use the contents of the returned `str` object as a **literal** instead: Just like we type, for example, `3` without quotes (i.e., \"literally\") into a code cell to create the `int` object `3`, we may type `0b11` to obtain an `int` object with the same value." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b11" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another example is the integer `123` that is the sum of $64 + 32 + 16 + 8 + 2 + 1$: Thus, its binary representation is the sequence of bits $01~11~10~11$, or to use our new notation, $123_{10} = 1111011_2$." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1111011'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(123)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogous to typing `123` into a code cell, we may write `0b1111011`, or `0b_111_1011` to make use of the underscores, and create an `int` object with the value `123`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "123" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b_111_1011" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`0` and `255` are the edge cases where we set all the bits to either $0$ or $1$." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b0'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b10'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b11111111'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(255)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Groups of eight bits are also called a **byte**. As a byte can only represent non-negative integers up to $255$, the table above is extended conceptually with greater digits to the left to model integers beyond $255$. The memory management needed to implement this is built into Python, and we do not need to worry about it.\n", + "\n", + "For example, `789` is encoded with ten bits and $789_{10} = 1100010101_2$." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1100010101'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(789)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To contrast this bits encoding with the familiar decimal system, we show an equivalent table with powers of $10$ as the digits:\n", + "\n", + "|Decimal| 3 | 2 | 1 | 0 |\n", + "|-------|------|------|------|------|\n", + "| Digit |$10^3$|$10^2$|$10^1$|$10^0$|\n", + "| $=$ |$1000$| $100$| $10$ | $1$ |\n", + "\n", + "Now, an integer is a linear combination of the digits where the coefficients are one of *ten* values, and the base is now $10$. For example, the number $123$ can be expressed as $0*1000 + 1*100 + 2*10 + 3*1$. So, the binary representation follows the same logic as the decimal system taught in elementary school. The decimal system is intuitive to us humans, mostly as we learn to count with our *ten* fingers. The $0$s and $1$s in a computer's memory are therefore no rocket science; they only feel unintuitive for a beginner." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Arithmetic with Bits" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Adding two numbers in their binary representations is straightforward and works just like we all learned addition in elementary school. Going from right to left, we add the individual digits, and ..." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 + 2" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1 + 0b10 = 0b11'" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(1) + \" + \" + bin(2) + \" = \" + bin(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... if any two digits add up to $2$, the resulting digit is $0$ and a $1$ carries over." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 + 3" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1 + 0b11 = 0b100'" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(1) + \" + \" + bin(3) + \" = \" + bin(4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Multiplication is also quite easy. All we need to do is to multiply the left operand by all digits of the right operand separately and then add up the individual products, just like in elementary school." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "4 * 3" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b100 * 0b11 = 0b1100'" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(4) + \" * \" + bin(3) + \" = \" + bin(12)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b100 * 0b1 = 0b100'" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(4) + \" * \" + bin(1) + \" = \" + bin(4) # multiply with first digit" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b100 * 0b10 = 0b1000'" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(4) + \" * \" + bin(2) + \" = \" + bin(8) # multiply with second digit" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [Further Resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/06_resources.ipynb) section at the end of this chapter provides video tutorials on addition and multiplication in binary. Subtraction and division are a bit more involved but essentially also easy to understand." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Hexadecimal Representations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While in the binary and decimal systems there are two and ten distinct coefficients per digit, another convenient representation, the **hexadecimal representation**, uses a base of $16$. It is convenient as one digit stores the same amount of information as *four* bits and the binary representation quickly becomes unreadable for larger numbers. The letters \"a\" through \"f\" are used as digits \"10\" through \"15\".\n", + "\n", + "The following table summarizes the relationship between the three systems:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "|Decimal|Hexadecimal|Binary|$~~~~~~$|Decimal|Hexadecimal|Binary|$~~~~~~$|Decimal|Hexadecimal|Binary|$~~~~~~$|...|\n", + "|-------|-----------|------|--------|-------|-----------|------|--------|-------|-----------|------|--------|---|\n", + "| 0 | 0 | 0000 |$~~~~~~$| 16 | 10 | 10000|$~~~~~~$| 32 | 20 |100000|$~~~~~~$|...|\n", + "| 1 | 1 | 0001 |$~~~~~~$| 17 | 11 | 10001|$~~~~~~$| 33 | 21 |100001|$~~~~~~$|...|\n", + "| 2 | 2 | 0010 |$~~~~~~$| 18 | 12 | 10010|$~~~~~~$| 34 | 22 |100010|$~~~~~~$|...|\n", + "| 3 | 3 | 0011 |$~~~~~~$| 19 | 13 | 10011|$~~~~~~$| 35 | 23 |100011|$~~~~~~$|...|\n", + "| 4 | 4 | 0100 |$~~~~~~$| 20 | 14 | 10100|$~~~~~~$| 36 | 24 |100100|$~~~~~~$|...|\n", + "| 5 | 5 | 0101 |$~~~~~~$| 21 | 15 | 10101|$~~~~~~$| 37 | 25 |100101|$~~~~~~$|...|\n", + "| 6 | 6 | 0110 |$~~~~~~$| 22 | 16 | 10110|$~~~~~~$| 38 | 26 |100110|$~~~~~~$|...|\n", + "| 7 | 7 | 0111 |$~~~~~~$| 23 | 17 | 10111|$~~~~~~$| 39 | 27 |100111|$~~~~~~$|...|\n", + "| 8 | 8 | 1000 |$~~~~~~$| 24 | 18 | 11000|$~~~~~~$| 40 | 28 |101000|$~~~~~~$|...|\n", + "| 9 | 9 | 1001 |$~~~~~~$| 25 | 19 | 11001|$~~~~~~$| 41 | 29 |101001|$~~~~~~$|...|\n", + "| 10 | a | 1010 |$~~~~~~$| 26 | 1a | 11010|$~~~~~~$| 42 | 2a |101010|$~~~~~~$|...|\n", + "| 11 | b | 1011 |$~~~~~~$| 27 | 1b | 11011|$~~~~~~$| 43 | 2b |101011|$~~~~~~$|...|\n", + "| 12 | c | 1100 |$~~~~~~$| 28 | 1c | 11100|$~~~~~~$| 44 | 2c |101100|$~~~~~~$|...|\n", + "| 13 | d | 1101 |$~~~~~~$| 29 | 1d | 11101|$~~~~~~$| 45 | 2d |101101|$~~~~~~$|...|\n", + "| 14 | e | 1110 |$~~~~~~$| 30 | 1e | 11110|$~~~~~~$| 46 | 2e |101110|$~~~~~~$|...|\n", + "| 15 | f | 1111 |$~~~~~~$| 31 | 1f | 11111|$~~~~~~$| 47 | 2f |101111|$~~~~~~$|...|" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To show more examples of the above subscript convention, we pick three random entries from the table:\n", + "\n", + "$11_{10} = \\text{b}_{16} = 1011_2$\n", + "\n", + "$25_{10} = 19_{16} = 11001_2$\n", + "\n", + "$46_{10} = 2\\text{e}_{16} = 101110_2$\n", + "\n", + "The built-in [hex() ](https://docs.python.org/3/library/functions.html#hex) function creates a `str` object starting with `\"0x\"` representing an `int` object's hexadecimal representation. The length depends on how many groups of four bits are implied by the corresponding binary representation.\n", + "\n", + "For `0` and `1`, the hexadecimal representation is similar to the binary one." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x0'" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x1'" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whereas `bin(3)` already requires two digits, one is enough for `hex(3)`." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x3'" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(3) # bin(3) => \"0b11\"; two digits needed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For `10` and `15`, we see the letter digits for the first time." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xa'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xf'" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(15)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The binary representation of `123`, `0b_111_1011`, can be viewed as *two* groups of four bits, $0111$ and $1011$, that are encoded as $7$ and $\\text{b}$ in hexadecimal (cf., table above)." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1111011'" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(123)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x7b'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(123)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To obtain a *new* `int` object with the value `123`, we call the [int() ](https://docs.python.org/3/library/functions.html#int) built-in with a properly formatted `str` object and `base=16` as arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "123" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "int(\"0x7b\", base=16)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we could use a literal notation instead." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "123" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0x7b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Hexadecimals between $00_{16}$ and $\\text{ff}_{16}$ (i.e., $0_{10}$ and $255_{10}$) are commonly used to describe colors, for example, in web development but also graphics editors. See this [online tool](https://www.w3schools.com/colors/colors_hexadecimal.asp) for some more background." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x0'" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xff'" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(255)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Just like binary representations, the hexadecimals extend to the left for larger numbers like `789`." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x315'" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(789)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For completeness sake, we mention that there is also the [oct() ](https://docs.python.org/3/library/functions.html#oct) built-in to obtain an integer's **octal representation**. The logic is the same as for the hexadecimal representation, and we use *eight* instead of *sixteen* digits. That is the equivalent of viewing the binary representations in groups of three bits. As of today, octal representations have become less important, and the data science practitioner may probably live without them quite well." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Negative Values" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While there are conventions that model negative integers with $0$s and $1$s in memory (cf., [Two's Complement ](https://en.wikipedia.org/wiki/Two%27s_complement)), Python manages that for us, and we do not look into the theory here for brevity. We have learned all that a practitioner needs to know about how integers are modeled in a computer. The [Further Resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/06_resources.ipynb) section at the end of this chapter provides a video tutorial on how the [Two's Complement ](https://en.wikipedia.org/wiki/Two%27s_complement) idea works.\n", + "\n", + "The binary and hexadecimal representations of negative integers are identical to their positive counterparts except that they start with a minus sign `-`. However, as the video tutorial at the end of the chapter reveals, that is *not* how the bits are organized in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-0b11'" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(-3)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-0x3'" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(-3)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-0b11111111'" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(-255)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-0xff'" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hex(-255)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Bitwise Operators" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now that we know how integers are represented with $0$s and $1$s, we look at ways of working with the individual bits, in particular with the so-called **[bitwise operators](https://wiki.python.org/moin/BitwiseOperators)**: As the name suggests, the operators perform some operation on a bit by bit basis. They only work with and always return `int` objects.\n", + "\n", + "We keep this overview rather short as such \"low-level\" operations are not needed by the data science practitioner regularly. Yet, it is worthwhile to have heard about them as they form the basis of all of arithmetic in computers.\n", + "\n", + "The first operator is the **bitwise AND** operator `&`: It looks at the bits of its two operands, `11` and `13` in the example, in a pairwise fashion and if *both* operands have a $1$ in the *same* position, the resulting integer will have a $1$ in this position as well. Otherwise, the resulting integer will have a $0$ in this position. The binary representations of `11` and `13` both have $1$s in their respective first and fourth bits, which is why `bin(11 & 13)` evaluates to `Ob_1001` or `9`." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "11 & 13" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1011 & 0b1101'" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(11) + \" & \" + bin(13) # to show the operands' bits" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1001'" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(11 & 13)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`0b_1001` is the binary representation of `9`." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b_1001" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The **bitwise OR** operator `|` evaluates to an `int` object whose bits are set to $1$ if the corresponding bits of either *one* or *both* operands are $1$. So in the example `9 | 13` only the second bit is $0$ for both operands, which is why the expression evaluates to `0b_1101` or `13`." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "13" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "9 | 13" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1001 | 0b1101'" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(9) + \" | \" + bin(13) # to show the operands' bits" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1101'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(9 | 13)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`0b_1101` evaluates to an `int` object with the value `13`." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "13" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b_1101" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The **bitwise XOR** operator `^` is a special case of the `|` operator in that it evaluates to an `int` object whose bits are set to $1$ if the corresponding bit of *exactly one* of the two operands is $1$. Colloquially, the \"X\" stands for \"exclusive.\" The `^` operator must *not* be confused with the exponentiation operator `**`! In the example, `9 ^ 13`, only the third bit differs between the two operands, which is why it evaluates to `0b_100` omitting the leading $0$." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "9 ^ 13" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b1001 ^ 0b1101'" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(9) + \" ^ \" + bin(13) # to show the operands' bits" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b100'" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(9 ^ 13)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`0b_100` evaluates to an `int` object with the value `4`." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b_100" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The **bitwise NOT** operator `~`, sometimes also called **inversion** operator, is said to \"flip\" the $0$s into $1$s and the $1$s into $0$s. However, it is based on the aforementioned [Two's Complement ](https://en.wikipedia.org/wiki/Two%27s_complement) convention and `~x = -(x + 1)` by definition (cf., the [reference ](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)). The full logic behind this, while actually quite simple, is considered out of scope in this book.\n", + "\n", + "We can at least verify the definition by comparing the binary representations of `7` and `-8`: They are indeed the same." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-8" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "~7" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "~7 == -(7 + 1) # = Two's Complement" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-0b1000'" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(~7)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-0b1000'" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(-(7 + 1))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`~x = -(x + 1)` can be reformulated as `~x + x = -1`, which is slightly easier to check." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "~7 + 7" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, the **bitwise left and right shift** operators, `<<` and `>>`, shift all the bits either to the left or to the right. This corresponds to multiplying or dividing an integer by powers of `2`.\n", + "\n", + "When shifting left, $0$s are filled in." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "28" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "7 << 2" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b111'" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(7)" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b11100'" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(7 << 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "28" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b_1_1100" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When shifting right, some bits are always lost." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "7 >> 1" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b111'" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(7)" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0b11'" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bin(7 >> 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0b_11" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/05_numbers/01_content.ipynb b/05_numbers/01_content.ipynb new file mode 100644 index 0000000..265ae0b --- /dev/null +++ b/05_numbers/01_content.ipynb @@ -0,0 +1,2180 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/01_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 5: Numbers & Bits (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this second part of the chapter, we look at the `float` type in detail. It is probably the most commonly used one in all of data science, even across programming languages." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `float` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As we have seen before, some assumptions need to be made as to how the $0$s and $1$s in a computer's memory are to be translated into numbers. This process becomes a lot more involved when we go beyond integers and model [real numbers ](https://en.wikipedia.org/wiki/Real_number) (i.e., the set $\\mathbb{R}$) with possibly infinitely many digits to the right of the period like $1.23$.\n", + "\n", + "The **[Institute of Electrical and Electronics Engineers ](https://en.wikipedia.org/wiki/Institute_of_Electrical_and_Electronics_Engineers)** (IEEE, pronounced \"eye-triple-E\") is one of the important professional associations when it comes to standardizing all kinds of aspects regarding the implementation of soft- and hardware.\n", + "\n", + "The **[IEEE 754 ](https://en.wikipedia.org/wiki/IEEE_754)** standard defines the so-called **floating-point arithmetic** that is commonly used today by all major programming languages. The standard not only defines how the $0$s and $1$s are organized in memory but also, for example, how values are to be rounded, what happens in exceptional cases like divisions by zero, or what is a zero value in the first place.\n", + "\n", + "In Python, the simplest way to create a `float` object is to use a literal notation with a dot `.` in it." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "b = 42.0" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139923238853936" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(b)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "float" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(b)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with `int` literals, we may use underscores `_` to make longer `float` objects easier to read." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.123456789" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0.123_456_789" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In cases where the dot `.` is unnecessary from a mathematical point of view, we either need to end the number with it nevertheless or use the [float() ](https://docs.python.org/3/library/functions.html#float) built-in to cast the number explicitly. [float() ](https://docs.python.org/3/library/functions.html#float) can process any numeric object or a properly formatted `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"42\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Leading and trailing whitespace is ignored ..." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.87" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\" 42.87 \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... but not whitespace in between." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "could not convert string to float: '42. 87'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mfloat\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m42. 87\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mValueError\u001b[0m: could not convert string to float: '42. 87'" + ] + } + ], + "source": [ + "float(\"42. 87\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`float` objects are implicitly created as the result of dividing an `int` object by another with the division operator `/`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.3333333333333333" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 / 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In general, if we combine `float` and `int` objects in arithmetic operations, we always end up with a `float` type: Python uses the \"broader\" representation." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "40.0 + 2" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "21 * 2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Scientific Notation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`float` objects may also be created with the **scientific literal notation**: We use the symbol `e` to indicate powers of $10$, so $1.23 * 10^0$ translates into `1.23e0`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.23" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1.23e0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Syntactically, `e` needs a `float` or `int` object in its literal notation on its left and an `int` object on its right, both without a space. Otherwise, we get a `SyntaxError`." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (1434619204.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[15], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m 1.23 e0\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "1.23 e0" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid decimal literal (3971374856.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[16], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m 1.23e 0\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid decimal literal\n" + ] + } + ], + "source": [ + "1.23e 0" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (2795275998.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[17], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m 1.23e0.0\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "1.23e0.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we leave out the number to the left, Python raises a `NameError` as it unsuccessfully tries to look up a variable named `e0`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'e0' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[18], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43me0\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'e0' is not defined" + ] + } + ], + "source": [ + "e0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So, to write $10^0$ in Python, we need to think of it as $1*10^0$ and write `1e0`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1e0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To express thousands of something (i.e., $10^3$), we write `1e3`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1000.0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1e3 # = thousands" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, to express, for example, milliseconds (i.e., $10^{-3} s$), we write `1e-3`." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.001" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1e-3 # = milli" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Special Values" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There are also three special values representing \"**not a number,**\" called `nan`, and positive or negative **infinity**, called `inf` or `-inf`, that are created by passing in the corresponding abbreviation as a `str` object to the [float() ](https://docs.python.org/3/library/functions.html#float) built-in. These values could be used, for example, as the result of a mathematically undefined operation like division by zero or to model the value of a mathematical function as it goes to infinity." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "nan" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"nan\") # also float(\"NaN\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "inf" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"+inf\") # also float(\"+infinity\") or float(\"infinity\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "inf" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") # also float(\"+inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-inf" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"-inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`nan` objects *never* compare equal to *anything*, not even to themselves. This happens in accordance with the [IEEE 754 ](https://en.wikipedia.org/wiki/IEEE_754) standard." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"nan\") == float(\"nan\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another caveat is that any arithmetic involving a `nan` object results in `nan`. In other words, the addition below **fails silently** as no error is raised. As this also happens in accordance with the [IEEE 754 ](https://en.wikipedia.org/wiki/IEEE_754) standard, we *need* to be aware of that and check any data we work with for any `nan` occurrences *before* doing any calculations." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "nan" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 + float(\"nan\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, as two values go to infinity, there is no such concept as difference and *everything* compares equal." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") == float(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Adding `42` to `inf` makes no difference." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "inf" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") + 42" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") + 42 == float(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We observe the same for multiplication ..." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "inf" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 * float(\"inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "42 * float(\"inf\") == float(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... and even exponentiation!" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "inf" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") ** 42" + ] + }, + { + "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": [ + "float(\"inf\") ** 42 == float(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Although absolute differences become unmeaningful as we approach infinity, signs are still respected." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "inf" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-42 * float(\"-inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-42 * float(\"-inf\") == float(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As a caveat, adding infinities of different signs is an *undefined operation* in math and results in a `nan` object. So, if we (accidentally or unknowingly) do this on a real dataset, we do *not* see any error messages, and our program may continue to run with non-meaningful results! This is another example of a piece of code **failing silently**." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "nan" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") + float(\"-inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "nan" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(\"inf\") - float(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Imprecision" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`float` objects are *inherently* imprecise, and there is *nothing* we can do about it! In particular, arithmetic operations with two `float` objects may result in \"weird\" rounding \"errors\" that are strictly deterministic and occur in accordance with the [IEEE 754 ](https://en.wikipedia.org/wiki/IEEE_754) standard.\n", + "\n", + "For example, let's add `1` to `1e15` and `1e16`, respectively. In the latter case, the `1` somehow gets \"lost.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1000000000000001.0" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1e15 + 1" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1e+16" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1e16 + 1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Interactions between sufficiently large and small `float` objects are not the only source of imprecision." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from math import sqrt" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2.0000000000000004" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sqrt(2) ** 2" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.30000000000000004" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0.1 + 0.2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This may become a problem if we rely on equality checks in our programs." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sqrt(2) ** 2 == 2" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0.1 + 0.2 == 0.3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A popular workaround is to benchmark the absolute value of the difference between the two numbers to be checked for equality against a pre-defined `threshold` *sufficiently* close to `0`, for example, `1e-15`." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "threshold = 1e-15" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs((sqrt(2) ** 2) - 2) < threshold" + ] + }, + { + "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": [ + "abs((0.1 + 0.2) - 0.3) < threshold" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The built-in [format() ](https://docs.python.org/3/library/functions.html#format) function allows us to show the **significant digits** of a `float` number as they exist in memory to arbitrary precision. To exemplify it, let's view a couple of `float` objects with `50` digits. This analysis reveals that almost no `float` number is precise! After 14 or 15 digits \"weird\" things happen. As we see further below, the \"random\" digits ending the `float` numbers do *not* \"physically\" exist in memory! Rather, they are \"calculated\" by the [format() ](https://docs.python.org/3/library/functions.html#format) function that is forced to show `50` digits.\n", + "\n", + "The [format() ](https://docs.python.org/3/library/functions.html#format) function is different from the [format() ](https://docs.python.org/3/library/stdtypes.html#str.format) method on `str` objects introduced in the next chapter (cf., [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb#format%28%29-Method)): Yet, both work with the so-called [format specification mini-language ](https://docs.python.org/3/library/string.html#format-specification-mini-language): `\".50f\"` is the instruction to show `50` digits of a `float` number." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.10000000000000000555111512312578270211815834045410'" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(0.1, \".50f\")" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.20000000000000001110223024625156540423631668090820'" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(0.2, \".50f\")" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.29999999999999998889776975374843459576368331909180'" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(0.3, \".50f\")" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.33333333333333331482961625624739099293947219848633'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(1 / 3, \".50f\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [format() ](https://docs.python.org/3/library/functions.html#format) function does *not* round a `float` object in the mathematical sense! It just allows us to show an arbitrary number of the digits as stored in memory, and it also does *not* change these.\n", + "\n", + "On the contrary, the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function creates a *new* numeric object that is a rounded version of the one passed in as the argument. It adheres to the common rules of math.\n", + "\n", + "For example, let's round `1 / 3` to five decimals. The obtained value for `roughly_a_third` is also *imprecise* but different from the \"exact\" representation of `1 / 3` above." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "roughly_a_third = round(1 / 3, 5)" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.33333" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "roughly_a_third" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.33333000000000001517008740847813896834850311279297'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(roughly_a_third, \".50f\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Surprisingly, `0.125` and `0.25` appear to be *precise*, and equality comparison works without the `threshold` workaround: Both are powers of $2$ in disguise." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.12500000000000000000000000000000000000000000000000'" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(0.125, \".50f\")" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.25000000000000000000000000000000000000000000000000'" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(0.25, \".50f\")" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0.125 + 0.125 == 0.25" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Binary Representations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To understand these subtleties, we need to look at the **[binary representation of floats ](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)** and review the basics of the **[IEEE 754 ](https://en.wikipedia.org/wiki/IEEE_754)** standard. On modern machines, floats are modeled in so-called double precision with $64$ bits that are grouped as in the figure below. The first bit determines the sign ($0$ for plus, $1$ for minus), the next $11$ bits represent an $exponent$ term, and the last $52$ bits resemble the actual significant digits, the so-called $fraction$ part. The three groups are put together like so:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$$float = (-1)^{sign} * 1.fraction * 2^{exponent-1023}$$" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A $1.$ is implicitly prepended as the first digit, and both, $fraction$ and $exponent$, are stored in base $2$ representation (i.e., they both are interpreted like integers above). As $exponent$ is consequently non-negative, between $0_{10}$ and $2047_{10}$ to be precise, the $-1023$, called the exponent bias, centers the entire $2^{exponent-1023}$ term around $1$ and allows the period within the $1.fraction$ part be shifted into either direction by the same amount. Floating-point numbers received their name as the period, formally called the **[radix point ](https://en.wikipedia.org/wiki/Radix_point)**, \"floats\" along the significant digits. As an aside, an $exponent$ of all $0$s or all $1$s is used to model the special values `nan` or `inf`.\n", + "\n", + "As the standard defines the exponent part to come as a power of $2$, we now see why `0.125` is a *precise* float: It can be represented as a power of $2$, i.e., $0.125 = (-1)^0 * 1.0 * 2^{1020-1023} = 2^{-3} = \\frac{1}{8}$. In other words, the floating-point representation of $0.125_{10}$ is $0_2$, $1111111100_2 = 1020_{10}$, and $0_2$ for the three groups, respectively." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The crucial fact for the data science practitioner to understand is that mapping the *infinite* set of the real numbers $\\mathbb{R}$ to a *finite* set of bits leads to the imprecisions shown above!\n", + "\n", + "So, floats are usually good approximations of real numbers only with their first $14$ or $15$ digits. If more precision is required, we need to revert to other data types such as a `Decimal` or a `Fraction`, as shown in the next two sections.\n", + "\n", + "This [blog post](http://fabiensanglard.net/floating_point_visually_explained/) gives another neat and *visual* way as to how to think of floats. It also explains why floats become worse approximations of the reals as their absolute values increase.\n", + "\n", + "The Python [documentation ](https://docs.python.org/3/tutorial/floatingpoint.html) provides another good discussion of floats and the goodness of their approximations.\n", + "\n", + "If we are interested in the exact bits behind a `float` object, we use the [.hex() ](https://docs.python.org/3/library/stdtypes.html#float.hex) method that returns a `str` object beginning with `\"0x1.\"` followed by the $fraction$ in hexadecimal notation and the $exponent$ as an integer after subtraction of $1023$ and separated by a `\"p\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "one_eighth = 1 / 8" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x1.0000000000000p-3'" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "one_eighth.hex()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Also, the [.as_integer_ratio() ](https://docs.python.org/3/library/stdtypes.html#float.as_integer_ratio) method returns the two smallest integers whose ratio best approximates a `float` object." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 8)" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "one_eighth.as_integer_ratio()" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x1.555475a31a4bep-2'" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "roughly_a_third.hex()" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(3002369727582815, 9007199254740992)" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "roughly_a_third.as_integer_ratio()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`0.0` is also a power of $2$ and thus a *precise* `float` number." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "zero = 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0x0.0p+0'" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero.hex()" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0, 1)" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero.as_integer_ratio()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As seen in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Data%29-Type-%2F-%22Behavior%22), the [.is_integer() ](https://docs.python.org/3/library/stdtypes.html#float.is_integer) method tells us if a `float` can be casted as an `int` object without any loss in precision." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "roughly_a_third.is_integer()" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "one = roughly_a_third / roughly_a_third\n", + "\n", + "one.is_integer()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the exact implementation of floats may vary and be dependent on a particular Python installation, we look up the [.float_info ](https://docs.python.org/3/library/sys.html#sys.float_info) attribute in the [sys ](https://docs.python.org/3/library/sys.html) module in the [standard library ](https://docs.python.org/3/library/index.html) to check the details. Usually, this is not necessary." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import sys" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sys.float_info" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/05_numbers/02_content.ipynb b/05_numbers/02_content.ipynb new file mode 100644 index 0000000..701a414 --- /dev/null +++ b/05_numbers/02_content.ipynb @@ -0,0 +1,1519 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/02_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 5: Numbers & Bits (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the third part of this chapter, we first look at the lesser known `complex` type. Then, we introduce a more abstract classification scheme for the numeric types." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `complex` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "**What is the solution to $x^2 = -1$ ?**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Some mathematical equations cannot be solved if the solution has to be in the set of the real numbers $\\mathbb{R}$. For example, $x^2 = -1$ can be rearranged into $x = \\sqrt{-1}$, but the square root is not defined for negative numbers. To mitigate this, mathematicians introduced the concept of an [imaginary number ](https://en.wikipedia.org/wiki/Imaginary_number) $\\textbf{i}$ that is *defined* as $\\textbf{i} = \\sqrt{-1}$ or often as the solution to the equation $\\textbf{i}^2 = -1$. So, the solution to $x = \\sqrt{-1}$ then becomes $x = \\textbf{i}$.\n", + "\n", + "If we generalize the example equation into $(mx-n)^2 = -1 \\implies x = \\frac{1}{m}(\\sqrt{-1} + n)$ where $m$ and $n$ are constants chosen from the reals $\\mathbb{R}$, then the solution to the equation comes in the form $x = a + b\\textbf{i}$, the sum of a real number and an imaginary number, with $a=\\frac{n}{m}$ and $b = \\frac{1}{m}$.\n", + "\n", + "Such \"compound\" numbers are called **[complex numbers ](https://en.wikipedia.org/wiki/Complex_number)**, and the set of all such numbers is commonly denoted by $\\mathbb{C}$. The reals $\\mathbb{R}$ are a strict subset of $\\mathbb{C}$ with $b=0$. Further, $a$ is referred to as the **real part** and $b$ as the **imaginary part** of the complex number.\n", + "\n", + "Complex numbers are often visualized in a plane like below, where the real part is depicted on the x-axis and the imaginary part on the y-axis." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`complex` numbers are part of core Python. The simplest way to create one is to write an arithmetic expression with the literal `j` notation for $\\textbf{i}$. The `j` is commonly used in many engineering disciplines instead of the symbol $\\textbf{i}$ from math as $I$ in engineering more often than not means [electric current ](https://en.wikipedia.org/wiki/Electric_current).\n", + "\n", + "For example, the answer to $x^2 = -1$ can be written in Python as `1j` like below. This creates a `complex` object with value `1j`. The same syntactic rules apply as with the above `e` notation: No spaces are allowed between the number and the `j`. The number may be any `int` or `float` literal; however, it is stored as a `float` internally. So, `complex` numbers suffer from the same imprecision as `float` numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "x = 1j" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140084727448400" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(x)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "complex" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(x)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1j" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To verify that it solves the equation, let's raise it to the power of $2$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x ** 2 == -1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Often, we write an expression of the form $a + b\\textbf{i}$." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2+0.5j)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 + 0.5j" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we may use the [complex() ](https://docs.python.org/3/library/functions.html#complex) built-in: This takes two parameters where the second is optional and defaults to `0`. We may either call it with one or two arguments of any numeric type or a `str` object in the format of the previous code cell without any spaces." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2+0.5j)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "complex(2, 0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By omitting the second argument, we set the imaginary part to $0$." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2+0j)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "complex(2) " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The arguments to [complex() ](https://docs.python.org/3/library/functions.html#complex) may be any numeric type or properly formated `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2+0.5j)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "complex(\"2+0.5j\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Arithmetic expressions work with `complex` numbers. They may be mixed with the other numeric types, and the result is always a `complex` number." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "c1 = 1 + 2j\n", + "c2 = 3 + 4j" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(4+6j)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 + c2" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(-2-2j)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 - c2" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2+2j)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 + 1" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.5-4j)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "3.5 - c2" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(5+10j)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "5 * c1" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.5+0.6666666666666666j)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c2 / 6" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(-5+10j)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 * c2" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.44+0.08j)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 / c2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A `complex` number comes with two **attributes** `.real` and `.imag` that return the two parts as `float` objects on their own." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x.real" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x.imag" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Also, a `.conjugate()` method is bound to every `complex` object. The [complex conjugate ](https://en.wikipedia.org/wiki/Complex_conjugate) is defined to be the complex number with identical real part but an imaginary part reversed in sign." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1j" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x.conjugate()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [cmath ](https://docs.python.org/3/library/cmath.html) module in the [standard library ](https://docs.python.org/3/library/index.html) implements many of the functions from the [math ](https://docs.python.org/3/library/math.html) module such that they work with complex numbers." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The Numerical Tower" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogous to the discussion of *containers* and *iterables* in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#Containers-vs.-Iterables), we contrast the *concrete* numeric data types in this chapter with the *abstract* ideas behind [numbers in mathematics ](https://en.wikipedia.org/wiki/Number).\n", + "\n", + "The figure below summarizes five *major* sets of [numbers in mathematics ](https://en.wikipedia.org/wiki/Number) as we know them from high school:\n", + "\n", + "- $\\mathbb{N}$: [Natural numbers ](https://en.wikipedia.org/wiki/Natural_number) are all non-negative count numbers, e.g., $0, 1, 2, ...$\n", + "- $\\mathbb{Z}$: [Integers ](https://en.wikipedia.org/wiki/Integer) are all numbers *without* a fractional component, e.g., $-1, 0, 1, ...$\n", + "- $\\mathbb{Q}$: [Rational numbers ](https://en.wikipedia.org/wiki/Rational_number) are all numbers that can be expressed as a quotient of two integers, e.g., $-\\frac{1}{2}, 0, \\frac{1}{2}, ...$\n", + "- $\\mathbb{R}$: [Real numbers ](https://en.wikipedia.org/wiki/Real_number) are all numbers that can be represented as a distance along a line, and negative means \"reversed,\" e.g., $\\sqrt{2}, \\pi, \\text{e}, ...$\n", + "- $\\mathbb{C}$: [Complex numbers ](https://en.wikipedia.org/wiki/Complex_number) are all numbers of the form $a + b\\textbf{i}$ where $a$ and $b$ are real numbers and $\\textbf{i}$ is the [imaginary number ](https://en.wikipedia.org/wiki/Imaginary_number), e.g., $0, \\textbf{i}, 1 + \\textbf{i}, ...$\n", + "\n", + "In the listed order, the five sets are perfect subsets of the respective following sets, and $\\mathbb{C}$ is the largest set. To be precise, all sets are infinite, but they still have a different number of elements." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The data types introduced in this chapter and its appendix are all *(im)perfect* models of *abstract* mathematical ideas.\n", + "\n", + "The `int` and `Fraction` types are the models \"closest\" to the idea they implement: Whereas $\\mathbb{Z}$ and $\\mathbb{Q}$ are, by definition, infinite, every computer runs out of bits when representing sufficiently large integers or fractions with a sufficiently large number of decimals. However, within a system-dependent range, we can model an integer or fraction without any loss in precision.\n", + "\n", + "For the other types, in particular, the `float` type, the implications of their imprecision are discussed in detail above.\n", + "\n", + "The abstract concepts behind the four outer-most mathematical sets are formalized in Python since [PEP 3141 ](https://www.python.org/dev/peps/pep-3141/) in 2007. The [numbers ](https://docs.python.org/3/library/numbers.html) module in the [standard library ](https://docs.python.org/3/library/index.html) defines what programmers call the **[numerical tower ](https://en.wikipedia.org/wiki/Numerical_tower)**, a collection of five **[abstract data types ](https://en.wikipedia.org/wiki/Abstract_data_type)**, or **abstract base classes** (ABCs) as they are called in Python jargon:\n", + "\n", + "- `Number`: \"any number\" (cf., [documentation ](https://docs.python.org/3/library/numbers.html#numbers.Number))\n", + "- `Complex`: \"all complex numbers\" (cf., [documentation ](https://docs.python.org/3/library/numbers.html#numbers.Complex))\n", + "- `Real`: \"all real numbers\" (cf., [documentation ](https://docs.python.org/3/library/numbers.html#numbers.Real))\n", + "- `Rational`: \"all rational numbers\" (cf., [documentation ](https://docs.python.org/3/library/numbers.html#numbers.Rational))\n", + "- `Integral`: \"all integers\" (cf., [documentation ](https://docs.python.org/3/library/numbers.html#numbers.Integral))" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['ABCMeta',\n", + " 'Complex',\n", + " 'Integral',\n", + " 'Number',\n", + " 'Rational',\n", + " 'Real',\n", + " '__all__',\n", + " '__builtins__',\n", + " '__cached__',\n", + " '__doc__',\n", + " '__file__',\n", + " '__loader__',\n", + " '__name__',\n", + " '__package__',\n", + " '__spec__',\n", + " 'abstractmethod']" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Duck Typing (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The primary purpose of ABCs is to classify the *concrete* data types and standardize how they behave. This guides us as the programmers in what kind of behavior we should expect from objects of a given data type. In this context, ABCs are not reflected in code but only in our heads.\n", + "\n", + "For, example, as all numeric data types are `Complex` numbers in the abstract sense, they all work with the built-in [abs() ](https://docs.python.org/3/library/functions.html#abs) function (cf., [documentation ](https://docs.python.org/3/library/numbers.html#numbers.Complex)). While it is intuitively clear what the [absolute value ](https://en.wikipedia.org/wiki/Absolute_value) (i.e., \"distance\" from $0$) of an integer, a fraction, or any real number is, [abs() ](https://docs.python.org/3/library/functions.html#abs) calculates the equivalent of that for complex numbers. That concept is called the [magnitude ](https://en.wikipedia.org/wiki/Magnitude_%28mathematics%29) of a number, and is really a *generalization* of the absolute value.\n", + "\n", + "Relating back to the concept of **duck typing** mentioned in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb#Duck-Typing), `int`, `float`, and `complex` objects \"walk\" and \"quack\" alike in context of the [abs() ](https://docs.python.org/3/library/functions.html#abs) function." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs(-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.87" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs(-42.87)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The absolute value of a `complex` number $x$ is defined with the Pythagorean Theorem where $\\lVert x \\rVert = \\sqrt{a^2 + b^2}$ and $a$ and $b$ are the real and imaginary parts." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5.0" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs(3 + 4j)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, only `Real` numbers in the abstract sense may be rounded with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "round(123, -2)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "round(42.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Complex` numbers are two-dimensional. So, rounding makes no sense here and leads to a `TypeError`. So, in the context of the [round() ](https://docs.python.org/3/library/functions.html#round) function, `int` and `float` objects \"walk\" and \"quack\" alike whereas `complex` objects do not." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "type complex doesn't define __round__ method", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[29], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mround\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43mj\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: type complex doesn't define __round__ method" + ] + } + ], + "source": [ + "round(1 + 2j)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Goose Typing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another way to use ABCs is in place of a *concrete* data type.\n", + "\n", + "For example, we may pass them as arguments to the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function and check in which of the five mathematical sets the object `1 / 10` is." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1 / 10, float)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A `float` object is a generic `Number` in the abstract sense but may also be seen as a `Complex` or `Real` number." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1 / 10, numbers.Number)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1 / 10, numbers.Complex)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1 / 10, numbers.Real)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Due to the `float` type's inherent imprecision, `1 / 10` is *not* a `Rational` number." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1 / 10, numbers.Rational)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, if we model `1 / 10` as a `Fraction` object (cf., [Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/03_appendix.ipynb#The-Fraction-Type)), it is recognized as a `Rational` number." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "from fractions import Fraction" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(Fraction(\"1/10\"), numbers.Rational)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Replacing *concrete* data types with ABCs is particularly valuable in the context of \"type checking:\" The revised version of the `factorial()` function below allows its user to take advantage of *duck typing*: If a real but non-integer argument `n` is passed in, `factorial()` tries to cast `n` as an `int` object with the [int() ](https://docs.python.org/3/library/functions.html#int) built-in.\n", + "\n", + "Two popular and distinguished Pythonistas, [Luciano Ramalho ](https://github.com/ramalho) and [Alex Martelli ](https://en.wikipedia.org/wiki/Alex_Martelli), coin the term **goose typing** to specifically mean using the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function with an ABC (cf., Chapter 11 in this [book](https://www.amazon.com/Fluent-Python-Concise-Effective-Programming/dp/1491946008) or this [summary](https://dgkim5360.github.io/blog/python/2017/07/duck-typing-vs-goose-typing-pythonic-interfaces/) thereof)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Example: [Factorial ](https://en.wikipedia.org/wiki/Factorial) (revisited)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def factorial(n, *, strict=True):\n", + " \"\"\"Calculate the factorial of a number.\n", + "\n", + " Args:\n", + " n (int): number to calculate the factorial for; must be positive\n", + " strict (bool): if n must not contain decimals; defaults to True;\n", + " if set to False, the decimals in n are ignored\n", + "\n", + " Returns:\n", + " factorial (int)\n", + "\n", + " Raises:\n", + " TypeError: if n is not an integer or cannot be cast as such\n", + " ValueError: if n is negative\n", + " \"\"\"\n", + " if not isinstance(n, numbers.Integral):\n", + " if isinstance(n, numbers.Real):\n", + " if n != int(n) and strict:\n", + " raise TypeError(\"n is not integer-like; it has non-zero decimals\")\n", + " n = int(n)\n", + " else:\n", + " raise TypeError(\"Factorial is only defined for integers\")\n", + "\n", + " if n < 0:\n", + " raise ValueError(\"Factorial is not defined for negative integers\")\n", + " elif n == 0:\n", + " return 1\n", + " return n * factorial(n - 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`factorial()` works as before, but now also accepts, for example, `float` numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the keyword-only argument `strict`, we can control whether or not a passed in `float` object may come with decimals that are then truncated. By default, this is not allowed and results in a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "n is not integer-like; it has non-zero decimals", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[41], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m3.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[37], line 19\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n, strict)\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(n, numbers\u001b[38;5;241m.\u001b[39mReal):\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mint\u001b[39m(n) \u001b[38;5;129;01mand\u001b[39;00m strict:\n\u001b[0;32m---> 19\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mn is not integer-like; it has non-zero decimals\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 20\u001b[0m n \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(n)\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n", + "\u001b[0;31mTypeError\u001b[0m: n is not integer-like; it has non-zero decimals" + ] + } + ], + "source": [ + "factorial(3.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In non-strict mode, the passed in `3.1` is truncated into `3` resulting in a factorial of `6`." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "factorial(3.1, strict=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For `complex` numbers, `factorial()` still raises a `TypeError` because they are neither an `Integral` nor a `Real` number." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Factorial is only defined for integers", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[43], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfactorial\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43mj\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[37], line 22\u001b[0m, in \u001b[0;36mfactorial\u001b[0;34m(n, strict)\u001b[0m\n\u001b[1;32m 20\u001b[0m n \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mint\u001b[39m(n)\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m---> 22\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is only defined for integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 24\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m n \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 25\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mFactorial is not defined for negative integers\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mTypeError\u001b[0m: Factorial is only defined for integers" + ] + } + ], + "source": [ + "factorial(1 + 2j)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/05_numbers/03_appendix.ipynb b/05_numbers/03_appendix.ipynb new file mode 100644 index 0000000..4ab003b --- /dev/null +++ b/05_numbers/03_appendix.ipynb @@ -0,0 +1,1508 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/03_appendix.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 5: Numbers & Bits (Appendix)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this appendix, we look at the `Decimal` and `Fraction` types that can be used as replacements for the built-in `float` type mitigating its imprecision. The content is put in an appendix as the data science practitioner can live without knowing about it for quite some time. Eventually, when working with financial data, for example, knowing how to not use the `float` type in a bad way pays off." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `Decimal` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [decimal ](https://docs.python.org/3/library/decimal.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides a [Decimal ](https://docs.python.org/3/library/decimal.html#decimal.Decimal) type that may be used to represent any real number to a user-defined level of precision: \"User-defined\" does *not* mean an infinite or exact precision! The `Decimal` type merely allows us to work with a number of bits *different* from the $64$ as specified for the `float` type and also to customize the rounding rules and some other settings.\n", + "\n", + "We import the `Decimal` type and also the [getcontext() ](https://docs.python.org/3/library/decimal.html#decimal.getcontext) function from the [decimal ](https://docs.python.org/3/library/decimal.html) module." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from decimal import Decimal, getcontext" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[getcontext() ](https://docs.python.org/3/library/decimal.html#decimal.getcontext) shows us how the [decimal ](https://docs.python.org/3/library/decimal.html) module is set up. By default, the precision is set to `28` significant digits, which is roughly twice as many as with `float` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "getcontext()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The two simplest ways to create a `Decimal` object is to either **instantiate** it with an `int` or a `str` object consisting of all the significant digits. In the latter case, the scientific notation is also possible." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('42')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('0.1')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"0.1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('0.001')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"1e-3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is *not* a good idea to create a `Decimal` from a `float` object. If we did so, we would create a `Decimal` object that internally used extra bits to store the \"random\" digits that are not stored in the `float` object in the first place." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('0.1000000000000000055511151231257827021181583404541015625')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(0.1) # do not do this!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the `Decimal` type, the imprecisions in the arithmetic and equality comparisons go away." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('0.3')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"0.1\") + Decimal(\"0.2\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"0.1\") + Decimal(\"0.2\") == Decimal(\"0.3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Decimal` numbers *preserve* the **significant digits**, even in cases where this is not needed." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('0.30000')" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"0.10000\") + Decimal(\"0.20000\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"0.10000\") + Decimal(\"0.20000\") == Decimal(\"0.3\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Arithmetic operations between `Decimal` and `int` objects work as the latter are inherently precise: The results are *new* `Decimal` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('42')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "21 + Decimal(21)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('42.0')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10 * Decimal(\"4.2\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('0.1')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(1) / 10" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To verify the precision, we apply the built-in [format() ](https://docs.python.org/3/library/functions.html#format) function to the previous code cell and compare it with the same division resulting in a `float` object." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.10000000000000000000000000000000000000000000000000'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(Decimal(1) / 10, \".50f\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.10000000000000000555111512312578270211815834045410'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(1 / 10, \".50f\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, mixing `Decimal` and `float` objects raises a `TypeError`: So, Python prevents us from potentially introducing imprecisions via innocent-looking arithmetic by **failing loudly**." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "unsupported operand type(s) for *: 'float' and 'decimal.Decimal'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;241;43m1.0\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mDecimal\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m42\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for *: 'float' and 'decimal.Decimal'" + ] + } + ], + "source": [ + "1.0 * Decimal(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To preserve the precision for more advanced mathematical functions, `Decimal` objects come with many **methods bound** on them. For example, [ln() ](https://docs.python.org/3/library/decimal.html#decimal.Decimal.ln) and [log10() ](https://docs.python.org/3/library/decimal.html#decimal.Decimal.log10) take the logarithm while [sqrt() ](https://docs.python.org/3/library/decimal.html#decimal.Decimal.sqrt) calculates the square root. The methods always return a *new* `Decimal` object. We must never use the functions in the [math ](https://docs.python.org/3/library/math.html) module in the [standard library ](https://docs.python.org/3/library/index.html) with `Decimal` objects as they do *not* preserve precision." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('2')" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(100).log10()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('1.414213562373095048801688724')" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(2).sqrt()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The object returned by the [sqrt() ](https://docs.python.org/3/library/decimal.html#decimal.Decimal.sqrt) method is still limited in precision: This must be so as, for example, $\\sqrt{2}$ is an **[irrational number ](https://en.wikipedia.org/wiki/Irrational_number)** that *cannot* be expressed with absolute precision using *any* number of bits, even in theory.\n", + "\n", + "We see this as raising $\\sqrt{2}$ to the power of $2$ results in an imprecise value as before!" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('1.999999999999999999999999999')" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "two = Decimal(2).sqrt() ** 2\n", + "\n", + "two" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, the [quantize() ](https://docs.python.org/3/library/decimal.html#decimal.Decimal.quantize) method allows us to [quantize](https://www.dictionary.com/browse/quantize) (i.e., \"round\") a `Decimal` number at any precision that is *smaller* than the set precision. It takes the number of decimals to the right of the period of the `Decimal` argument we pass in and rounds accordingly.\n", + "\n", + "For example, as the overall imprecise value of `two` still has an internal precision of `28` digits, we can correctly round it to *four* decimals (i.e., `Decimal(\"0.0000\")` has four decimals)." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('2.0000')" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "two.quantize(Decimal(\"0.0000\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We can never round a `Decimal` number and obtain a greater precision than before: The `InvalidOperation` exception tells us that *loudly*." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "InvalidOperation", + "evalue": "[]", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mInvalidOperation\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[21], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtwo\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mquantize\u001b[49m\u001b[43m(\u001b[49m\u001b[43mDecimal\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m1e-28\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mInvalidOperation\u001b[0m: []" + ] + } + ], + "source": [ + "two.quantize(Decimal(\"1e-28\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Consequently, with this little workaround $\\sqrt{2}^2 = 2$ works, even in Python." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "two.quantize(Decimal(\"0.0000\")) == 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The downside is that the entire expression is not as pretty as `sqrt(2) ** 2 == 2`." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(Decimal(2).sqrt() ** 2).quantize(Decimal(\"0.0000\")) == 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`nan` and positive and negative `inf` exist as well, and the same remarks from the discussion of the `float` type apply." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('NaN')" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"nan\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Decimal(\"nan\")`s never compare equal to anything, not even to themselves." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"nan\") == Decimal(\"nan\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Infinity is larger than any concrete number." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('Infinity')" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('-Infinity')" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"-inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Decimal('Infinity')" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"inf\") + 42" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Decimal(\"inf\") + 42 == Decimal(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with `float` objects, we cannot add infinities of different signs: Now, get a module-specific `InvalidOperation` exception instead of a `nan` value. Here, **failing loudly** is a good thing as it prevents us from working with invalid results." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "InvalidOperation", + "evalue": "[]", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mInvalidOperation\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[30], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mDecimal\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43minf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mDecimal\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m-inf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mInvalidOperation\u001b[0m: []" + ] + } + ], + "source": [ + "Decimal(\"inf\") + Decimal(\"-inf\")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "InvalidOperation", + "evalue": "[]", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mInvalidOperation\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[31], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mDecimal\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43minf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mDecimal\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43minf\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mInvalidOperation\u001b[0m: []" + ] + } + ], + "source": [ + "Decimal(\"inf\") - Decimal(\"inf\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For more information on the `Decimal` type, see the tutorial at [PYMOTW](https://pymotw.com/3/decimal/index.html) or the official [documentation ](https://docs.python.org/3/library/decimal.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `Fraction` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If the numbers in an application can be expressed as [rational numbers ](https://en.wikipedia.org/wiki/Rational_number) (i.e., the set $\\mathbb{Q}$), we may model them as a [Fraction ](https://docs.python.org/3/library/fractions.html#fractions.Fraction) type from the [fractions ](https://docs.python.org/3/library/fractions.html) module in the [standard library ](https://docs.python.org/3/library/index.html). As any fraction can always be formulated as the division of one integer by another, `Fraction` objects are inherently precise, just as `int` objects on their own. Further, we maintain the precision as long as we do not use them in a mathematical operation that could result in an irrational number (e.g., taking the square root).\n", + "\n", + "We import the `Fraction` type from the [fractions ](https://docs.python.org/3/library/fractions.html) module." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from fractions import Fraction" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Among others, there are two simple ways to create a `Fraction` object: We either instantiate one with two `int` objects representing the numerator and denominator or with a `str` object. In the latter case, we have two options again and use either the format \"numerator/denominator\" (i.e., *without* any spaces) or the same format as for `float` and `Decimal` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(1, 3)" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(1, 3) # 1 / 3 with \"full\" precision" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(1, 3)" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(\"1/3\") # 1 / 3 with \"full\" precision" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(3333333333, 10000000000)" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(\"0.3333333333\") # 1 / 3 with a precision of 10 significant digits" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(3333333333, 10000000000)" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(\"3333333333e-10\") # scientific notation is also allowed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Only the lowest common denominator version is maintained after creation: For example, $\\frac{3}{2}$ and $\\frac{6}{4}$ are the same, and both become `Fraction(3, 2)`." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(3, 2)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(3, 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(3, 2)" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(6, 4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We could also cast a `Decimal` object as a `Fraction` object: This only makes sense as `Decimal` objects come with a pre-defined precision." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(1, 10)" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(Decimal(\"0.1\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`float` objects may *syntactically* be cast as `Fraction` objects as well. However, then we create a `Fraction` object that precisely remembers the `float` object's imprecision: A *bad* idea!" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(3602879701896397, 36028797018963968)" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(0.1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Fraction` objects follow the arithmetic rules from middle school and may be mixed with `int` objects *without* any loss of precision. The result is always a *new* `Fraction` object." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(7, 4)" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(3, 2) + Fraction(1, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(1, 2)" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(5, 2) - 2" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(1, 1)" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "3 * Fraction(1, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Fraction(1, 1)" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Fraction(3, 2) * Fraction(2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Fraction` and `float` objects may also be mixed *syntactically*. However, then the results may exhibit imprecision again, even if we do not see them at first sight! This is another example of code **failing silently**." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.1" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10.0 * Fraction(1, 100) # do not do this!" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.10000000000000000555111512312578270211815834045410'" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "format(10.0 * Fraction(1, 100), \".50f\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For more examples and discussions, see the tutorial at [PYMOTW](https://pymotw.com/3/fractions/index.html) or the official [documentation ](https://docs.python.org/3/library/fractions.html)." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/05_numbers/04_summary.ipynb b/05_numbers/04_summary.ipynb new file mode 100644 index 0000000..ad1ebde --- /dev/null +++ b/05_numbers/04_summary.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 5: Numbers & Bits (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There exist three numeric types in core Python:\n", + "- `int`: a near-perfect model for whole numbers (i.e., $\\mathbb{Z}$); inherently precise\n", + "- `float`: the \"gold\" standard to approximate real numbers (i.e., $\\mathbb{R}$); inherently imprecise\n", + "- `complex`: layer on top of the `float` type to approximate complex numbers (i.e., $\\mathbb{C}$); inherently imprecise\n", + "\n", + "Furthermore, the [standard library ](https://docs.python.org/3/library/index.html) provides two more types that can be used as substitutes for the `float` type:\n", + "- `Decimal`: similar to `float` but allows customizing the precision; still inherently imprecise\n", + "- `Fraction`: a near-perfect model for rational numbers (i.e., $\\mathbb{Q}$); built on top of the `int` type and therefore inherently precise\n", + "\n", + "The *important* takeaways for the data science practitioner are:\n", + "\n", + "1. **Do not mix** precise and imprecise data types, and\n", + "2. actively expect `nan` results when working with `float` numbers as there are no **loud failures**.\n", + "\n", + "The **numerical tower** is Python's way of implementing various **abstract** ideas of what numbers are in mathematics." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/05_numbers/05_review.ipynb b/05_numbers/05_review.ipynb new file mode 100644 index 0000000..9c0a424 --- /dev/null +++ b/05_numbers/05_review.ipynb @@ -0,0 +1,327 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 5: Numbers & Bits (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb), [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/01_content.ipynb), and the [third ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/02_content.ipynb) part of Chapter 5. Some questions regard the [Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/03_appendix.ipynb); that is indicated with a **\\***.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Answer the following questions briefly with *at most* 300 characters per question!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: In what way is the **binary representation** of `int` objects *similar* to the **decimal system** taught in elementary school?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Why may objects of type `bool` be regarded a **numeric type** as well?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Colors are commonly expressed in the **hexadecimal system** in websites (cf., the [HTML ](https://en.wikipedia.org/wiki/HTML) and [CSS ](https://en.wikipedia.org/wiki/Cascading_Style_Sheets) formats).\n", + "\n", + "For example, $#000000$, $#ff9900$, and $#ffffff$ turn out to be black, orange, and white. The six digits are read in *pairs of two* from left to right, and the *three pairs* correspond to the proportions of red, green, and blue mixed together. They reach from $0_{16} = 0_{10}$ for $0$% to $\\text{ff}_{16} = 255_{10}$ for $100$% (cf., this [article ](https://en.wikipedia.org/wiki/RGB_color_model) for an in-depth discussion).\n", + "\n", + "In percent, what are the proportions of red, green, and blue that make up orange? Calculate the three percentages separately! How many **bytes** are needed to encode a color? How many **bits** are that?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: What does it mean for a code fragment to **fail silently**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Explain why the mathematical set of all real numbers $\\mathbb{R}$ can only be **approximated** by floating-point numbers on a computer!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: How do we deal with a `float` object's imprecision if we need to **check for equality**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: What data type, built-in or from the [standard library ](https://docs.python.org/3/library/index.html), is best suited to represent the [transcendental numbers ](https://en.wikipedia.org/wiki/Transcendental_number) $\\pi$ and $\\text{e}$?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: How can **abstract base classes**, for example, as defined in the **numerical tower**, be helpful in enabling **duck typing**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: The precision of `int` objects depends on how we choose to represent them in memory. For example, using a **hexadecimal representation** gives us $16^8$ digits whereas with a **binary representation** an `int` object can have *at most* $2^8$ digits." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: With the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function, we obtain a *precise* representation for any `float` object if we can live with *less than* $15$ digits of precision." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: As most currencies operate with $2$ or $3$ decimals (e.g., EUR $9.99$), the `float` type's limitation of *at most* $15$ digits is *not* a problem in practice." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: The [IEEE 754 ](https://en.wikipedia.org/wiki/IEEE_754) standard's **special values** provide no benefit in practice as we could always use a **[sentinel ](https://en.wikipedia.org/wiki/Sentinel_value)** value (i.e., a \"dummy\"). For example, instead of `nan`, we can always use `0` to indicate a *missing* value." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: The following code fragment raises an `InvalidOperation` exception. That is an example of code **failing loudly**.\n", + "```python\n", + "float(\"inf\") + float(\"-inf\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Python provides a `scientific` type (e.g., `1.23e4`) that is useful mainly to model problems in the domains of physics or astrophysics." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q15**: From a practitioner's point of view, the built-in [format() ](https://docs.python.org/3/library/functions.html#format) function does the *same* as the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q16\\***: The `Decimal` type from the [decimal ](https://docs.python.org/3/library/decimal.html) module in the [standard library ](https://docs.python.org/3/library/index.html) allows us to model the set of the real numbers $\\mathbb{R}$ *precisely*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q17\\***: The `Fraction` type from the [fractions ](https://docs.python.org/3/library/fractions.html) module in the [standard library ](https://docs.python.org/3/library/index.html) allows us to model the set of the rational numbers $\\mathbb{Q}$ *precisely*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/05_numbers/06_resources.ipynb b/05_numbers/06_resources.ipynb new file mode 100644 index 0000000..76db27c --- /dev/null +++ b/05_numbers/06_resources.ipynb @@ -0,0 +1,318 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/06_resources.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 5: Numbers & Bits (Further Resources)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with Bits" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The two videos below show how addition and multiplication works with numbers in their binary representations. Subtraction is a bit more involved as we need to understand how negative numbers are represented in binary with the concept of [Two's Complement ](https://en.wikipedia.org/wiki/Two%27s_complement) first. A video on that is shown further below. Division in binary is actually also quite simple." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAUDBAcICAgICAgJCAgIBwgICAgICAgICAgICAgICAgICAgIChALCAgOCQgIDRUODhERExMTCAsXGBYSGBASExIBBQUFBwYHDwkJDxcVEhUXFRMVFxUYFRUVFRUVFRcYFRUVGBUXFRIVFhUXFxcVFRUXGBYVFxUVFRUVFRYVFRUSFf/AABEIAWgB4AMBIgACEQEDEQH/xAAdAAEAAQUBAQEAAAAAAAAAAAAAAQQFBgcIAwIJ/8QAVxAAAQQBAwIDBAQHCQsJCQEAAQACAwQRBQYSEyEHCDEUIkFRCTJhcRUjNkJSgZEzYnJzdHWhtLUWJDQ1Y3aCorGzwjdDRFNVh5LBxCaDk5SjssPR1Bj/xAAbAQEBAQEAAwEAAAAAAAAAAAAAAQIDBAYHBf/EAC8RAQEAAgAEBAIJBQAAAAAAAAABAhEDEiExBAVBUTJxBiJhgZGhsdHwFCNSweH/2gAMAwEAAhEDEQA/AOMkREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCEU4TCCUREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQFUdEdHqd89Thj4Y48h/5qnVZAc1pR+hLE79ThI0/08VrGb/NvCb38qoyiIssCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAAiq6E7mnixjHSPcA1zm8iM9sNDuwz88Jq3DrP444h2PdxjIADsY+HIFb5fq7b5fq7UiKqsRBjGD1e4c3fvWn6jcD4kd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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"RgklPQ8rbkg\", width=\"60%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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OVfTdF7GINmCsLIdz60jfXrt2239Ctd+POs6+odQ5LMVopYIMi+u6OKxw6rRDUr1zz6bnN3LoXHsT2IWDogKw3kCw2Rn1W21VeY6uMpWHZN227ZYrDDFDVP9Z04jlH/lXH4KvKs95ZPHrS2kMNNWlo5Szk71mSzcmhhpCu4tHTq145X2GydBsY5buYSHTS7AjYIOx+US197TkaWnoZN4sQz2y+1pBBu2WbV2P+IdHVcXf/en12G1UF2+s9QWMrkbuStO5T5K3NZl7ucGmZ5eI2cu4jY0hjR8GsaPguoQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQERAgKeP+Sy/Rfh7kMlxk4+zVj62ZmnZw/8GPs6X8vZvr3W7dKaKx2ObtDCJZXNIfYsNbJK7f6TR22Yz7mgffv6rkw4WWXd571T8S8n6fej+LP2x/zfl979FYiEW+daeFVO2HTUS2pOdz0tj7LIfzW7mH8rdx/VWmtQ4C3Ql6NuF0TvxSdiyQD1dHIPde309D8e+ymeGWHl2/S/W+U9Sx/c5fF88b5/3PrHVoiLD9YRNvgiAiIgIiICIiAiljSewBJAJ2Hc7NBc4/kABJ/IoQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQF2mko2vv0WPaHMkvVWva4btc107A5rgexBBIXVrttG/6xx/6QqfvEas8uHmLrg5We1/st3SwV+aB1iCpPLXjcWOlhifI1rmtaSHCMEtADm99tgCtweXGTHCrZG8TbxsHq9QsEprcGdPhv3MXLqb/AH+vwXy8teajDLePc4CTqe1wNP47XNZFKG/e0xxn/wBQ/Yvp49aHidGMnViAmEscdqONoAm672xRy8f+96jmNJ+IfufTv2s8t3or516T6f8AsnLY+q8v8dkvVjfvq+886s8ffX/ihBWnzdmPFx9VryzeOmwyNfOGjrGJkQPL3vXj+NyWqPHn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P+1d9y1iiDYevPGPUeTyd2+zK5GnHdsySQ062QtRQ1od+MMDGRPazZkbWN3AHIgk9ys88G/MOcfi8th9RxXc7SzLXNYX2zJYhbNC6CzH1bRcem5gic0N24ua8/jdtAIgsR4L+YytQxDdO6lxLc5iIhtX3EUk0UbX9SOB8NkdOeNj+7HcmuZsANwG8fj4ueYKjbixVHTeDrYijgclBlazpoIOt7ZXkEsfSjg9ytGX7l7g5z5OwJaAQ6vqILW6k8yekLtirnZdKST6moQMZVmsWR7DDLG4vik3Y/eYRyOc9pdCHj0Dm9iNeeIXjzYt6vrarxVd1OWnWrwitacJWyhkckdiKXp7coZGyvZ22O2xGx220qiC3eT802k5Hty7dItdqSOJoit2BUfHFM1vBj/bA3rvawdgem12w4gt9Rqfxh8cbGX1Hi9SY6F+PuYihUha17hI02IJrM0rgB613+0uj4nuW8t/VacRBbex5mdF5Aw5HM6ObZzdZjQJmspzQufF3jPXn2kDAe4D2PLN+xO251d4deM9TG6ys6okxTYoLnte+Mxzw1sJsxhu7HSjZzi8F7js0F0jiA0bNGmUQbdwHi5Wra7fq51OZ1d967Z9hbIzrhtqpPWa3qEcOQMwcfzSFOV8Xa02vG6vFOYVm3q1n2EyM6/CCpFVLee3DkTGXf37LUKINr+K+uJdW6vgyOJglp2b1jGVcfFK9jpW24zDDA/k0cRvNxI9fRWT+UH12+nh6OAZIDZzUjbF/h2HsdQtIHEndrZbfEj17VnhVA8JdZu09l6uXZUhuS0Oq6GCy57Y+pJE+Jsh6Z35N6hI+8D7iPv4yeId3VGWmy1xjInyxwww1oi50VeGFnFscZf7xBeZJDv+NK78iDDUREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CUUbpuglFG6boJRRum6CEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQf/9k=\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"xHWKYFhhtJQ\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The video below explains the idea behind [Two's Complement ](https://en.wikipedia.org/wiki/Two%27s_complement). This is how most modern programming languages implement negative integers. The video also shows how subtraction in binary works." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"4qH4unVtJkE\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Intuition behind Floating Point Numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This video by the YouTube channel [Computerphile](https://www.youtube.com/channel/UC9-y-6csu5WGm29I7JiwpnA) explains floating point numbers in an intuitive way with some numeric examples." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"PZRI1IfStY0\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## An Introduction to Complex Numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Below is a short introduction to [complex numbers ](https://en.wikipedia.org/wiki/Complex_number) by [MIT](https://www.mit.edu) professor [Gilbert Strang ](https://en.wikipedia.org/wiki/Gilbert_Strang) aimed at high school students." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"Jkv-55ndVYY\", width=\"60%\")" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/05_numbers/static/complex_numbers.png b/05_numbers/static/complex_numbers.png new file mode 100644 index 0000000..807eb46 Binary files /dev/null and b/05_numbers/static/complex_numbers.png differ diff --git a/05_numbers/static/floating_point.png b/05_numbers/static/floating_point.png new file mode 100644 index 0000000..345aaba Binary files /dev/null and b/05_numbers/static/floating_point.png differ diff --git a/05_numbers/static/numbers.png b/05_numbers/static/numbers.png new file mode 100644 index 0000000..6d75726 Binary files /dev/null and b/05_numbers/static/numbers.png differ diff --git a/06_text/00_content.ipynb b/06_text/00_content.ipynb new file mode 100644 index 0000000..57c34fa --- /dev/null +++ b/06_text/00_content.ipynb @@ -0,0 +1,3970 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 6: Text & Bytes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this chapter, we continue the study of the built-in data types. The next layer on top of numbers consists of **textual data** that are modeled primarily with the `str` type in Python. `str` objects are more complex than the numeric objects in [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb) as they *consist* of an *arbitrary* and possibly large number of *individual* characters that may be chosen from *any* alphabet in the history of humankind. Luckily, Python abstracts away most of this complexity from us. However, after looking at the `str` type in great detail, we briefly introduce the `bytes` type at the end of this chapter to understand how characters are modeled in memory." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `str` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To create a `str` object, we use the *literal* notation and type the text between enclosing **double quotes** `\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "text = \"Lorem ipsum dolor sit amet.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Like everything in Python, `text` is an object with an *identity*, a *type*, and a *value*." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140667764715968" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(text)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "str" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As seen before, a `str` object evaluates to itself in a literal notation with enclosing **single quotes** `'`.\n", + "\n", + "In [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Value-/-(Semantic)-\"Meaning\"), we specify the double quotes `\"` convention this book follows. Yet, single quotes `'` and double quotes `\"` are *perfect* substitutes. We could use the reverse convention, as well. As [this discussion ](https://stackoverflow.com/questions/56011/single-quotes-vs-double-quotes-in-python) shows, many programmers have *strong* opinions about such conventions. Consequently, the discussion was \"closed as not constructive\" by the moderators." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the single quote `'` is often used in the English language as a shortener, we could make an argument in favor of using the double quotes `\"`: There are possibly fewer situations like the two code cells below, where we must **escape** the kind of quote used as the `str` object's delimiter with a backslash `\"\\\"` inside the text (cf., also the \"*Unicode & (Special) Characters*\" section further below). However, double quotes `\"` are often used as well, for example, to indicate a quote like the one by [Albert Einstein](https://de.wikipedia.org/wiki/Albert_Einstein) below. So, such arguments are not convincing.\n", + "\n", + "Many proponents of the single quote `'` usage claim that double quotes `\"` cause more **visual noise** on the screen. However, this argument is also not convincing as, for example, one could claim that *two* single quotes `''` look so similar to *one* double quote `\"` that a reader may confuse an *empty* `str` object with a missing closing quote `\"`. With the double quotes `\"` convention we at least avoid such confusion (i.e., empty `str` objects are written as `\"\"`).\n", + "\n", + "This discussion is an excellent example of a [flame war ](https://en.wikipedia.org/wiki/Flaming_%28Internet%29#Flame_war) in the programming world: Everyone has an opinion and the discussion leads to *no* result." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Einstein said, \"If you can\\'t explain it, you don\\'t understand it.\"'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Einstein said, \\\"If you can't explain it, you don't understand it.\\\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Einstein said, \"If you can\\'t explain it, you don\\'t understand it.\"'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "'Einstein said, \"If you can\\'t explain it, you don\\'t understand it.\"'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "An *important* fact to know is that enclosing quotes of either kind are *not* part of the `str` object's *value*! They are merely *syntax* indicating the literal notation.\n", + "\n", + "So, printing out the sentence with the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function does the same in both cases." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Einstein said, \"If you can't explain it, you don't understand it.\"\n" + ] + } + ], + "source": [ + "print(\"Einstein said, \\\"If you can't explain it, you don't understand it.\\\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Einstein said, \"If you can't explain it, you don't understand it.\"\n" + ] + } + ], + "source": [ + "print('Einstein said, \"If you can\\'t explain it, you don\\'t understand it.\"')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As an alternative to the literal notation, we may use the built-in [str() ](https://docs.python.org/3/library/stdtypes.html#str) constructor to cast non-`str` objects as `str` ones. As [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) reveals, basically any object in Python has a **text representation**. Because of that we may also pass `list` objects, the boolean `True` and `False`, or `None` to [str() ](https://docs.python.org/3/library/stdtypes.html#str)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'42'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'42.87'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(42.87)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'[1, 2, 3]'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'True'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'False'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(False)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'None'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(None)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### User Input" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As shown in the \"*Guessing a Coin Toss*\" example in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-Guessing-a-Coin-Toss), the built-in [input() ](https://docs.python.org/3/library/functions.html#input) function displays a prompt to the user and returns whatever is entered as a `str` object. [input() ](https://docs.python.org/3/library/functions.html#input) is in particular valuable when writing command-line tools." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "Whatever you enter is put in a new string: 123\n" + ] + } + ], + "source": [ + "user_input = input(\"Whatever you enter is put in a new string: \")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "str" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(user_input)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'123'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "user_input" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Reading Files" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A more common situation where we obtain `str` objects is when reading the contents of a file with the [open() ](https://docs.python.org/3/library/functions.html#open) built-in. In its simplest usage form, to open a [text file ](https://en.wikipedia.org/wiki/Text_file) file, we pass in its path (i.e., \"filename\") as a `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "file = open(\"lorem_ipsum.txt\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[open() ](https://docs.python.org/3/library/functions.html#open) returns a **[proxy ](https://en.wikipedia.org/wiki/Proxy_pattern)** object of type `TextIOWrapper` that allows us to interact with the file on disk. `mode='r'` shows that we opened the file in read-only mode and `encoding='UTF-8'` is explained in detail in the [The `bytes` Type ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/01_content.ipynb#The-bytes-Type) section at the end of this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "_io.TextIOWrapper" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(file)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "<_io.TextIOWrapper name='lorem_ipsum.txt' mode='r' encoding='UTF-8'>" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`TextIOWrapper` objects come with plenty of type-specific methods and attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.readable()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.writable()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'lorem_ipsum.txt'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.name" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'UTF-8'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.encoding" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, we have not yet read anything from the file (i.e., from disk)! That is intentional as, for example, the file could contain more data than could fit into our computer's memory. Therefore, we have to explicitly instruct the `file` object to read some of or all the data in the file.\n", + "\n", + "One way to do that, is to simply loop over the `file` object with the `for` statement as shown next: In each iteration, `line` is assigned the next line in the file. Because we may loop over `TextIOWrapper` objects, they are *iterables*." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.\n", + "\n", + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s\n", + "\n", + "when an unknown printer took a galley of type and scrambled it to make a type\n", + "\n", + "specimen book. It has survived not only five centuries but also the leap into\n", + "\n", + "electronic typesetting, remaining essentially unchanged. It was popularised in\n", + "\n", + "the 1960s with the release of Letraset sheets.\n", + "\n" + ] + } + ], + "source": [ + "for line in file:\n", + " print(line)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Once we looped over the `file` object, it is **exhausted**: We can *not* loop over it a second time. So, the built-in [print() ](https://docs.python.org/3/library/functions.html#print) function is *never* called in the code cell below!" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "for line in file:\n", + " print(line)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After the `for`-loop, the `line` variable is still set and references the *last* line in the file. We verify that it is indeed a `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'the 1960s with the release of Letraset sheets.\\n'" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "line" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "str" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(line)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "An *important* observation is that the `file` object is still associated with an *open* **[file descriptor ](https://en.wikipedia.org/wiki/File_descriptor)**. Without going into any technical details, we note that an operating system can only handle a *limited* number of \"open files\" at the same time, and, therefore, we should always *close* the file once we are done processing it.\n", + "\n", + "`TextIOWrapper` objects have a `closed` attribute on them that indicates if the associated file descriptor is still open or has been closed. We can \"manually\" close any `TextIOWrapper` object with the [close() ](https://docs.python.org/3/library/io.html#io.IOBase.close) method." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.closed" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "file.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.closed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The more Pythonic way is to use [open() ](https://docs.python.org/3/library/functions.html#open) within the compound `with` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement)): In the example below, the indented code block is said to be executed within the **context** of the `file` object that now plays the role of a **[context manager ](https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers)**. Many different kinds of context managers exist in Python with different applications and purposes. Context managers returned from [open() ](https://docs.python.org/3/library/functions.html#open) mainly ensure that file descriptors get automatically closed after the last line in the code block is executed." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.\n", + "\n", + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s\n", + "\n", + "when an unknown printer took a galley of type and scrambled it to make a type\n", + "\n", + "specimen book. It has survived not only five centuries but also the leap into\n", + "\n", + "electronic typesetting, remaining essentially unchanged. It was popularised in\n", + "\n", + "the 1960s with the release of Letraset sheets.\n", + "\n" + ] + } + ], + "source": [ + "with open(\"lorem_ipsum.txt\") as file:\n", + " for line in file:\n", + " print(line)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.closed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Using syntax familiar from [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#The-try-Statement) to explain what the `with open(...) as file:` does above, we provide an alternative formulation with a `try` statement below: The `finally`-branch is *always* executed, even if an exception is raised inside the `for`-loop. Therefore, `file` is sure to be closed too. However, this formulation is somewhat less expressive." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.\n", + "\n", + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s\n", + "\n", + "when an unknown printer took a galley of type and scrambled it to make a type\n", + "\n", + "specimen book. It has survived not only five centuries but also the leap into\n", + "\n", + "electronic typesetting, remaining essentially unchanged. It was popularised in\n", + "\n", + "the 1960s with the release of Letraset sheets.\n", + "\n" + ] + } + ], + "source": [ + "try:\n", + " file = open(\"lorem_ipsum.txt\")\n", + " for line in file:\n", + " print(line)\n", + "finally:\n", + " file.close()" + ] + }, + { + "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": [ + "file.closed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As an alternative to reading the contents of a file by looping over a `TextIOWrapper` object, we may also call one of the methods they come with.\n", + "\n", + "For example, the [read() ](https://docs.python.org/3/library/io.html#io.TextIOBase.read) method takes a single `size` argument of type `int` and returns a `str` object with the specified number of characters." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "file = open(\"lorem_ipsum.txt\")" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem Ipsum'" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.read(11)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When we call [read() ](https://docs.python.org/3/library/io.html#io.TextIOBase.read) again, the returned `str` object begins where the previous one left off. This is because `TextIOWrapper` objects like `file` simply store a position at which the associated file on disk is being read. In other words, `file` is like a **cursor** pointing into a file." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "' is simply '" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.read(11)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, the [readline() ](https://docs.python.org/3/library/io.html#io.TextIOBase.readline) method keeps reading until it hits a **newline character**. These are shown in `str` objects as `\"\\n\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'dummy text of the printing and typesetting industry.\\n'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.readline()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When we call [readline() ](https://docs.python.org/3/library/io.html#io.TextIOBase.readline) again, we obtain the next line." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s\\n\"" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.readline()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, the [readlines() ](https://docs.python.org/3/library/io.html#io.IOBase.readlines) method returns a `list` object that holds *all* lines in the `file` from the current position to the end of the file. The latter position is often abbreviated as **EOF** in the documentation. Let's always remember that [readlines() ](https://docs.python.org/3/library/io.html#io.IOBase.readlines) has the potential to crash a computer with a `MemoryError`." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['when an unknown printer took a galley of type and scrambled it to make a type\\n',\n", + " 'specimen book. It has survived not only five centuries but also the leap into\\n',\n", + " 'electronic typesetting, remaining essentially unchanged. It was popularised in\\n',\n", + " 'the 1960s with the release of Letraset sheets.\\n']" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.readlines()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Calling [readlines() ](https://docs.python.org/3/library/io.html#io.IOBase.readlines) a second time, is as pointless as looping over `file` a second time." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "file.readlines()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "file.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because every `str` object created by reading the contents of a file in any of the ways shown in this section ends with a `\"\\n\"`, we see empty lines printed between each `line` in the `for`-loops above. To print the entire text without empty lines in between, we pass a `end=\"\"` argument to the [print() ](https://docs.python.org/3/library/functions.html#print) function." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.\n", + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s\n", + "when an unknown printer took a galley of type and scrambled it to make a type\n", + "specimen book. It has survived not only five centuries but also the leap into\n", + "electronic typesetting, remaining essentially unchanged. It was popularised in\n", + "the 1960s with the release of Letraset sheets.\n" + ] + } + ], + "source": [ + "with open(\"lorem_ipsum.txt\") as file:\n", + " for line in file:\n", + " print(line, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## A String of Characters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A **sequence** is yet another *abstract* concept (cf., the \"*Containers vs. Iterables*\" section in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#Containers-vs.-Iterables)).\n", + "\n", + "It unifies *four* [orthogonal ](https://en.wikipedia.org/wiki/Orthogonality) (i.e., \"independent\") concepts into one bigger idea: Any data type, such as `str`, is considered a sequence if it\n", + "\n", + "1. **contains**\n", + "2. a **finite** number of other objects that\n", + "3. can be **iterated** over\n", + "4. in a *predictable* **order**.\n", + "\n", + "[Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Collections-vs.-Sequences) formalizes these concepts in great detail. Here, we keep our focus on the `str` type that historically received its name as it models a **[string of characters ](https://en.wikipedia.org/wiki/String_%28computer_science%29)**. *String* is simply another term for *sequence* in the computer science literature.\n", + "\n", + "Another example of a sequence is the `list` type. Because of that, `str` objects may be treated like `list` objects in many situations.\n", + "\n", + "Below, the built-in [len() ](https://docs.python.org/3/library/functions.html#len) function tells us how many characters make up `text`. [len() ](https://docs.python.org/3/library/functions.html#len) would not work with an \"infinite\" object. As anything modeled in a program must fit into a computer's finite memory, there cannot exist truly infinite objects; however, [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb#Iterators-vs.-Iterables) introduces specialized iterable data types that can be used to model an *infinite* series of \"things\" and that, consequently, have no concept of \"length.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "27" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(text)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Being iterable, we may loop over `text` and do something with the individual characters, for example, print them out with extra space in between them. If it were not for the appropriately chosen name of the `text` variable, we could not tell what *concrete* type of object the `for` statement is looping over." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "L o r e m i p s u m d o l o r s i t a m e t . " + ] + } + ], + "source": [ + "for character in text:\n", + " print(character, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in, we may loop over `text` in reversed order. Reversing `text` only works as it has a forward order to begin with." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". t e m a t i s r o l o d m u s p i m e r o L " + ] + } + ], + "source": [ + "for character in reversed(text):\n", + " print(character, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Being a container, we may check if a given `str` object is contained in `text` with the `in` operator, which has *two* distinct usages: First, it checks if a *single* character is contained in a `str` object. Second, it may also check if a shorter `str` object, then called a **substring**, is contained in a longer one." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"L\" in text" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"ipsum\" in text" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"veni, vidi, vici\" in text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Indexing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As `str` objects are *ordered* and *finite*, we may **index** into them to obtain individual characters with the **indexing operator** `[]`. This is analogous to how we obtained individual elements of a `list` object in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Who-am-I?-And-how-many?)." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'L'" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'o'" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The index must be of type `int`; othewise, we get a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "string indices must be integers, not 'float'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[54], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtext\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m1.0\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: string indices must be integers, not 'float'" + ] + } + ], + "source": [ + "text[1.0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The last index is one less than the above \"length\" of the `str` object as we start counting at `0`." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'.'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[26] # == text[len(text) - 1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "An `IndexError` is raised whenever the index is out of range." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "IndexError", + "evalue": "string index out of range", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[56], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtext\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m27\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;66;03m# == text[len(text)]\u001b[39;00m\n", + "\u001b[0;31mIndexError\u001b[0m: string index out of range" + ] + } + ], + "source": [ + "text[27] # == text[len(text)]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may use *negative* indexes to start counting from the end of the `str` object, as shown in the figure below. Note how this only works because sequences are *finite*." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "| Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13| 14| 15| 16| 17| 18| 19| 20| 21| 22| 23| 24| 25| 26|\n", + "|:---------:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n", + "|**Reverse**|-27|-26|-25|-24|-23|-22|-21|-20|-19|-18|-17|-16|-15|-14|-13|-12|-11|-10|-9 |-8 |-7 |-6 |-5 |-4 |-3 |-2 |-1 |\n", + "| **Character** |`L`|`o`|`r`|`e`|`m`|` `|`i`|`p`|`s`|`u`|`m`|` `|`d`|`o`|`l`|`o`|`r`|` `|`s`|`i`|`t`|` `|`a`|`m`|`e`|`t`|`.`|" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'.'" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'L'" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[-27] # == text[-len(text)]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "One reason why programmers like to start counting at `0` is that a positive index and its *corresponding* negative index always add up to the length of the sequence. Here, `6` and `21` add to `27`." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'i'" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[6]" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'i'" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[-21]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Slicing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A **slice** is a substring of a `str` object.\n", + "\n", + "The **slicing operator** is a generalization of the indexing operator: We put one, two, or three integers within the brackets `[]`, separated by colons `:`. The three integers are then referred to as the *start*, *stop*, and *step* values.\n", + "\n", + "Let's start with two integers, *start* and *stop*. Whereas the character at the *start* position is included in the returned `str` object, the one at the *stop* position is not. If both *start* and *stop* are positive, the difference \"*stop* minus *start*\" tells us how many characters the resulting slice has. So, below, `5 - 0 == 5` implies that `\"Lorem\"` consists of `5` characters. So, colloquially speaking, `text[0:5]` means \"taking the first `5 - 0 == 5` characters of `text`.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem'" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[0:5]" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'dolor sit amet.'" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[12:len(text)]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If left out, *start* defaults to `0` and *stop* to the length of the `str` object (i.e., the end)." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem'" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[:5]" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'dolor sit amet.'" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[12:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Not including the character at the *stop* position makes working with individual slices easier as they add up to the original `str` object again (cf., the \"*String Operations*\" section below regarding the overloaded `+` operator)." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[:5] + text[5:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Slicing and indexing makes it easy to obtain shorter versions of the original `str` object. A common application would be to **parse** out meaningful substrings from raw text data." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum sit amet.'" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[:11] + text[-10:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By combining a positive *start* with a negative *stop* index, we specify both ends of the slice *relative* to the ends of the entire `str` object. So, colloquially speaking, `[6:-10]` below means \"drop the first six and last ten characters.\" The length of the resulting slice can then *not* be calculated from the indexes and depends only on the length of the original `str` object!" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'ipsum dolor'" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[6:-10]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For convenience, the indexes do not need to lie within the range from `0` to `len(text)` when slicing. So, no `IndexError` is raised here." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[-999:999]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By leaving out both *start* and *stop*, we take a \"full\" slice that is essentially a *copy* of the original `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A *step* value of `i` can be used to obtain only every `i`th character." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lrmismdlrstae.'" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[::2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A negative *step* size of `-1` reverses the order of the characters." + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'.tema tis rolod muspi meroL'" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text[::-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Immutability" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whereas elements of a `list` object *may* be *re-assigned*, as shortly hinted at in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Who-am-I?-And-how-many?), this is *not* allowed for the individual characters of `str` objects. Once created, they can *not* be changed. Formally, we say that `str` objects are **immutable**. In that regard, they are like the numeric types in [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb).\n", + "\n", + "On the contrary, objects that may be changed after creation, are called **mutable**. We already saw in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Who-am-I?-And-how-many?) how mutable objects are more difficult to reason about for a beginner, in particular, if more than one variable references it. Yet, mutability does have its place in a programmer's toolbox, and we revisit this idea in the next chapters.\n", + "\n", + "The `TypeError` indicates that `str` objects are *immutable*: Assignment to an index or a slice are *not* supported." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'str' object does not support item assignment", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[72], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtext\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mX\u001b[39m\u001b[38;5;124m\"\u001b[39m\n", + "\u001b[0;31mTypeError\u001b[0m: 'str' object does not support item assignment" + ] + } + ], + "source": [ + "text[0] = \"X\"" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'str' object does not support item assignment", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[73], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mtext\u001b[49m\u001b[43m[\u001b[49m\u001b[43m:\u001b[49m\u001b[38;5;241;43m5\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrandom\u001b[39m\u001b[38;5;124m\"\u001b[39m\n", + "\u001b[0;31mTypeError\u001b[0m: 'str' object does not support item assignment" + ] + } + ], + "source": [ + "text[:5] = \"random\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## String Methods" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Objects of type `str` come with many **methods** bound on them (cf., the [documentation ](https://docs.python.org/3/library/stdtypes.html#string-methods) for a full list). As seen before, they work like *normal* functions and are accessed via the **dot operator** `.`. Calling a method is also referred to as **method invocation**.\n", + "\n", + "The [.find() ](https://docs.python.org/3/library/stdtypes.html#str.find) method returns the index of the first occurrence of a character or a substring. If no match is found, it returns `-1`. A mirrored version searching from the right called [.rfind() ](https://docs.python.org/3/library/stdtypes.html#str.rfind) exists as well. The [.index() ](https://docs.python.org/3/library/stdtypes.html#str.index) and [.rindex() ](https://docs.python.org/3/library/stdtypes.html#str.rindex) methods work in the same way but raise a `ValueError` if no match is found. So, we can control if a search fails *silently* or *loudly*." + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "22" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.find(\"a\")" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.find(\"b\")" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.find(\"dolor\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[.find() ](https://docs.python.org/3/library/stdtypes.html#str.find) takes optional *start* and *end* arguments that allow us to find occurrences other than the first one." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.find(\"o\")" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "13" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.find(\"o\", 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.find(\"o\", 2, 12)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [.count() ](https://docs.python.org/3/library/stdtypes.html#str.count) method does what we expect." + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.count(\"l\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As [.count() ](https://docs.python.org/3/library/stdtypes.html#str.count) is *case-sensitive*, we must **chain** it with the [.lower() ](https://docs.python.org/3/library/stdtypes.html#str.lower) method to get the count of all `\"L\"`s and `\"l\"`s." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.lower().count(\"l\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we can use the [.upper() ](https://docs.python.org/3/library/stdtypes.html#str.upper) method and search for `\"L\"`s." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.upper().count(\"L\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because `str` objects are *immutable*, [.upper() ](https://docs.python.org/3/library/stdtypes.html#str.upper) and [.lower() ](https://docs.python.org/3/library/stdtypes.html#str.lower) return *new* `str` objects, even if they do *not* change the value of the original `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "example = \"random\"" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140667840152112" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(example)" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "lower = example.lower()" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140667764453680" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(lower)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`example` and `lower` are *different* objects with the *same* value." + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 89, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "example is lower" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "example == lower" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Besides [.upper() ](https://docs.python.org/3/library/stdtypes.html#str.upper) and [.lower() ](https://docs.python.org/3/library/stdtypes.html#str.lower) there exist also [.title() ](https://docs.python.org/3/library/stdtypes.html#str.title) and [.swapcase() ](https://docs.python.org/3/library/stdtypes.html#str.swapcase) methods." + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'lorem ipsum dolor sit amet.'" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.lower()" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'LOREM IPSUM DOLOR SIT AMET.'" + ] + }, + "execution_count": 92, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.upper()" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem Ipsum Dolor Sit Amet.'" + ] + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.title()" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'lOREM IPSUM DOLOR SIT AMET.'" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.swapcase()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another popular string method is [.split() ](https://docs.python.org/3/library/stdtypes.html#str.split): It separates a longer `str` object into smaller ones collected in a `list` object. By default, groups of contiguous whitespace characters are used as the *separator*.\n", + "\n", + "As an example, we use [.split() ](https://docs.python.org/3/library/stdtypes.html#str.split) to print out the individual words in `text` with more whitespace in between them." + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Lorem', 'ipsum', 'dolor', 'sit', 'amet.']" + ] + }, + "execution_count": 95, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "text.split()" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lorem ipsum dolor sit amet. " + ] + } + ], + "source": [ + "for word in text.split():\n", + " print(word, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The opposite of splitting is done with the [.join() ](https://docs.python.org/3/library/stdtypes.html#str.join) method. It is typically invoked on a `str` object that represents a separator (e.g., `\" \"` or `\", \"`) and connects the elements provided by an *iterable* argument (e.g., `words` below) into one *new* `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "words = [\"This\", \"will\", \"become\", \"a\", \"sentence.\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "sentence = \" \".join(words)" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'This will become a sentence.'" + ] + }, + "execution_count": 99, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sentence" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the `str` object `\"abcde\"` below is an *iterable* itself, its characters (!) are joined together with a space `\" \"` in between." + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'a b c d e'" + ] + }, + "execution_count": 100, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\" \".join(\"abcde\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [.replace() ](https://docs.python.org/3/library/stdtypes.html#str.replace) method creates a *new* `str` object with parts of the original `str` object potentially replaced." + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'This is a sentence.'" + ] + }, + "execution_count": 101, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sentence.replace(\"will become\", \"is\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Note how `sentence` itself remains unchanged. Bound to an immutable object, [.replace() ](https://docs.python.org/3/library/stdtypes.html#str.replace) must create *new* objects." + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'This will become a sentence.'" + ] + }, + "execution_count": 102, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sentence" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As seen previously, the [.strip() ](https://docs.python.org/3/library/stdtypes.html#str.strip) method is often helpful in cleaning text data from unreliable sources like user input from unnecessary leading and trailing whitespace. The [.lstrip() ](https://docs.python.org/3/library/stdtypes.html#str.lstrip) and [.rstrip() ](https://docs.python.org/3/library/stdtypes.html#str.rstrip) methods are specialized versions of it." + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'text with whitespace'" + ] + }, + "execution_count": 103, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\" text with whitespace \".strip()" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'text with whitespace '" + ] + }, + "execution_count": 104, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\" text with whitespace \".lstrip()" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "' text with whitespace'" + ] + }, + "execution_count": 105, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\" text with whitespace \".rstrip()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When justifying a `str` object for output, the [.ljust() ](https://docs.python.org/3/library/stdtypes.html#str.ljust) and [.rjust() ](https://docs.python.org/3/library/stdtypes.html#str.rjust) methods may be helpful." + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'This will become a sentence. '" + ] + }, + "execution_count": 106, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sentence.ljust(40)" + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "' This will become a sentence.'" + ] + }, + "execution_count": 107, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sentence.rjust(40)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, the [.zfill() ](https://docs.python.org/3/library/stdtypes.html#str.zfill) method can be used to pad a `str` representation of a number with leading `0`s for justified output." + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0000042.87'" + ] + }, + "execution_count": 108, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"42.87\".zfill(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'-000042.87'" + ] + }, + "execution_count": 109, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"-42.87\".zfill(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## String Operations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As mentioned in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Operator-Overloading), the `+` and `*` operators are *overloaded* and used for **string concatenation**. They always create *new* `str` objects. That has nothing to do with the `str` type's immutability, but is the default behavior of operators." + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello Lore'" + ] + }, + "execution_count": 110, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Hello \" + text[:4]" + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum ...'" + ] + }, + "execution_count": 111, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "5 * text[:12] + \"...\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### String Comparison" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The *relational* operators also work with `str` objects, another example of operator overloading. Comparison is done one character at a time in a pairwise fashion until the first pair differs or one operand ends. However, `str` objects are sorted in a \"weird\" way. For example, all upper case characters come before all lower case characters. The reason for that is given in the \"*Characters are Numbers with a Convention*\" sub-section in the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/02_content.ipynb#Characters-are-Numbers-with-a-Convention) of this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 112, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Apple\" < \"Banana\"" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 113, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"apple\" < \"Banana\"" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 114, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"apple\" < \"Banana\".lower()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Below is an example with typical German last names that shows how characters other than the first decide the ordering." + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 115, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Mai\" < \"Maier\" < \"Mayer\" < \"Meier\" < \"Meyer\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## String Interpolation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Often, we want to use `str` objects as drafts in the source code that are filled in with concrete text only at runtime. This approach is called **string interpolation**. There are three ways to do that in Python." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### f-strings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**[Formatted string literals ](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals)**, of **f-strings** for short, are the least recently added (cf., [PEP 498 ](https://www.python.org/dev/peps/pep-0498/) in 2016) and most readable way: We simply prepend a `str` in its literal notation with an `f`, and put variables, or more generally, expressions, within curly braces `{}`. These are then filled in when the string literal is evaluated." + ] + }, + { + "cell_type": "code", + "execution_count": 116, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "name = \"Alexander\"\n", + "time_of_day = \"morning\"" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello Alexander! Good morning.'" + ] + }, + "execution_count": 117, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "f\"Hello {name}! Good {time_of_day}.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Separated by a colon `:`, various formatting options are available. In the beginning, the ability to round numbers for output may be particularly useful: This can be achieved by adding `:.2f` to the variable name inside the curly braces, which casts the number as a `float` and rounds it to two digits. The `:.2f` is a so-called format specifier, and there exists a whole **[format specification mini-language ](https://docs.python.org/3/library/string.html#formatspec)** to govern how specifiers work." + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "pi = 3.141592653" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Pi is 3.14'" + ] + }, + "execution_count": 119, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "f\"Pi is {pi:.2f}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### [format() ](https://docs.python.org/3/library/stdtypes.html#str.format) Method" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`str` objects also provide a [.format() ](https://docs.python.org/3/library/stdtypes.html#str.format) method that accepts an arbitrary number of *positional* arguments that are inserted into the `str` object in the same order replacing empty curly brackets `{}`. String interpolation with the [.format() ](https://docs.python.org/3/library/stdtypes.html#str.format) method is a more traditional and probably the most common way as of today. While f-strings are the recommended way going forward, usage of the [.format() ](https://docs.python.org/3/library/stdtypes.html#str.format) method is likely not declining any time soon." + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello Alexander! Good morning.'" + ] + }, + "execution_count": 120, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Hello {}! Good {}.\".format(name, time_of_day)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may use index numbers inside the curly braces if the order is different in the `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Good morning, Alexander'" + ] + }, + "execution_count": 121, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Good {1}, {0}\".format(name, time_of_day)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [.format() ](https://docs.python.org/3/library/stdtypes.html#str.format) method may alternatively be used with *keyword* arguments as well. Then, we must put the keywords' names within the curly brackets." + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello Alexander! Good morning.'" + ] + }, + "execution_count": 122, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Hello {name}! Good {time}.\".format(name=name, time=time_of_day)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Format specifiers work as in the f-string case." + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Pi is 3.14'" + ] + }, + "execution_count": 123, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Pi is {:.2f}\".format(pi)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### `%` Operator" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `%` operator that we saw in the context of modulo division in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#%28Arithmetic%29-Operators) is overloaded with string interpolation when its first operand is a `str` object. The second operand consists of all expressions to be filled in. Format specifiers work with a `%` instead of curly braces and according to a different set of rules referred to as **[printf-style string formatting ](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting)**. So, `{:.2f}` becomes `%.2f`.\n", + "\n", + "This way of string interpolation is the oldest and originates from the [C language ](https://en.wikipedia.org/wiki/C_%28programming_language%29). It is still widely spread, but we should use one of the other two ways instead. We show it here mainly for completeness sake." + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Pi is 3.14'" + ] + }, + "execution_count": 124, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Pi is %.2f\" % pi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To insert more than one expression, we must list them in order and between parenthesis `(` and `)`. As [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#The-tuple-Type) reveals, this literal syntax creates an object of type `tuple`. Also, to format an expression as text, we use the format specifier `%s`." + ] + }, + { + "cell_type": "code", + "execution_count": 125, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello Alexander! Good morning.'" + ] + }, + "execution_count": 125, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Hello %s! Good %s.\" % (name, time_of_day)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/06_text/01_exercises_palindromes.ipynb b/06_text/01_exercises_palindromes.ipynb new file mode 100644 index 0000000..b8a8c33 --- /dev/null +++ b/06_text/01_exercises_palindromes.ipynb @@ -0,0 +1,995 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 6: Text & Bytes (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb) of Chapter 6.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Detecting Palindromes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Palindromes ](https://en.wikipedia.org/wiki/Palindrome) are sequences of characters that read the same backward as forward. Examples are first names like \"Hannah\" or \"Otto,\" words like \"radar\" or \"level,\" or sentences like \"Was it a car or a cat I saw?\"\n", + "\n", + "In this exercise, you implement various functions that check if the given arguments are palindromes or not. We start with an iterative implementation and end with a recursive one.\n", + "\n", + "Conceptually, the first function, `unpythonic_palindrome()`, is similar to the \"*Is the square of a number in `[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]` greater than `100`?*\" example in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-Is-the-square-of-a-number-in-[7,-11,-8,-5,-3,-12,-2,-6,-9,-10,-1,-4]-greater-than-100?): It assumes that the `text` argument is a palindrome (i.e., it initializes `is_palindrom` to `True`) and then checks in a `for`-loop if a pair of corresponding characters, `forward` and `backward`, contradicts that.\n", + "\n", + "**Q1**: How many iterations are needed in the `for`-loop? Take into account that `text` may contain an even or odd number of characters! Inside `unpythonic_palindrome()` below, write an expression whose result is assigned to `chars_to_check`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer > " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: `forward_index` is the index going from left to right. How can we calculate `backward_index`, the index going from right to left, for a given `forward_index`? Write an expression whose result is assigned to `backward_index`! Then, use the indexing operator `[]` to obtain the two characters, `forward` and `backward`, from `text`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Finish `unpythonic_palindrome()` below! Add code that adjusts `text` such that the function is case insensitive if the `ignore_case` argument is `True`! Make sure that the function returns once the first pair of corresponding characters does not match!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def unpythonic_palindrome(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answer to Q3\n", + " is_palindrome = ...\n", + " if ignore_case:\n", + " ...\n", + " # answer to Q1\n", + " chars_to_check = ...\n", + "\n", + " for forward_index in range(chars_to_check):\n", + " # answer to Q2\n", + " backward_index = ...\n", + " forward = ...\n", + " backward = ...\n", + "\n", + " print(forward, \"and\", backward) # added for didactical purposes\n", + "\n", + " # answer to Q3\n", + " if ...:\n", + " is_palindrome = ...\n", + " ...\n", + "\n", + " return is_palindrome" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Ensure that `unpythonic_palindrome()` works for the provided test cases (i.e., the `assert` statements do *not* raise an `AssertionError`)! Also, for each of the test cases, provide a brief explanation of what makes them *unique*!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert unpythonic_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 1 >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert unpythonic_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 2 >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert unpythonic_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 3 >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert unpythonic_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 4 >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert unpythonic_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 5 >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert unpythonic_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your explanation 6 >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`unpythonic_palindrome()` is considered *not* Pythonic as it uses index variables to implement the looping logic. Instead, we should simply loop over an *iterable* object to work with its elements one by one.\n", + "\n", + "**Q5**: Copy your solutions to the previous questions into `almost_pythonic_palindrome()` below!\n", + "\n", + "**Q6**: The [reversed() ](https://docs.python.org/3/library/functions.html#reversed) and [zip() ](https://docs.python.org/3/library/functions.html#zip) built-ins allow us to loop over the same `text` argument *in parallel* in both forward *and* backward order. Finish the `for` statement's header line to do just that!\n", + "\n", + "Hint: You may need to slice the `text` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def almost_pythonic_palindrome(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " is_palindrome = ...\n", + " if ignore_case:\n", + " ...\n", + " chars_to_check = ...\n", + "\n", + " # answer to Q6\n", + " for ... in ...:\n", + "\n", + " print(forward, \"and\", backward) # added for didactical purposes\n", + "\n", + " # answers from above\n", + " if ...:\n", + " is_palindrome = ...\n", + " ...\n", + "\n", + " return is_palindrome" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Verify that the test cases work as before!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert almost_pythonic_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert almost_pythonic_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert almost_pythonic_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert almost_pythonic_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert almost_pythonic_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert almost_pythonic_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: `almost_pythonic_palindrome()` above may be made more Pythonic by removing the variable `is_palindrome` with the *early exit* pattern. Make the corresponding changes!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def pythonic_palindrome(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " if ignore_case:\n", + " ...\n", + " chars_to_check = ...\n", + "\n", + " for ... in ...:\n", + "\n", + " print(forward, \"and\", backward) # added for didactical purposes\n", + "\n", + " if ...:\n", + " # answer to Q8\n", + " ...\n", + "\n", + " # answer to Q8\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Verify that the test cases still work!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert pythonic_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "assert pythonic_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert pythonic_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert pythonic_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert pythonic_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert pythonic_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: `pythonic_palindrome()` is *not* able to check numeric palindromes. In addition to the string method that implements the case insensitivity and that essentially causes the `AttributeError`, what *abstract behaviors* are numeric data types, such as the `int` type in the example below, missing that would also cause runtime errors? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pythonic_palindrome(12321)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Copy your code from `pythonic_palindrome()` above into `palindrome_ducks()` below and make the latter conform to *duck typing*!\n", + "\n", + "Hints: You may want to use the [str() ](https://docs.python.org/3/library/functions.html#func-str) built-in. You only need to add *one* short line of code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def palindrome_ducks(text, *, ignore_case=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; must be an individual word\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answer to Q11\n", + " ...\n", + " # answers from above\n", + " if ignore_case:\n", + " text = ...\n", + " chars_to_check = ...\n", + "\n", + " for ... in ...:\n", + " if ...:\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: Verify that the two new test cases work as well!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert palindrome_ducks(12321) is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert palindrome_ducks(12345) is False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`palindrome_ducks()` can *not* process palindromes that consist of more than one word." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "palindrome_ducks(\"Never odd or even.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "palindrome_ducks(\"Eva, can I stab bats in a cave?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "palindrome_ducks(\"A man, a plan, a canal - Panama.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "palindrome_ducks(\"A Santa lived as a devil at NASA!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "palindrome_ducks(\"\"\"\n", + " Dennis, Nell, Edna, Leon, Nedra, Anita, Rolf, Nora, Alice, Carol, Leo, Jane,\n", + " Reed, Dena, Dale, Basil, Rae, Penny, Lana, Dave, Denny, Lena, Ida, Bernadette,\n", + " Ben, Ray, Lila, Nina, Jo, Ira, Mara, Sara, Mario, Jan, Ina, Lily, Arne, Bette,\n", + " Dan, Reba, Diane, Lynn, Ed, Eva, Dana, Lynne, Pearl, Isabel, Ada, Ned, Dee,\n", + " Rena, Joel, Lora, Cecil, Aaron, Flora, Tina, Arden, Noel, and Ellen sinned.\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: Implement the final iterative version `is_a_palindrome()` below. Copy your solution from `palindrome_ducks()` above and add code that removes the \"special\" characters (and symbols) from the longer example palindromes above so that they are effectively ignored! Note that this processing should only be done if the `ignore_symbols` argument is set to `True`.\n", + "\n", + "Hints: Use the [replace() ](https://docs.python.org/3/library/stdtypes.html#str.replace) method on the `str` type to achieve that. You may want to do so within another `for`-loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def is_a_palindrome(text, *, ignore_case=True, ignore_symbols=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; may be multiple words\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + " ignore_symbols (bool): If special characters like \".\" or \"?\" and others\n", + " are ignored; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " ...\n", + " if ignore_case:\n", + " ...\n", + " # answer to Q13\n", + " if ignore_symbols:\n", + " for ... in ...:\n", + " ...\n", + " # answers from above\n", + " chars_to_check = ...\n", + "\n", + " for ... in ...:\n", + " if ...:\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Verify that all test cases below work!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(12321) is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(12345) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Never odd or even.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Never odd or even.\", ignore_symbols=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"Eva, can I stab bats in a cave?\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"A man, a plan, a canal - Panama.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"A Santa lived as a devil at NASA!\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert is_a_palindrome(\"\"\"\n", + " Dennis, Nell, Edna, Leon, Nedra, Anita, Rolf, Nora, Alice, Carol, Leo, Jane,\n", + " Reed, Dena, Dale, Basil, Rae, Penny, Lana, Dave, Denny, Lena, Ida, Bernadette,\n", + " Ben, Ray, Lila, Nina, Jo, Ira, Mara, Sara, Mario, Jan, Ina, Lily, Arne, Bette,\n", + " Dan, Reba, Diane, Lynn, Ed, Eva, Dana, Lynne, Pearl, Isabel, Ada, Ned, Dee,\n", + " Rena, Joel, Lora, Cecil, Aaron, Flora, Tina, Arden, Noel, and Ellen sinned.\n", + "\"\"\") is True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's look at a *recursive* formulation in `recursive_palindrome()` below.\n", + "\n", + "**Q15**: Copy the code from `is_a_palindrome()` that implements the duck typing, the case insensitivity, and the removal of special characters!\n", + "\n", + "The recursion becomes apparent if we remove the *first* and the *last* character from a given `text`: `text` can only be a palindrome if the two removed characters are the same *and* the remaining substring is a palindrome itself! So, the word `\"noon\"` has only *one* recursive call while `\"radar\"` has *two*.\n", + "\n", + "Further, `recursive_palindrome()` has *two* base cases of which only *one* is reached for a given `text`: First, if `recursive_palindrome()` is called with either an empty `\"\"` or a `text` argument with `len(text) == 1`, and, second, if the two removed characters are *not* the same.\n", + "\n", + "**Q16**: Implement the two base cases in `recursive_palindrome()`! Use the *early exit* pattern!\n", + "\n", + "**Q17**: Add the recursive call to `recursive_palindrome()` with a substring of `text`! Pass in the `ignore_case` and `ignore_symbols` arguments as `False`! This avoids unnecessary computations in the recursive calls. Why is that the case?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def recursive_palindrome(text, *, ignore_case=True, ignore_symbols=True):\n", + " \"\"\"Check if a text is a palindrome or not.\n", + "\n", + " Args:\n", + " text (str): Text to be checked; may be multiple words\n", + " ignore_case (bool): If the check is case insensitive; defaults to True\n", + " ignore_symbols (bool): If special characters like \".\" or \"?\" and others\n", + " are ignored; defaults to True\n", + "\n", + " Returns:\n", + " is_palindrome (bool)\n", + " \"\"\"\n", + " # answers from above\n", + " ...\n", + " if ignore_case:\n", + " ...\n", + " if ignore_symbols:\n", + " for ... in ...:\n", + " ...\n", + "\n", + " # answer to Q16\n", + " if ...:\n", + " ...\n", + " elif ...:\n", + " ...\n", + "\n", + " # answer to Q17\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q18**: Lastly, verify that `recursive_palindrome()` passes all the test cases below!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"noon\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Hannah\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Hannah\", ignore_case=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"radar\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Hanna\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Warsaw\") is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(12321) is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(12345) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Never odd or even.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Never odd or even.\", ignore_symbols=False) is False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"Eva, can I stab bats in a cave?\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"A man, a plan, a canal - Panama.\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"A Santa lived as a devil at NASA!\") is True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert recursive_palindrome(\"\"\"\n", + " Dennis, Nell, Edna, Leon, Nedra, Anita, Rolf, Nora, Alice, Carol, Leo, Jane,\n", + " Reed, Dena, Dale, Basil, Rae, Penny, Lana, Dave, Denny, Lena, Ida, Bernadette,\n", + " Ben, Ray, Lila, Nina, Jo, Ira, Mara, Sara, Mario, Jan, Ina, Lily, Arne, Bette,\n", + " Dan, Reba, Diane, Lynn, Ed, Eva, Dana, Lynne, Pearl, Isabel, Ada, Ned, Dee,\n", + " Rena, Joel, Lora, Cecil, Aaron, Flora, Tina, Arden, Noel, and Ellen sinned.\n", + "\"\"\") is True" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/06_text/02_content.ipynb b/06_text/02_content.ipynb new file mode 100644 index 0000000..887deb5 --- /dev/null +++ b/06_text/02_content.ipynb @@ -0,0 +1,2110 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/01_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 6: Text & Bytes (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this second part of the chapter, we look in more detail at how `str` objects work in memory, in particular how the $0$s and $1$s in the memory translate into characters." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Special Characters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As previously seen, some characters have a special meaning when following the **escape character** `\"\\\"`. Besides escaping the kind of quote used as the `str` object's delimiter, `'` or `\"`, most of these **escape sequences** (i.e., `\"\\\"` with the subsequent character), act as a **control character** that moves the \"cursor\" in the output *without* generating any pixel on the screen. Because of that, we only see the effect of such escape sequences when used with the [print() ](https://docs.python.org/3/library/functions.html#print) function. The [documentation ](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals) lists all available escape sequences, of which we show the most important ones below.\n", + "\n", + "The most common escape sequence is `\"\\n\"` that \"prints\" a [newline character ](https://en.wikipedia.org/wiki/Newline) that is also called the line feed character or LF for short." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'This is a sentence\\nthat is printed\\non three lines.'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"This is a sentence\\nthat is printed\\non three lines.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This is a sentence\n", + "that is printed\n", + "on three lines.\n" + ] + } + ], + "source": [ + "print(\"This is a sentence\\nthat is printed\\non three lines.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`\"\\b\"` is the [backspace character ](https://en.wikipedia.org/wiki/Backspace), or BS for short, that moves the cursor back by one character." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ABX\n" + ] + } + ], + "source": [ + "print(\"ABC\\bX\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ABXY\n" + ] + } + ], + "source": [ + "print(\"ABC\\bXY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, `\"\\r\"` is the [carriage return character ](https://en.wikipedia.org/wiki/Carriage_return), or CR for short, that moves the cursor back to the beginning of the line." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "XBC\n" + ] + } + ], + "source": [ + "print(\"ABC\\rX\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "XYC\n" + ] + } + ], + "source": [ + "print(\"ABC\\rXY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While Linux and modern MacOS systems use solely `\"\\n\"` to express a new line, Windows systems default to using `\"\\r\\n\"`. This may lead to \"weird\" bugs on software projects where people using both kind of operating systems collaborate." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This is a sentence\n", + "that is printed\n", + "on three lines.\n" + ] + } + ], + "source": [ + "print(\"This is a sentence\\r\\nthat is printed\\r\\non three lines.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`\"\\t\"` makes the cursor \"jump\" in equidistant tab stops. That may be useful for formatting a program with lengthy and tabular results." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Jump\tfrom\ttab\tstop\tto\ttab\tstop.\n", + "The\tsecond\tline\tdoes\tso\ttoo.\n" + ] + } + ], + "source": [ + "print(\"Jump\\tfrom\\ttab\\tstop\\tto\\ttab\\tstop.\\nThe\\tsecond\\tline\\tdoes\\tso\\ttoo.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Raw Strings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sometimes we do *not* want the backslash `\"\\\"` and its subsequent character be interpreted as an escape sequence. For example, let's print a typical installation path on a Windows systems. Obviously, the newline character `\"\\n\"` does *not* makes sense here." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C:\\Programs\n", + "ew_application\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "<>:1: SyntaxWarning: invalid escape sequence '\\P'\n", + "<>:1: SyntaxWarning: invalid escape sequence '\\P'\n", + "/tmp/ipykernel_159416/1102122489.py:1: SyntaxWarning: invalid escape sequence '\\P'\n", + " print(\"C:\\Programs\\new_application\")\n" + ] + } + ], + "source": [ + "print(\"C:\\Programs\\new_application\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Some `str` objects even produce a `SyntaxError` because the `\"\\U\"` can *not* be interpreted as a Unicode code point (cf., next section)." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "(unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \\UXXXXXXXX escape (2296736867.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[10], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m print(\"C:\\Users\\Administrator\\Desktop\\Project\")\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \\UXXXXXXXX escape\n" + ] + } + ], + "source": [ + "print(\"C:\\Users\\Administrator\\Desktop\\Project\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A simple solution would be to escape the escape character with a *second* backslash `\"\\\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C:\\Programs\\new_application\n" + ] + } + ], + "source": [ + "print(\"C:\\\\Programs\\\\new_application\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C:\\Users\\Administrator\\Desktop\\Project\n" + ] + } + ], + "source": [ + "print(\"C:\\\\Users\\\\Administrator\\\\Desktop\\\\Project\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, this is tedious to remember and type. For such use cases, Python allows to prefix any string literal with a `r`. The literal is then interpreted in a \"raw\" way." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C:\\Programs\\new_application\n" + ] + } + ], + "source": [ + "print(r\"C:\\Programs\\new_application\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "C:\\Users\\Administrator\\Desktop\\Project\n" + ] + } + ], + "source": [ + "print(r\"C:\\Users\\Administrator\\Desktop\\Project\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Characters are Numbers with a Convention" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, we used the term **character** without any further consideration. In this section, we briefly look into what characters are and how they are modeled in software.\n", + "\n", + "[Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb) gives us an idea on how individual **bits** are used to express all types of numbers, from \"simple\" `int` objects to \"complex\" `float` ones. To model characters, another **layer of abstraction** is put on top of whole numbers. So, just as bits are used to express integers, they themselves are used to express characters." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### ASCII" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Many conventions have been developed as to what integer is associated with which character. The most basic one that was also adopted around the world is the the so-called [American Standard Code for Information Interchange ](https://en.wikipedia.org/wiki/ASCII), or **ASCII** for short. It uses 7 bits of information to map the unprintable control characters as well as the printable letters of the alphabet, numbers, and common symbols to the numbers `0` through `127`.\n", + "\n", + "A mapping from characters to numbers is referred to by the technical term **encoding**. We may use the built-in [ord() ](https://docs.python.org/3/library/functions.html#ord) function to **encode** any single character. The inverse to that is the built-in [chr() ](https://docs.python.org/3/library/functions.html#chr) function, which **decodes** a number into a character." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "65" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ord(\"A\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'A'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chr(65)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, unprintable escape sequences like `\"\\n\"` count as only *one* character." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ord(\"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\n'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "chr(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In ASCII, the numbers `0` through `31` (and `127`) are mapped to all kinds of unprintable control characters. The decimal digits are encoded with the numbers `48` through `57`, the upper case letters with `65` through `90`, and the lower case letters with `97` through `122`. While this seems random as first, there is of course a \"sophisticated\" system behind it. That can immediately be seen when looking at the encoded numbers in their *binary* representations.\n", + "\n", + "For example, the digit `5` is mapped to the number `53` in ASCII. The binary representation of `53` is `0b_11_0101` and the least significant four bits, `0101`, mean $5$. Similarly, the letter `\"E\"` is the fifth letter in the alphabet. It is encoded with the number `69` in ASCII, which is `0b_100_0101` in binary. And, the least significant bits, `0_0101`, mean $5$. Analogously, `\"e\"` is encoded with `101` in ASCII, which is `0b_110_0101` in binary. And, the least significant bits, `0_0101`, mean $5$ again. This encoding was chosen mainly because programmers \"in the old days\" needed to implement these encodings \"by hand.\" Python abstracts that logic away from its users.\n", + "\n", + "This encoding scheme is also the cause for the \"weird\" sorting in the \"*String Comparison*\" section in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/01_content.ipynb#String-Comparison) of this chapter, where `\"apple\"` comes *after* `\"Banana\"`. As `\"a\"` is encoded with `97` and `\"B\"` with `66`, `\"Banana\"` must of course be \"smaller\" than `\"apple\"` when comparison is done in a pairwise fashion of the individual characters." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "48 0b110000 -> 0\n", + "49 0b110001 -> 1\n", + "50 0b110010 -> 2\n", + "51 0b110011 -> 3\n", + "52 0b110100 -> 4\n", + "53 0b110101 -> 5\n", + "54 0b110110 -> 6\n", + "55 0b110111 -> 7\n", + "56 0b111000 -> 8\n", + "57 0b111001 -> 9\n" + ] + } + ], + "source": [ + "for number in range(48, 58):\n", + " print(number, bin(number), \"-> \", chr(number))" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "65 0b1000001 -> A\t66 0b1000010 -> B\t67 0b1000011 -> C\n", + "68 0b1000100 -> D\t69 0b1000101 -> E\t70 0b1000110 -> F\n", + "71 0b1000111 -> G\t72 0b1001000 -> H\t73 0b1001001 -> I\n", + "74 0b1001010 -> J\t75 0b1001011 -> K\t76 0b1001100 -> L\n", + "77 0b1001101 -> M\t78 0b1001110 -> N\t79 0b1001111 -> O\n", + "80 0b1010000 -> P\t81 0b1010001 -> Q\t82 0b1010010 -> R\n", + "83 0b1010011 -> S\t84 0b1010100 -> T\t85 0b1010101 -> U\n", + "86 0b1010110 -> V\t87 0b1010111 -> W\t88 0b1011000 -> X\n", + "89 0b1011001 -> Y\t90 0b1011010 -> Z\t" + ] + } + ], + "source": [ + "for i, number in enumerate(range(65, 91), start=1):\n", + " end = \"\\n\" if i % 3 == 0 else \"\\t\"\n", + " print(number, bin(number), \"-> \", chr(number), end=end)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 97 0b1100001 -> a\t 98 0b1100010 -> b\t 99 0b1100011 -> c\n", + "100 0b1100100 -> d\t101 0b1100101 -> e\t102 0b1100110 -> f\n", + "103 0b1100111 -> g\t104 0b1101000 -> h\t105 0b1101001 -> i\n", + "106 0b1101010 -> j\t107 0b1101011 -> k\t108 0b1101100 -> l\n", + "109 0b1101101 -> m\t110 0b1101110 -> n\t111 0b1101111 -> o\n", + "112 0b1110000 -> p\t113 0b1110001 -> q\t114 0b1110010 -> r\n", + "115 0b1110011 -> s\t116 0b1110100 -> t\t117 0b1110101 -> u\n", + "118 0b1110110 -> v\t119 0b1110111 -> w\t120 0b1111000 -> x\n", + "121 0b1111001 -> y\t122 0b1111010 -> z\t" + ] + } + ], + "source": [ + "for i, number in enumerate(range(97, 123), start=1):\n", + " end = \"\\n\" if i % 3 == 0 else \"\\t\"\n", + " print(str(number).rjust(3), bin(number), \"-> \", chr(number), end=end)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The remaining `symbols` encoded in ASCII are encoded with the numbers still unused, which is why they are scattered." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "symbols = (\n", + " list(range(32, 48))\n", + " + list(range(58, 65))\n", + " + list(range(91, 97))\n", + " + list(range(123, 127))\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 32 0b100000 -> \t 33 0b100001 -> !\t 34 0b100010 -> \"\n", + " 35 0b100011 -> #\t 36 0b100100 -> $\t 37 0b100101 -> %\n", + " 38 0b100110 -> &\t 39 0b100111 -> '\t 40 0b101000 -> (\n", + " 41 0b101001 -> )\t 42 0b101010 -> *\t 43 0b101011 -> +\n", + " 44 0b101100 -> ,\t 45 0b101101 -> -\t 46 0b101110 -> .\n", + " 47 0b101111 -> /\t 58 0b111010 -> :\t 59 0b111011 -> ;\n", + " 60 0b111100 -> <\t 61 0b111101 -> =\t 62 0b111110 -> >\n", + " 63 0b111111 -> ?\t 64 0b1000000 -> @\t 91 0b1011011 -> [\n", + " 92 0b1011100 -> \\\t 93 0b1011101 -> ]\t 94 0b1011110 -> ^\n", + " 95 0b1011111 -> _\t 96 0b1100000 -> `\t123 0b1111011 -> {\n", + "124 0b1111100 -> |\t125 0b1111101 -> }\t126 0b1111110 -> ~\n" + ] + } + ], + "source": [ + "for i, number in enumerate(symbols, start=1):\n", + " end = \"\\n\" if i % 3 == 0 else \"\\t\"\n", + " print(str(number).rjust(3), bin(number).rjust(10), \"-> \", chr(number), end=end)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the ASCII character set does not work for many languages other than English, various encodings were developed. Popular examples are [ISO 8859-1 ](https://en.wikipedia.org/wiki/ISO/IEC_8859-1) for western European letters or [Windows 1250 ](https://en.wikipedia.org/wiki/Windows-1250) for Latin ones. Many of these encodings use 8-bit numbers (i.e., `0` through `255`) to map the multitude of non-English letters (e.g., the German [umlauts ](https://en.wikipedia.org/wiki/Umlaut_%28linguistics%29) `\"ä\"`, `\"ö\"`, `\"ü\"`, or `\"ß\"`)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Unicode" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, none of these specialized encodings can map *all* characters of *all* languages around the world from *all* times in human history. To achieve that, a truly global standard called **[Unicode ](https://en.wikipedia.org/wiki/Unicode)** was developed and its first version released in 1991. Since then, Unicode has been amended with many other \"characters.\" The most popular among them being [emojis ](https://en.wikipedia.org/wiki/Emoji) or the [Klingon ](https://en.wikipedia.org/wiki/Klingon_scripts) language (from the science fiction series [Star Trek ](https://en.wikipedia.org/wiki/Star_Trek)). In Unicode, every character is given an identity referred to as the **code point**. Code points are hexadecimal numbers from `0x0000` through `0x10ffff`, written as U+0000 and U+10FFFF outside of Python. Consequently, there exist at most $1,114,112$ code points, of which only about 10% are currently in use, allowing lots of room for new characters to be invented. The first `127` code points are identical to the ASCII encoding for reasons explained in the \"*The `bytes` Type*\" section further below. There exist plenty of lists of all Unicode characters on the web (e.g., [Wikipedia ](https://en.wikipedia.org/wiki/List_of_Unicode_characters)).\n", + "\n", + "All we need to know to print a character is its code point. Python uses the escape sequence `\"\\U\"` that is followed by eight hexadecimal digits. Underscore separators are unfortunately *not* allowed here.\n", + "\n", + "So, to print a smiley, we just need to look up the corresponding number (e.g., [here ](https://en.wikipedia.org/wiki/Emoji#Unicode_blocks))." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'😄'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\\U0001f604\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Every Unicode character also has a descriptive name that we can use with the escape sequence `\"\\N\"` and within curly braces `{}`." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'😂'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\\N{FACE WITH TEARS OF JOY}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whenever the code point can be expressed with just four hexadecimal digits, we may use the escape sequence `\"\\u\"` for brevity." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'A'" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\\U00000041\" # hex(65) == 0x41" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'A'" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\\u0041\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogously, if the code point can be expressed with two hexadecimal digits, we may use the escape sequence `\"\\x\"` for even conciser code." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'A'" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\\x41\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the `str` type is based on Unicode, a `str` object's behavior is more in line with how humans view text and not how it is expressed in source code.\n", + "\n", + "For example, while it is obvious that `len(\"A\")` evaluates to `1`, ..." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(\"A\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... what should `len(\"\\N{SNAKE}\")` evaluate to? As the idea of a snake is expressed as *one* \"character,\" [len() ](https://docs.python.org/3/library/functions.html#len) also returns `1` here." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'🐍'" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\\N{SNAKE}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(\"\\N{SNAKE}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Many of the built-in `str` methods also consider Unicode. For example, in contrast to [lower() ](https://docs.python.org/3/library/stdtypes.html#str.lower), the [casefold() ](https://docs.python.org/3/library/stdtypes.html#str.casefold) method knows that the German `\"ß\"` is commonly converted to `\"ss\"`. So, when searching for exact matches, normalizing text with [casefold() ](https://docs.python.org/3/library/stdtypes.html#str.casefold) may yield better results than with [lower() ](https://docs.python.org/3/library/stdtypes.html#str.lower)." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'straße'" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Straße\".lower()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'strasse'" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Straße\".casefold()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Many other methods like [isdecimal() ](https://docs.python.org/3/library/stdtypes.html#str.isdecimal), [isdigit() ](https://docs.python.org/3/library/stdtypes.html#str.isdigit), [isnumeric() ](https://docs.python.org/3/library/stdtypes.html#str.isnumeric), [isprintable() ](https://docs.python.org/3/library/stdtypes.html#str.isprintable), [isidentifier() ](https://docs.python.org/3/library/stdtypes.html#str.isidentifier), and many more may be worthwhile to know for the data science practitioner, especially when it comes to data cleaning." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Multi-line Strings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sometimes, it is convenient to split text across multiple lines in source code. For example, to make lines fit into the 79 characters requirement of [PEP 8 ](https://www.python.org/dev/peps/pep-0008/) or because the text consists of many lines and typing out `\"\\n\"` is tedious. However, using single double quotes `\"` around multiple lines results in a `SyntaxError`." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "unterminated string literal (detected at line 1) (2682216481.py, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m Cell \u001b[0;32mIn[34], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m \"\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m unterminated string literal (detected at line 1)\n" + ] + } + ], + "source": [ + "\"\n", + "Do not break the lines like this\n", + "\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Instead, we may enclose a string literal with either **triple double** quotes `\"\"\"` or **triple single** quotes `'''`. Then, newline characters in the source code are converted into `\"\\n\"` characters in the resulting `str` object. Docstrings are precisely that, and, by convention, always written within triple double quotes `\"\"\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "multi_line = \"\"\"\n", + "I am a multi-line string\n", + "consisting of four lines.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A caveat is that `\"\\n\"` characters are often inserted at the beginning or end of the text when we try to format the source code nicely." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\nI am a multi-line string\\nconsisting of four lines.\\n'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "multi_line" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "I am a multi-line string\n", + "consisting of four lines.\n", + "\n" + ] + } + ], + "source": [ + "print(multi_line)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Using the [split() ](https://docs.python.org/3/library/stdtypes.html#str.split) method with the optional `sep` argument, we confirm that `multi_line` consists of *four* lines with the first and last line being empty." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 \n", + "2 I am a multi-line string\n", + "3 consisting of four lines.\n", + "4 \n" + ] + } + ], + "source": [ + "for i, line in enumerate(multi_line.split(\"\\n\"), start=1):\n", + " print(i, line)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To mitigate that, we often see the [strip() ](https://docs.python.org/3/library/stdtypes.html#bytes.strip) method in source code." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "multi_line = \"\"\"\n", + "I am a multi-line string\n", + "consisting of two lines.\n", + "\"\"\".strip()" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 I am a multi-line string\n", + "2 consisting of two lines.\n" + ] + } + ], + "source": [ + "for i, line in enumerate(multi_line.split(\"\\n\"), start=1):\n", + " print(i, line)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `bytes` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To end this chapter, we want to briefly look at the `bytes` data type, which conceptually is a sequence of bytes. That data format is probably one of the most generic ways of exchanging data between any two programs or computers (e.g., a web browser obtains its data from a web server in this format).\n", + "\n", + "Let's open a binary file in read-only mode (i.e., `mode=\"rb\"`) and read in all of its contents." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "with open(\"full_house.bin\", mode=\"rb\") as binary_file:\n", + " data = binary_file.read()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`data` is an object of type `bytes`." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139880714782512" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "bytes" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It's value is given out in the literal bytes notation with a `b` prefix (cf., the [reference ](https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals)). Every byte is expressed in hexadecimal representation with the escape sequence `\"\\x\"`. This representation is commonly chosen as we can *not* tell what kind of information is hidden in the `data` by just looking at the bytes. Instead, we must be told by some other source how to **decode** the raw bytes into information we can interpret." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "b'\\xf0\\x9f\\x82\\xa7\\xf0\\x9f\\x82\\xb7\\xf0\\x9f\\x83\\x97\\xf0\\x9f\\x83\\x8e\\xf0\\x9f\\x83\\x9e'" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`bytes` objects work like `str` objects in many ways. In particular, they are *sequences* as well: The number of bytes is *finite* and we may *iterate* over them in *order*." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Consisting of 8 bits, a single byte can always be interpreted as a whole number between `0` through `255`. That is exactly what we see when we loop over the `data` ..." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "240 159 130 167 240 159 130 183 240 159 131 151 240 159 131 142 240 159 131 158 " + ] + } + ], + "source": [ + "for byte in data:\n", + " print(byte, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... or index into them." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "158" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data[-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Slicing returns another `bytes` object." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "b'\\xf0\\x82\\xf0\\x82\\xf0\\x83\\xf0\\x83\\xf0\\x83'" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data[::2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Character Encodings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Luckily, `data` consists of bytes encoded with the [UTF-8 ](https://en.wikipedia.org/wiki/UTF-8) encoding. That is the most common way of mapping a Unicode character's code point to a sequence of bytes.\n", + "\n", + "To obtain a `str` object out of a given `bytes` object, we decode it with the `bytes` type's [decode() ](https://docs.python.org/3/library/stdtypes.html#bytes.decode) method." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "cards = data.decode()" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "str" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(cards)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So, `data` consisted of a [full house ](https://en.wikipedia.org/wiki/List_of_poker_hands#Full_house) hand in a poker game." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'🂧🂷🃗🃎🃞'" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cards" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To go the opposite direction and encode a given `str` object, we use the `str` type's [encode() ](https://docs.python.org/3/library/stdtypes.html#str.encode) method." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "place = \"Café Kastanientörtchen\"" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "b'Caf\\xc3\\xa9 Kastanient\\xc3\\xb6rtchen'" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "place.encode()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By default, [encode() ](https://docs.python.org/3/library/stdtypes.html#str.encode) and [decode() ](https://docs.python.org/3/library/stdtypes.html#bytes.decode) use an `encoding=\"utf-8\"` argument. We may use another encoding like, for example, `\"iso-8859-1\"`, which can deal with ASCII and western European letters." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "b'Caf\\xe9 Kastanient\\xf6rtchen'" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "place.encode(\"iso-8859-1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, we must use the *same* encoding for the decoding step as for the encoding step. Otherwise, a `UnicodeDecodeError` is raised." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "UnicodeDecodeError", + "evalue": "'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mUnicodeDecodeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[55], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mplace\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mencode\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43miso-8859-1\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdecode\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mUnicodeDecodeError\u001b[0m: 'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte" + ] + } + ], + "source": [ + "place.encode(\"iso-8859-1\").decode()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Not all encodings map all Unicode code points. For example `\"iso-8859-1\"` does not know Czech letters. Below, [encode() ](https://docs.python.org/3/library/stdtypes.html#str.encode) raises a `UnicodeEncodeError` because of that." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "UnicodeEncodeError", + "evalue": "'latin-1' codec can't encode character '\\u0159' in position 12: ordinal not in range(256)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mUnicodeEncodeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[56], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mDobrý den, přátelé!\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mencode\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43miso-8859-1\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mUnicodeEncodeError\u001b[0m: 'latin-1' codec can't encode character '\\u0159' in position 12: ordinal not in range(256)" + ] + } + ], + "source": [ + "\"Dobrý den, přátelé!\".encode(\"iso-8859-1\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Reading Files (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [open() ](https://docs.python.org/3/library/functions.html#open) function takes an optional `encoding` argument as well." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "UnicodeDecodeError", + "evalue": "'utf-8' codec can't decode byte 0xe4 in position 9: invalid continuation byte", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mUnicodeDecodeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[57], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mumlauts.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m file:\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mjoin(\u001b[43mfile\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mreadlines\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m))\n", + "File \u001b[0;32m:322\u001b[0m, in \u001b[0;36mdecode\u001b[0;34m(self, input, final)\u001b[0m\n", + "\u001b[0;31mUnicodeDecodeError\u001b[0m: 'utf-8' codec can't decode byte 0xe4 in position 9: invalid continuation byte" + ] + } + ], + "source": [ + "with open(\"umlauts.txt\") as file:\n", + " print(\"\".join(file.readlines()))" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lerchen-Lärchen-Ähnlichkeiten\n", + "fehlen. Dieses abzustreiten\n", + "mag im Klang der Worte liegen.\n", + "Merke, eine Lerch' kann fliegen,\n", + "Lärchen nicht, was kaum verwundert,\n", + "denn nicht eine unter hundert\n", + "ist geflügelt. Auch im Singen\n", + "sind die Bäume zu bezwingen.\n", + "Die Bätrachtung sollte reichen,\n", + "Rächtschreibfählern auszuweichen.\n", + "Leicht gälingt's, zu unterscheiden,\n", + "wär ist wär nun von dän beiden.\n" + ] + } + ], + "source": [ + "with open(\"umlauts.txt\", encoding=\"iso-8859-1\") as file:\n", + " print(\"\".join(file.readlines()))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Best Practice: Use UTF-8 explicitly" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A best practice is to *always* specify the `encoding`, especially on computers running on Windows (cf., the talk by Łukasz Langa in the [Further Resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/05_resources.ipynb#Unicode)) section at the end of this chapter.\n", + "\n", + "Below is the first example involving [open() ](https://docs.python.org/3/library/functions.html#open) one last time: It shows how *all* the contents of a text file should be read into one `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "with open(\"lorem_ipsum.txt\", encoding=\"utf-8\") as file:\n", + " content = \"\".join(file.readlines())" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Lorem Ipsum is simply dummy text of the printing and typesetting industry.\\nLorem Ipsum has been the industry's standard dummy text ever since the 1500s\\nwhen an unknown printer took a galley of type and scrambled it to make a type\\nspecimen book. It has survived not only five centuries but also the leap into\\nelectronic typesetting, remaining essentially unchanged. It was popularised in\\nthe 1960s with the release of Letraset sheets.\\n\"" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "content" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.\n", + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s\n", + "when an unknown printer took a galley of type and scrambled it to make a type\n", + "specimen book. It has survived not only five centuries but also the leap into\n", + "electronic typesetting, remaining essentially unchanged. It was popularised in\n", + "the 1960s with the release of Letraset sheets.\n", + "\n" + ] + } + ], + "source": [ + "print(content)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/06_text/03_summary.ipynb b/06_text/03_summary.ipynb new file mode 100644 index 0000000..f3ac4d7 --- /dev/null +++ b/06_text/03_summary.ipynb @@ -0,0 +1,75 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 6: Text & Bytes (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Textual data is modeled with the **immutable** `str` type.\n", + "\n", + "The `str` type supports *four* orthogonal **abstract concepts** that together constitute the idea of a **sequence**: Every `str` object is an *iterable container* of a *finite* number of *ordered* characters.\n", + "\n", + "A single **character** in a `str` object follows the idea of a **Unicode** character. It is mapped to a *unique* **code point** that is encoded into **bytes** with a dedicated character encoding, for example, **UTF-8**." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/06_text/04_review.ipynb b/06_text/04_review.ipynb new file mode 100644 index 0000000..17dd0a5 --- /dev/null +++ b/06_text/04_review.ipynb @@ -0,0 +1,199 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 6: Text & Bytes (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb) and [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/01_content.ipynb) part of Chapter 6.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Answer the following questions *briefly*!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: In what sense is a **\"string\" of characters** a **sequence**? What is a **sequence** after all? A *concrete* **data type**, or something else?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What is a direct consequence of the `str` type's property of being an **ordered** sequence? What operations could we *not* do with it if it were *unordered*?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: What does it mean for an object to be **immutable**? Discuss how we can verify the `str` type's immutability by comparing the two variables `example` and `full_slice` below. Are they pointing to the *same* object in memory?\n", + "```python\n", + "example = \"text\"\n", + "full_slice = example[:]\n", + "```\n", + "Hint: Find out what `[:]` does first!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Describe in your own words what we mean by **string interpolation**!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: **Triple-double** quotes `\"\"\"` and **triple-single** quotes `'''` create a *new* object of type `text` that models so-called **multi-line strings**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: A **substring** is a string that *subsumes* another string." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Indexing into a `str` object with a *negative* index **fails silently**: It does *neither* raise an error *nor* do anything useful." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: We *cannot* assign a *different* character to an index or slice of a `str` object because it is **immutable**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/06_text/05_resources.ipynb b/06_text/05_resources.ipynb new file mode 100644 index 0000000..425c6cf --- /dev/null +++ b/06_text/05_resources.ipynb @@ -0,0 +1,365 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/05_resources.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 6: Text & Bytes (Further Resources)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unicode" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We refer to the official [Unicode HOWTO ](https://docs.python.org/3/howto/unicode.html) in the Python documentation. Furthermore, the [unicodedata ](https://docs.python.org/3/library/unicodedata.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides a lot of utility functions around the Unicode standard.\n", + "\n", + "Next is a brief summary video by the YouTube channel [Computerphile](https://www.youtube.com/channel/UC9-y-6csu5WGm29I7JiwpnA) titled \"*Characters, Symbols and the Unicode Miracle*\"." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"MijmeoH9LT4\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In his [PyCon Australia 2018](https://2018.pycon-au.org/) talk titled \"*Unicode and Python: The absolute minimum you need to know*\" [Raphaël Merx](https://www.linkedin.com/raphaelmerx/) explains some caveats and best practices regarding Unicode." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"oXVmZGN6plY\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In a similar talk at [PyCon 2017](https://us.pycon.org/2017/) titled \"*Unicode what is the big deal*\" [Łukasz Langa](https://www.linkedin.com/llanga/) provides further lessons learned regarding Unicode." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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BERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQDKgNktpI66CeUmNklNVV8E0TJBI6NtJW1VLG+Ro75heyn14P5RxlTxWE7H2S4UEL6Dk1vFE6quMjZYquZkrIKyrqamJraTkW7DmMmYzTqx3nOt4pNM5TlJSVtln+LF3tVtnBBbZauleySZ9nrbtQskjl3c0NJBFLrfwBa3VUU2Wkg/Geg4kZ9qqGNtQXzZ5LJDBPu4ppcVE+ndU0YjYTNUnXH8UzLhvGZHELBpdhrrNRspJeQRim2cuVjgkZPUPM0tXHQxw1ErXU43EeKPLmDURr5ypU7IV1LSzUNBMzkYqIKmnjkqqmGch8xlr6KWtiYZmxPfmRs4y7Mz2kY4rpmwttI6qVbt21W/BPHbK37lk4klIfO6mbC2lqnVZqGMdI+E0bYuUCRsbHPILeYZ5uKpV+3dthp21b5ZzSuidNyiKhrp4WMY5zH76SGAtgka5jg5j8EaTkLG6bYuubDNrhoZXy3R1c2HltwjfFG+hhpRye6MZyiCpa5jwXhp1Nc4d7nArz7P3ottscxo6+Kj1VEsc9XUQGasEzn0YkkbSO5TBTx6cF4Bc9jHkAtTMh4me1q+HQyraK9GlbRubHrFVWU1L3+qMsbUE5fpIzqGPBKr3y+U9EI9+ZC6VzmxRQwT1M8ha3U/RBTsMjmtbxLgMDgo7bK2VdVBSGBtPyimrKWrdFLNIyF25JL42zMhLuc8HafqUdfbfeKvkz3R0se5lm3tHFdK6KKdj2RiGZ1ZBStl1xvEnxBaWnWDnIC0jFPadZTkr2+W4kKvbi2xtifv5JBPTcriFPS1dS91MCGumMdPEXNY0uAdkcMjK9uu21speMs7i3korXPhp6mpjipHB5ZUTSU8ZbDE4RyaS/GdDscxURsfshVUjYRM+Bxjtk1E7dulIMr6p8zXjeNzu9DhzknOefnWH7S26qtlBU2+PcVE9bs9S0D48VgkdVQUs9E0UBZTFlZryPiiWlmA53B3DeMIN2RxnVqRjdrp6mzKra6ginFO+Z+vXBG5zaeofBFLVaOTxT1LI9zTyv3kelshB+Nj8YzPLXG0OyNzqZn4fA+F09tnhdLW1sfJY6N9JJNStoYWGnlLpIJXiZxz8dgghoWxwtJxirWO9KU23nL6BERczsEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQHIXb62j8tS9Ub7SdvraPy1L1RvtLVyvtnWg1lGCAWuq6UEEZBBnjBBB4EYPMvUvC0kr5q5HgI47EN2z3zNg9v3aLy9J1VntJ2/dovL0nVWe0uqG7MW3H8n0XVYPYT7mLb5vouqwewqnS6Hw108j0PduK+M+vmcr9v3aLy9J1VntJ2/dovL0nVWe0uqPuYtvm+i6rB7CfcxbfN9F1WD2E0yh8NdPIz3bivjPr5nK/b92i8vSdVZ7Sdv3aLy9J1VntLqj7mLb5vouqwewn3MW3zfRdVg9hNMofDXTyHduK+M+vmcr9v3aLy9J1VntL3t9bR+WpOqN9pdP3PZq3CCYigogRFIQeSwcCGH+YuDYT3rfzR/YpmF7Gvf2ErFblBYnCZt6rd7+O77m1O31tH5al6o32k7fW0flqXqjfaWrkUvRaXCuRW94YjjfM2j2+to/LUvVG+0nb62j8tS9Ub7S1c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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"7m5JA3XaZ4k\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In a \"classic\" talk from PyCon 2012 titled \"*Pragmatic Unicode, or, How do I stop the pain?*\" [Ned Batchelder](https://nedbatchelder.com/) explains among others the concept of a \"Unicode Sandwich.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"sgHbC6udIqc\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Character Encodings" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, in his entertaining talk at [PyCon.DE 2019](https://de.pycon.org/) titled \"*Your Name is Invalid!*\" [Miroslav Šedivý](https://www.linkedin.com/%C5%A1ediv%C3%BD/) shows how hard it actually is to write software that can process any name a human can possibly have. Miroslav also gave a lightning talk where he shows how he uses only one keyboard for the 12 (!!!) languages he speaks." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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A+uEbf2R/wCnRBBTe3iXHUso/ICtNPjquYu1DNaipuu904nymHhoRMEWhcyGtV8tu5xMzdRqdT6Tdt+Rpx3Raq4mWxFxLmsbCXZSanWrVMXFNaHfpTUkdFZfBE3l+SGyAukJja3QzhLh6jw0Oq13BnM2h1eIhvOOs23VvTeervX6dV+rOLqy7xJWgk5r2PkEkMb974xox79zfKc0DoT1CCHO0/tTuwDEd1nqYiPLcPz5rm5COnZkmliNDTGV3WMhVqcxkdx80jucdWQHZ61j43tuytgwCHDVZI5ruFxoms5KahM69msNWykEjqRpzd3qNfPtd+1keG6ENkIIU33aMM7Q2eGKZrXB7WzRska1zfouAeCA4etHUIC4vMMRc6RkpcYmFxljbtZITpqZGtAAd5wAgrnf+UJegtOsSVqDasOBFqzip8rHDObdXiLL4W981ymoX5Kd5oxcuJ3KGhbro52h23AXGluDJQUa+JZSqZTj3iWncyMMNCOtdFSPKlh2R2TbORJoV980sQDuU7yjq1TgcPU1a7utbdG4OY7u8WrHB75Q5p2+S4SSSO1Hpe4+clezaMAIIhiBZK+ZpEbAWyybg+UHTpI4Odq7zncfWgyEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERARUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQXMRUz8c/st7/APhqeOf2W9//AA1BcxFTPxz+y3v/AOGp45/Zb3/8NQXMRUz8c/st7/8AhqeOf2W9/wDw1BcxFTPxz+y3v/4anjn9lvf/AMNQUzREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERB//2Q==\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"pBuS7EUPnQA\", width=\"60%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"-4QjII981sM\", width=\"60%\")" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/06_text/full_house.bin b/06_text/full_house.bin new file mode 100644 index 0000000..4ad5e35 --- /dev/null +++ b/06_text/full_house.bin @@ -0,0 +1 @@ +🂧🂷🃗🃎🃞 \ No newline at end of file diff --git a/06_text/lorem_ipsum.txt b/06_text/lorem_ipsum.txt new file mode 100644 index 0000000..c0e21ab --- /dev/null +++ b/06_text/lorem_ipsum.txt @@ -0,0 +1,6 @@ +Lorem Ipsum is simply dummy text of the printing and typesetting industry. +Lorem Ipsum has been the industry's standard dummy text ever since the 1500s +when an unknown printer took a galley of type and scrambled it to make a type +specimen book. It has survived not only five centuries but also the leap into +electronic typesetting, remaining essentially unchanged. It was popularised in +the 1960s with the release of Letraset sheets. diff --git a/06_text/umlauts.txt b/06_text/umlauts.txt new file mode 100644 index 0000000..9c0f911 --- /dev/null +++ b/06_text/umlauts.txt @@ -0,0 +1,12 @@ +Lerchen-Lrchen-hnlichkeiten +fehlen. Dieses abzustreiten +mag im Klang der Worte liegen. +Merke, eine Lerch' kann fliegen, +Lrchen nicht, was kaum verwundert, +denn nicht eine unter hundert +ist geflgelt. Auch im Singen +sind die Bume zu bezwingen. +Die Btrachtung sollte reichen, +Rchtschreibfhlern auszuweichen. +Leicht glingt's, zu unterscheiden, +wr ist wr nun von dn beiden. \ No newline at end of file diff --git a/07_sequences/00_content.ipynb b/07_sequences/00_content.ipynb new file mode 100644 index 0000000..8f8984f --- /dev/null +++ b/07_sequences/00_content.ipynb @@ -0,0 +1,958 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 7: Sequential Data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We studied numbers (cf., [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb)) and textual data (cf., [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb)) first mainly because objects of the presented data types are \"simple.\" That is so for two reasons: First, they are *immutable*, and, as we saw in the \"*Who am I? And how many?*\" section in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb#Who-am-I?-And-how-many?), mutable objects can quickly become hard to reason about. Second, they are \"flat\" in the sense that they are *not* composed of other objects.\n", + "\n", + "The `str` type is a bit of a corner case in this regard. While one could argue that a longer `str` object, for example, `\"text\"`, is composed of individual characters, this is *not* the case in memory as the literal `\"text\"` only creates *one* object (i.e., one \"bag\" of $0$s and $1$s modeling all characters).\n", + "\n", + "This chapter, [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb), [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb), and [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/10_arrays/00_content.ipynb) introduce various \"complex\" data types. While some are mutable and others are not, they all share that they are primarily used to \"manage,\" or structure, the memory in a program (i.e., they provide references to other objects). Unsurprisingly, computer scientists refer to the ideas behind these data types as **[data structures ](https://en.wikipedia.org/wiki/Data_structure)**.\n", + "\n", + "In this chapter, we focus on data types that model all kinds of sequential data. Examples of such data are [spreadsheets ](https://en.wikipedia.org/wiki/Spreadsheet) or [matrices ](https://en.wikipedia.org/wiki/Matrix_%28mathematics%29) and [vectors ](https://en.wikipedia.org/wiki/Vector_%28mathematics_and_physics%29). These formats share the property that they are composed of smaller units that come in a sequence of, for example, rows/columns/cells or elements/entries." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Collections vs. Sequences" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb#A-\"String\"-of-Characters) already describes the **sequence** properties of `str` objects. In this section, we take a step back and study these properties one by one.\n", + "\n", + "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) defines a variety of **abstract base classes** (ABCs). We saw ABCs already in [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/02_content.ipynb#The-Numerical-Tower), where we use the ones from the [numbers ](https://docs.python.org/3/library/numbers.html) module in the [standard library ](https://docs.python.org/3/library/index.html) to classify Python's numeric data types according to mathematical ideas. Now, we take the ABCs from the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module to classify the data types in this chapter according to their behavior in various contexts.\n", + "\n", + "As an illustration, consider `numbers` and `text` below, two objects of *different* types." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]\n", + "text = \"Lorem ipsum dolor sit amet.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Among others, one commonality between the two is that we may loop over them with the `for` statement. So, in the context of iteration, both exhibit the *same* behavior." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 8 5 3 12 2 6 9 10 1 4 " + ] + } + ], + "source": [ + "for number in numbers:\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "L o r e m i p s u m d o l o r s i t a m e t . " + ] + } + ], + "source": [ + "for character in text:\n", + " print(character, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#Containers-vs.-Iterables), we referred to such types as *iterables*. That is *not* a proper [English](https://dictionary.cambridge.org/spellcheck/english-german/?q=iterable) word, even if it may sound like one at first sight. Yet, it is an official term in the Python world formalized with the `Iterable` ABC in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module.\n", + "\n", + "For the data science practitioner, it is worthwhile to know such terms as, for example, the documentation on the [built-ins ](https://docs.python.org/3/library/functions.html) uses them extensively: In simple words, any built-in that takes an argument called \"*iterable*\" may be called with *any* object that supports being looped over. Already familiar [built-ins ](https://docs.python.org/3/library/functions.html) include [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate), [sum() ](https://docs.python.org/3/library/functions.html#sum), or [zip() ](https://docs.python.org/3/library/functions.html#zip). So, they do *not* require the argument to be of a certain data type (e.g., `list`); instead, any *iterable* type works." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "collections.abc.Iterable" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abc.Iterable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As seen in [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/02_content.ipynb#Goose-Typing), we can use ABCs with the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function to check if an object supports a behavior.\n", + "\n", + "So, let's \"ask\" Python if it can loop over `numbers` or `text`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Iterable)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(text, abc.Iterable)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Contrary to `list` or `str` objects, numeric objects are *not* iterable." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(999, abc.Iterable)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Instead of asking, we could try to loop over `999`, but this results in a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'int' object is not iterable", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[9], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mdigit\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;241;43m999\u001b[39;49m\u001b[43m:\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mprint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mdigit\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: 'int' object is not iterable" + ] + } + ], + "source": [ + "for digit in 999:\n", + " print(digit)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Most of the data types in this chapter and [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb) and [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/10_arrays/00_content.ipynb) exhibit three [orthogonal ](https://en.wikipedia.org/wiki/Orthogonality) (i.e., \"independent\") behaviors, formalized by ABCs in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module as:\n", + "- `Iterable`: An object may be looped over.\n", + "- `Container`: An object \"contains\" references to other objects; a \"whole\" is composed of many \"parts.\"\n", + "- `Sized`: The number of references to other objects, the \"parts,\" is *finite*.\n", + "\n", + "The characteristical operation supported by `Container` types is the `in` operator for membership testing." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 in numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"l\" in text" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we could also check if `numbers` and `text` are `Container` types with [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Container)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(text, abc.Container)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Numeric objects do *not* \"contain\" references to other objects, and that is why they are considered \"flat\" data types. The `in` operator raises a `TypeError`. Conceptually speaking, Python views numeric types as \"wholes\" without any \"parts.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(999, abc.Container)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "argument of type 'int' is not iterable", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[15], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;241;43m9\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;241;43m999\u001b[39;49m\n", + "\u001b[0;31mTypeError\u001b[0m: argument of type 'int' is not iterable" + ] + } + ], + "source": [ + "9 in 999" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogously, being `Sized` types, we can pass `numbers` and `text` as the argument to the built-in [len() ](https://docs.python.org/3/library/functions.html#len) function and obtain \"meaningful\" results. The exact meaning depends on the data type: For `numbers`, [len() ](https://docs.python.org/3/library/functions.html#len) tells us how many elements are in the `list` object; for `text`, it tells us how many [Unicode characters ](https://en.wikipedia.org/wiki/Unicode) make up the `str` object. *Abstractly* speaking, both data types exhibit the *same* behavior of *finiteness*." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "27" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(text)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Sized)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(text, abc.Sized)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, even though `999` consists of three digits for humans, numeric objects in Python have no concept of a \"size\" or \"length,\" and the [len() ](https://docs.python.org/3/library/functions.html#len) function raises a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(999, abc.Sized)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "object of type 'int' has no len()", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[21], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m999\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: object of type 'int' has no len()" + ] + } + ], + "source": [ + "len(999)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "These three behaviors are so essential that whenever they coincide for a data type, it is called a **collection**, formalized with the `Collection` ABC. That is where the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module got its name from: It summarizes all ABCs related to collections; in particular, it defines a hierarchy of specialized kinds of collections.\n", + "\n", + "Without going into too much detail, one way to read the summary table at the beginning of the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module's documention is as follows: The first column, titled \"ABC\", lists all collection-related ABCs in Python. The second column, titled \"Inherits from,\" indicates if the idea behind the ABC is *original* (e.g., the first row with the `Container` ABC has an empty \"Inherits from\" column) or a *combination* (e.g., the row with the `Collection` ABC has `Sized`, `Iterable`, and `Container` in the \"Inherits from\" column). The third and fourth columns list the methods that come with a data type following an ABC. We keep ignoring the methods named in the dunder style for now.\n", + "\n", + "So, let's confirm that both `numbers` and `text` are collections." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Collection)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(text, abc.Collection)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "They share one more common behavior: When looping over them, we can *predict* the *order* of the elements or characters. The ABC in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module corresponding to this behavior is `Reversible`. While sounding unintuitive at first, it is evident that if something is reversible, it must have a forward order, to begin with.\n", + "\n", + "The [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in allows us to loop over the elements or characters in reverse order." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 1 10 9 6 2 12 3 5 8 11 7 " + ] + } + ], + "source": [ + "for number in reversed(numbers):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ". t e m a t i s r o l o d m u s p i m e r o L " + ] + } + ], + "source": [ + "for character in reversed(text):\n", + " print(character, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Reversible)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(text, abc.Reversible)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Collections that exhibit this fourth behavior are referred to as **sequences**, formalized with the `Sequence` ABC in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Sequence)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(text, abc.Sequence)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The data types introduced in this chapter are sequences. Nevertheless, we also look at some data types that are neither collections nor sequences but are still useful to model sequential data in practice in [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb).\n", + "\n", + "In Python-related documentations, the terms collection and sequence are heavily used, and the data science practitioner should always think of them in terms of the three or four behaviors they exhibit.\n", + "\n", + "Data types that are collections but not sequences are covered in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb)." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/07_sequences/01_content.ipynb b/07_sequences/01_content.ipynb new file mode 100644 index 0000000..d267d48 --- /dev/null +++ b/07_sequences/01_content.ipynb @@ -0,0 +1,2999 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/01_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 7: Sequential Data (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this second part of the chapter, we look closely at the built-in `list` type, which is probably the most commonly used sequence data type in practice." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `list` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As already seen multiple times, to create a `list` object, we use the *literal notation* and list all elements within brackets `[` and `]`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "empty = []" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "simple = [40, 50]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The elements do *not* need to be of the *same* type, and `list` objects may also be **nested**." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "nested = [empty, 10, 20.0, \"Thirty\", simple]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[PythonTutor ](http://pythontutor.com/visualize.html#code=empty%20%3D%20%5B%5D%0Asimple%20%3D%20%5B40,%2050%5D%0Anested%20%3D%20%5Bempty,%2010,%2020.0,%20%22Thirty%22,%20simple%5D&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how `nested` holds references to the `empty` and `simple` objects. Technically, it holds three more references to the `10`, `20.0`, and `\"Thirty\"` objects as well. However, to simplify the visualization, these three objects are shown right inside the `nested` object. That may be done because they are immutable and \"flat\" data types. In general, the $0$s and $1$s inside a `list` object in memory always constitute references to other objects only." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[], 10, 20.0, 'Thirty', [40, 50]]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's not forget that `nested` is an object on its own with an *identity* and *data type*." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139688113982848" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(nested)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "list" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(nested)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we use the built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor to create a `list` object out of any (finite) *iterable* we pass to it as the argument.\n", + "\n", + "For example, we can wrap the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in with [list() ](https://docs.python.org/3/library/functions.html#func-list): As described in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#Containers-vs.-Iterables), `range` objects, like `range(1, 13)` below, are iterable and generate `int` objects \"on the fly\" (i.e., one by one). The [list() ](https://docs.python.org/3/library/functions.html#func-list) around it acts like a `for`-loop and **materializes** twelve `int` objects in memory that then become the elements of the newly created `list` object. [PythonTutor ](http://pythontutor.com/visualize.html#code=r%20%3D%20range%281,%2013%29%0Al%20%3D%20list%28range%281,%2013%29%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows this difference visually." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(range(1, 13))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Beware of passing a `range` object over a \"big\" horizon as the argument to [list() ](https://docs.python.org/3/library/functions.html#func-list) as that may lead to a `MemoryError` and the computer crashing." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "MemoryError", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mMemoryError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[8], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mlist\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mrange\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m999_999_999_999\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mMemoryError\u001b[0m: " + ] + } + ], + "source": [ + "list(range(999_999_999_999))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As another example, we create a `list` object from a `str` object, which is iterable, as well. Then, the individual characters become the elements of the new `list` object!" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['i', 't', 'e', 'r', 'a', 'b', 'l', 'e']" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(\"iterable\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sequence Behaviors" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`list` objects are *sequences*. To reiterate that, we briefly summarize the *four* behaviors of a sequence and provide some more `list`-specific details below:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "- `Container`:\n", + " - holds references to other objects in memory (with their own *identity* and *type*)\n", + " - implements membership testing via the `in` operator\n", + "- `Iterable`:\n", + " - supports being looped over\n", + " - works with the `for` or `while` statements\n", + "- `Reversible`:\n", + " - the elements come in a *predictable* order that we may loop over in a forward or backward fashion\n", + " - works with the [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in\n", + "- `Sized`:\n", + " - the number of elements is finite *and* known in advance\n", + " - works with the built-in [len() ](https://docs.python.org/3/library/functions.html#len) function" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The \"length\" of `nested` is *five* because `empty` and `simple` count as *one* element each. In other words, `nested` holds five references to other objects, two of which are `list` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(nested)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With a `for`-loop, we can iterate over all elements in a *predictable* order, forward or backward. As `list` objects hold *references* to other *objects*, these have an *indentity* and may even be of *different* types; however, the latter observation is rarely, if ever, useful in practice." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[] 139688113991744 \n", + "10 94291567220768 \n", + "20.0 139688114016656 \n", + "Thirty 139688114205808 \n", + "[40, 50] 139688113982208 \n" + ] + } + ], + "source": [ + "for element in nested:\n", + " print(str(element).ljust(10), str(id(element)).ljust(18), type(element))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[40, 50] Thirty 20.0 10 [] " + ] + } + ], + "source": [ + "for element in reversed(nested):\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `in` operator checks if a given object is \"contained\" in a `list` object. It uses the `==` operator behind the scenes (i.e., *not* the `is` operator) conducting a **[linear search ](https://en.wikipedia.org/wiki/Linear_search)**: So, Python implicitly loops over *all* elements and only stops prematurely if an element evaluates equal to the searched object. A linear search may, therefore, be relatively *slow* for big `list` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10 in nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`20` compares equal to the `20.0` in `nested`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "20 in nested" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "30 in nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Indexing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because of the *predictable* order and the *finiteness*, each element in a sequence can be labeled with a unique *index*, an `int` object in the range $0 \\leq \\text{index} < \\lvert \\text{sequence} \\rvert$.\n", + "\n", + "Brackets, `[` and `]`, are the literal syntax for accessing individual elements of any sequence type. In this book, we also call them the *indexing operator* in this context." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The last index is one less than `len(nested)`, and Python raises an `IndexError` if we look up an index that is not in the range." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "IndexError", + "evalue": "list index out of range", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[17], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnested\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m5\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\u001b[0;31mIndexError\u001b[0m: list index out of range" + ] + } + ], + "source": [ + "nested[5]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Negative indices are used to count in reverse order from the end of a sequence, and brackets may be chained to access nested objects. So, to access the `50` inside `simple` via the `nested` object, we write `nested[-1][1]`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[40, 50]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "50" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[-1][1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Slicing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Slicing `list` objects works analogously to slicing `str` objects: We use the literal syntax with either one or two colons `:` inside the brackets `[]` to separate the *start*, *stop*, and *step* values. Slicing creates a *new* `list` object with the elements chosen from the original one.\n", + "\n", + "For example, to obtain the three elements in the \"middle\" of `nested`, we slice from `1` (including) to `4` (excluding)." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[10, 20.0, 'Thirty']" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[1:4]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To obtain \"every other\" element, we slice from beginning to end, defaulting to `0` and `len(nested)` when omitted, in steps of `2`." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[], 20.0, [40, 50]]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[::2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The literal notation with the colons `:` is *syntactic sugar*. It saves us from using the [slice() ](https://docs.python.org/3/library/functions.html#slice) built-in to create `slice` objects. [slice() ](https://docs.python.org/3/library/functions.html#slice) takes *start*, *stop*, and *step* arguments in the same way as the familiar [range() ](https://docs.python.org/3/library/functions.html#func-range), and the `slice` objects it creates are used just as *indexes* above.\n", + "\n", + "In most cases, the literal notation is more convenient to use; however, with `slice` objects, we can give names to slices and reuse them across several sequences." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "middle = slice(1, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "slice" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(middle)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[10, 20.0, 'Thirty']" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[middle]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`slice` objects come with three read-only attributes `start`, `stop`, and `step` on them." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "middle.start" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "middle.stop" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If not passed to [slice() ](https://docs.python.org/3/library/functions.html#slice), these attributes default to `None`. That is why the cell below has no output." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "middle.step" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A good trick to know is taking a \"full\" slice: This copies *all* elements of a `list` object into a *new* `list` object." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "nested_copy = nested[:]" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[], 10, 20.0, 'Thirty', [40, 50]]" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "At first glance, `nested` and `nested_copy` seem to cause no pain. For `list` objects, the comparison operator `==` goes over the elements in both operands in a pairwise fashion and checks if they all evaluate equal (cf., the \"*List Comparison*\" section below for more details).\n", + "\n", + "We confirm that `nested` and `nested_copy` compare equal as could be expected but also that they are *different* objects." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested == nested_copy" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested is nested_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, as [PythonTutor ](http://pythontutor.com/visualize.html#code=nested%20%3D%20%5B%5B%5D,%2010,%2020.0,%20%22Thirty%22,%20%5B40,%2050%5D%5D%0Anested_copy%20%3D%20nested%5B%3A%5D&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) reveals, only the *references* to the elements are copied, and not the objects in `nested` themselves! Because of that, `nested_copy` is a so-called **[shallow copy ](https://en.wikipedia.org/wiki/Object_copying#Shallow_copy)** of `nested`.\n", + "\n", + "We could also see this with the [id() ](https://docs.python.org/3/library/functions.html#id) function: The respective first elements in both `nested` and `nested_copy` are the *same* object, namely `empty`. So, we have three ways of accessing the *same* address in memory. Also, we say that `nested` and `nested_copy` partially share the *same* state." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[0] is nested_copy[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139688113991744" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(nested[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139688113991744" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(nested_copy[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Knowing this becomes critical if the elements in a `list` object are mutable objects (i.e., we can change them *in place*), and this is the case with `nested` and `nested_copy`, as we see in the next section on \"*Mutability*\".\n", + "\n", + "As both the original `nested` object and its copy reference the *same* `list` objects in memory, any changes made to them are visible to both! Because of that, working with shallow copies can easily become confusing.\n", + "\n", + "Instead of a shallow copy, we could also create a so-called **[deep copy ](https://en.wikipedia.org/wiki/Object_copying#Deep_copy)** of `nested`: Then, the copying process recursively follows every reference in a nested data structure and creates copies of *every* object found.\n", + "\n", + "To explicitly create shallow or deep copies, the [copy ](https://docs.python.org/3/library/copy.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides two functions, [copy() ](https://docs.python.org/3/library/copy.html#copy.copy) and [deepcopy() ](https://docs.python.org/3/library/copy.html#copy.deepcopy). We must always remember that slicing creates *shallow* copies only." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import copy" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "nested_deep_copy = copy.deepcopy(nested)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested == nested_deep_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, the first elements of `nested` and `nested_deep_copy` are *different* objects, and [PythonTutor ](http://pythontutor.com/visualize.html#code=import%20copy%0Anested%20%3D%20%5B%5B%5D,%2010,%2020.0,%20%22Thirty%22,%20%5B40,%2050%5D%5D%0Anested_deep_copy%20%3D%20copy.deepcopy%28nested%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows that there are *six* `list` objects in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested[0] is nested_deep_copy[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139688113991744" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(nested[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139688113233152" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(nested_deep_copy[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As this [StackOverflow question ](https://stackoverflow.com/questions/184710/what-is-the-difference-between-a-deep-copy-and-a-shallow-copy) shows, understanding shallow and deep copies is a common source of confusion, independent of the programming language." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Mutability" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In contrast to `str` objects, `list` objects are *mutable*: We may assign new elements to indices or slices and also remove elements. That changes the *references* in a `list` object. In general, if an object is *mutable*, we say that it may be changed *in place*." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "nested[0] = 0" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 10, 20.0, 'Thirty', [40, 50]]" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When we re-assign a slice, we can even change the size of the `list` object." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "nested[:4] = [100, 100, 100]" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[100, 100, 100, [40, 50]]" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(nested)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `list` object's identity does *not* change. That is the main point behind mutable objects." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139688113982848" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(nested) # same memory location as before" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`nested_copy` is unchanged!" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[], 10, 20.0, 'Thirty', [40, 50]]" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's change the nested `[40, 50]` via `nested_copy` into `[1, 2, 3]` by replacing all its elements." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "nested_copy[-1][:] = [1, 2, 3]" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[], 10, 20.0, 'Thirty', [1, 2, 3]]" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "That has a surprising side effect on `nested`." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[100, 100, 100, [1, 2, 3]]" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "That is precisely the confusion we talked about above when we said that `nested_copy` is a *shallow* copy of `nested`. [PythonTutor ](http://pythontutor.com/visualize.html#code=nested%20%3D%20%5B%5B%5D,%2010,%2020.0,%20%22Thirty%22,%20%5B40,%2050%5D%5D%0Anested_copy%20%3D%20nested%5B%3A%5D%0Anested%5B%3A4%5D%20%3D%20%5B100,%20100,%20100%5D%0Anested_copy%5B-1%5D%5B%3A%5D%20%3D%20%5B1,%202,%203%5D&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how both reference the *same* nested `list` object that is changed *in place* from `[40, 50]` into `[1, 2, 3]`.\n", + "\n", + "Lastly, we use the `del` statement to remove an element." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "del nested[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[100, 100, 100]" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `del` statement also works for slices. Here, we remove all references `nested` holds." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "del nested[:]" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Mutability for sequences is formalized by the `MutableSequence` ABC in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## List Methods" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `list` type is an essential data structure in any real-world Python application, and many typical `list`-related algorithms from computer science theory are already built into it at the C level (cf., the [documentation ](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) or the [tutorial ](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists) for a full overview; unfortunately, not all methods have direct links). So, understanding and applying the built-in methods of the `list` type not only speeds up the development process but also makes programs significantly faster.\n", + "\n", + "In contrast to the `str` type's methods in [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb#String-Methods) (e.g., [.upper() ](https://docs.python.org/3/library/stdtypes.html#str.upper) or [.lower() ](https://docs.python.org/3/library/stdtypes.html#str.lower)), the `list` type's methods that mutate an object do so *in place*. That means they *never* create *new* `list` objects and return `None` to indicate that. So, we must *never* assign the return value of `list` methods to the variable holding the list!\n", + "\n", + "Let's look at the following `names` example." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "names = [\"Carl\", \"Peter\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To add an object to the end of `names`, we use the `.append()` method. The code cell shows no output indicating that `None` must be the return value." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "names.append(\"Eckardt\")" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Carl', 'Peter', 'Eckardt']" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the `.extend()` method, we may also append multiple elements provided by an iterable. Here, the iterable is a `list` object itself holding two `str` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "names.extend([\"Karl\", \"Oliver\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Carl', 'Peter', 'Eckardt', 'Karl', 'Oliver']" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similar to `.append()`, we may add a new element at an arbitrary position with the `.insert()` method. `.insert()` takes two arguments, an *index* and the element to be inserted." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "names.insert(1, \"Berthold\")" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Carl', 'Berthold', 'Peter', 'Eckardt', 'Karl', 'Oliver']" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`list` objects may be sorted *in place* with the [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) method. That is different from the built-in [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function that takes any *finite* and *iterable* object and returns a *new* `list` object with the iterable's elements sorted!" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Carl', 'Eckardt', 'Karl', 'Oliver', 'Peter']" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sorted(names)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As the previous code cell created a *new* `list` object, `names` is still unsorted." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Carl', 'Berthold', 'Peter', 'Eckardt', 'Karl', 'Oliver']" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's sort the elements in `names` instead." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "names.sort()" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Carl', 'Eckardt', 'Karl', 'Oliver', 'Peter']" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To sort in reverse order, we pass a keyword-only `reverse=True` argument to either the [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) method or the [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "names.sort(reverse=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Peter', 'Oliver', 'Karl', 'Eckardt', 'Carl', 'Berthold']" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) method and the [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function sort the elements in `names` in alphabetical order, forward or backward. However, that does *not* hold in general.\n", + "\n", + "We mention above that `list` objects may contain objects of *any* type and even of *mixed* types. Because of that, the sorting is **[delegated ](https://en.wikipedia.org/wiki/Delegation_(object-oriented_programming))** to the elements in a `list` object. In a way, Python \"asks\" the elements in a `list` object to sort themselves. As `names` contains only `str` objects, they are sorted according the the comparison rules explained in [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb#String-Comparison).\n", + "\n", + "To customize the sorting, we pass a keyword-only `key` argument to [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) or [sorted() ](https://docs.python.org/3/library/functions.html#sorted), which must be a `function` object accepting *one* positional argument. Then, the elements in the `list` object are passed to that one by one, and the return values are used as the **sort keys**. The `key` argument is also a popular use case for `lambda` expressions.\n", + "\n", + "For example, to sort `names` not by alphabet but by the names' lengths, we pass in a reference to the built-in [len() ](https://docs.python.org/3/library/functions.html#len) function as `key=len`. Note that there are *no* parentheses after `len`!" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "names.sort(key=len)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If two names have the same length, their relative order is kept as is. That is why `\"Karl\"` comes before `\"Carl\" ` below. A [sorting algorithm ](https://en.wikipedia.org/wiki/Sorting_algorithm) with that property is called **[stable ](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability)**.\n", + "\n", + "Sorting is an important topic in programming, and we refer to the official [HOWTO ](https://docs.python.org/3/howto/sorting.html) for a more comprehensive introduction." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Karl', 'Carl', 'Peter', 'Oliver', 'Eckardt', 'Berthold']" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`.sort(reverse=True)` is different from the `.reverse()` method. Whereas the former applies some sorting rule in reverse order, the latter simply reverses the elements in a `list` object." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "names.reverse()" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Eckardt', 'Oliver', 'Peter', 'Carl', 'Karl']" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `.pop()` method removes the *last* element from a `list` object *and* returns it. Below we **capture** the `removed` element to show that the return value is not `None` as with all the methods introduced so far." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "removed = names.pop()" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Karl'" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "removed" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Eckardt', 'Oliver', 'Peter', 'Carl']" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`.pop()` takes an optional *index* argument and removes that instead.\n", + "\n", + "So, to remove the second element, `\"Eckhardt\"`, from `names`, we write this." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "removed = names.pop(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Eckardt'" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "removed" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Peter', 'Carl']" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Instead of removing an element by its index, we can also remove it by its value with the `.remove()` method. Behind the scenes, Python then compares the object to be removed, `\"Peter\"` in the example, sequentially to each element with the `==` operator and removes the *first* one that evaluates equal to it. `.remove()` does *not* return the removed element." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "names.remove(\"Peter\")" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl']" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Also, `.remove()` raises a `ValueError` if the value is not found." + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "list.remove(x): x not in list", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[80], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnames\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mremove\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mPeter\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mValueError\u001b[0m: list.remove(x): x not in list" + ] + } + ], + "source": [ + "names.remove(\"Peter\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`list` objects implement an `.index()` method that returns the position of the first element that compares equal to its argument. It fails *loudly* with a `ValueError` if no element compares equal." + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl']" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names.index(\"Oliver\")" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "'Karl' is not in list", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[83], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnames\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mKarl\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mValueError\u001b[0m: 'Karl' is not in list" + ] + } + ], + "source": [ + "names.index(\"Karl\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `.count()` method returns the number of elements that compare equal to its argument." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names.count(\"Carl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names.count(\"Karl\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Two more methods, `.copy()` and `.clear()`, are *syntactic sugar* and replace working with slices.\n", + "\n", + "`.copy()` creates a *shallow* copy. So, `names.copy()` below does the same as taking a full slice with `names[:]`, and the caveats from above apply, too." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "names_copy = names.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl']" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`.clear()` removes all references from a `list` object. So, `names_copy.clear()` is the same as `del names_copy[:]`." + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "names_copy.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 89, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names_copy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Many methods introduced in this section are mentioned in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module's documentation as well: While the `.index()` and `.count()` methods come with any data type that is a `Sequence`, the `.append()`, `.extend()`, `.insert()`, `.reverse()`, `.pop()`, and `.remove()` methods are part of any `MutableSequence` type. The `.sort()`, `.copy()`, and `.clear()` methods are `list`-specific.\n", + "\n", + "So, being a sequence does not only imply the four *behaviors* specified above, but also means that a data type comes with certain standardized methods." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## List Operations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with `str` objects, the `+` and `*` operators are overloaded for concatenation and always return a *new* `list` object. The references in this newly created `list` object reference the *same* objects as the two original `list` objects. So, the same caveat as with *shallow* copies from above applies!" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl']" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl', 'Diedrich', 'Yves']" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names + [\"Diedrich\", \"Yves\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl', 'Berthold', 'Oliver', 'Carl']" + ] + }, + "execution_count": 92, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 * names" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Unpacking" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Besides being an operator, the `*` symbol has a second syntactical use, as explained in [PEP 3132 ](https://www.python.org/dev/peps/pep-3132/) and [PEP 448 ](https://www.python.org/dev/peps/pep-0448/): It implements what is called **iterable unpacking**. It is *not* an operator syntactically but a notation that Python reads as a literal.\n", + "\n", + "In the example, Python interprets the expression as if the elements of the iterable `names` were placed between `\"Achim\"` and `\"Xavier\"` one by one. So, we do not obtain a nested but a *flat* list." + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Achim', 'Berthold', 'Oliver', 'Carl', 'Xavier']" + ] + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[\"Achim\", *names, \"Xavier\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Effectively, Python reads that as if we wrote the following." + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Achim', 'Berthold', 'Oliver', 'Carl', 'Xavier']" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[\"Achim\", names[0], names[1], names[2], \"Xavier\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### List Comparison" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The relational operators also work with `list` objects; yet another example of operator overloading.\n", + "\n", + "Comparison is made in a pairwise fashion until the first pair of elements does not evaluate equal or one of the `list` objects ends. The exact comparison rules depend on the elements and not the `list` objects. As with [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) or [sorted() ](https://docs.python.org/3/library/functions.html#sorted) above, comparison is *delegated* to the objects to be compared, and Python \"asks\" the elements in the two `list` objects to compare themselves. Usually, all elements are of the *same* type, and comparison is straightforward." + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Berthold', 'Oliver', 'Carl']" + ] + }, + "execution_count": 95, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 96, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names == [\"Berthold\", \"Oliver\", \"Carl\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 97, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names != [\"Berthold\", \"Oliver\", \"Karl\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 98, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "names < [\"Berthold\", \"Oliver\", \"Karl\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If two `list` objects have a different number of elements and all overlapping elements compare equal, the shorter `list` object is considered \"smaller.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 99, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[\"Berthold\", \"Oliver\"] < names < [\"Berthold\", \"Oliver\", \"Carl\", \"Xavier\"]" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/07_sequences/02_exercises_lists.ipynb b/07_sequences/02_exercises_lists.ipynb new file mode 100644 index 0000000..7d5fab5 --- /dev/null +++ b/07_sequences/02_exercises_lists.ipynb @@ -0,0 +1,257 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 7: Sequential Data (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb) of Chapter 7.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with Lists" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Write a function `nested_sum()` that takes a `list` object as its argument, which contains other `list` objects with numbers, and adds up the numbers! Use `nested_numbers` below to test your function!\n", + "\n", + "Hint: You need at least one `for`-loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nested_numbers = [[1, 2, 3], [4], [5], [6, 7], [8], [9]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def nested_sum(list_of_lists):\n", + " \"\"\"Add up numbers in nested lists.\n", + " \n", + " Args:\n", + " list_of_lists (list): A list containing the lists with the numbers\n", + " \n", + " Returns:\n", + " sum (int or float)\n", + " \"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nested_sum(nested_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Generalize `nested_sum()` into a function `mixed_sum()` that can process a \"mixed\" `list` object, which contains numbers and other `list` objects with numbers! Use `mixed_numbers` below for testing!\n", + "\n", + "Hints: Use the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function to check how an element is to be processed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mixed_numbers = [[1, 2, 3], 4, 5, [6, 7], 8, [9]]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def mixed_sum(list_of_lists_or_numbers):\n", + " \"\"\"Add up numbers in nested lists.\n", + " \n", + " Args:\n", + " list_of_lists_or_numbers (list): A list containing both numbers and\n", + " lists with numbers\n", + " \n", + " Returns:\n", + " sum (int or float)\n", + " \"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mixed_sum(mixed_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.1**: Write a function `cum_sum()` that takes a `list` object with numbers as its argument and returns a *new* `list` object with the **cumulative sums** of these numbers! So, `sum_up` below, `[1, 2, 3, 4, 5]`, should return `[1, 3, 6, 10, 15]`.\n", + "\n", + "Hint: The idea behind is similar to the [cumulative distribution function ](https://en.wikipedia.org/wiki/Cumulative_distribution_function) from statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sum_up = [1, 2, 3, 4, 5]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def cum_sum(numbers):\n", + " \"\"\"Create the cumulative sums for some numbers.\n", + "\n", + " Args:\n", + " numbers (list): A list with numbers for that the cumulative sums\n", + " are calculated\n", + " \n", + " Returns:\n", + " cum_sums (list): A list with all the cumulative sums\n", + " \"\"\"\n", + " ...\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cum_sum(sum_up)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3.2**: We should always make sure that our functions also work in corner cases. What happens if your implementation of `cum_sum()` is called with an empty list `[]`? Make sure it handles that case *without* crashing! What would be a good return value in this corner case?\n", + "\n", + "Hint: It is possible to write this without any extra input validation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cum_sum([])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/07_sequences/03_content.ipynb b/07_sequences/03_content.ipynb new file mode 100644 index 0000000..bf7e929 --- /dev/null +++ b/07_sequences/03_content.ipynb @@ -0,0 +1,2494 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/03_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 7: Sequential Data (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this third part of the chapter, we first look at a major implication of the `list` type's mutability. Then, we see how its close relative, the `tuple` type, can mitigate this. Lastly, we see how Python's syntax assumes sequential data at various places: for example, when unpacking iterables during a `for`-loop or an assignment, or when working with `function` objects." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Modifiers vs. Pure Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As `list` objects are mutable, the caller of a function can see the changes made to a `list` object passed to the function as an argument. That is often a surprising *side effect* and should be avoided.\n", + "\n", + "As an example, consider the `add_xyz()` function." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "letters = [\"a\", \"b\", \"c\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def add_xyz(arg):\n", + " \"\"\"Append letters to a list.\"\"\"\n", + " arg.extend([\"x\", \"y\", \"z\"])\n", + " return arg" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While this function is being executed, two variables, namely `letters` in the global scope and `arg` inside the function's local scope, reference the *same* `list` object in memory. Furthermore, the passed in `arg` is also the return value.\n", + "\n", + "So, after the function call, `letters_with_xyz` and `letters` are **aliases** as well, referencing the *same* object. We can also visualize that with [PythonTutor ](http://pythontutor.com/visualize.html#code=letters%20%3D%20%5B%22a%22,%20%22b%22,%20%22c%22%5D%0A%0Adef%20add_xyz%28arg%29%3A%0A%20%20%20%20arg.extend%28%5B%22x%22,%20%22y%22,%20%22z%22%5D%29%0A%20%20%20%20return%20arg%0A%0Aletters_with_xyz%20%3D%20add_xyz%28letters%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "letters_with_xyz = add_xyz(letters)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['a', 'b', 'c', 'x', 'y', 'z']" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letters_with_xyz" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['a', 'b', 'c', 'x', 'y', 'z']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A better practice is to first create a copy of `arg` within the function that is then modified and returned. If we are sure that `arg` contains immutable elements only, we get away with a shallow copy. The downside of this approach is the higher amount of memory necessary.\n", + "\n", + "The revised `add_xyz()` function below is more natural to reason about as it does *not* modify the passed in `arg` internally. [PythonTutor ](http://pythontutor.com/visualize.html#code=letters%20%3D%20%5B%22a%22,%20%22b%22,%20%22c%22%5D%0A%0Adef%20add_xyz%28arg%29%3A%0A%20%20%20%20new_arg%20%3D%20arg%5B%3A%5D%0A%20%20%20%20new_arg.extend%28%5B%22x%22,%20%22y%22,%20%22z%22%5D%29%0A%20%20%20%20return%20new_arg%0A%0Aletters_with_xyz%20%3D%20add_xyz%28letters%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows that as well. This approach is following the **[functional programming ](https://en.wikipedia.org/wiki/Functional_programming)** paradigm that is going through a \"renaissance\" currently. Two essential characteristics of functional programming are that a function *never* changes its inputs and *always* returns the same output given the same inputs.\n", + "\n", + "For a beginner, it is probably better to stick to this idea and not change any arguments as the original `add_xyz()` above. However, functions that modify and return the argument passed in are an important aspect of object-oriented programming, as explained in [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "letters = [\"a\", \"b\", \"c\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def add_xyz(arg):\n", + " \"\"\"Create a new list from an existing one.\"\"\"\n", + " new_arg = arg[:]\n", + " new_arg.extend([\"x\", \"y\", \"z\"])\n", + " return new_arg" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "letters_with_xyz = add_xyz(letters)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['a', 'b', 'c', 'x', 'y', 'z']" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letters_with_xyz" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['a', 'b', 'c']" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we want to modify the argument passed in, it is best to return `None` and not `arg`, as does the final version of `add_xyz()` below. Then, the user of our function cannot accidentally create two aliases to the same object. That is also why the list methods above all return `None`. [PythonTutor ](http://pythontutor.com/visualize.html#code=letters%20%3D%20%5B%22a%22,%20%22b%22,%20%22c%22%5D%0A%0Adef%20add_xyz%28arg%29%3A%0A%20%20%20%20arg.extend%28%5B%22x%22,%20%22y%22,%20%22z%22%5D%29%0A%20%20%20%20return%0A%0Aadd_xyz%28letters%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how there is only *one* reference to `letters` after the function call." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "letters = [\"a\", \"b\", \"c\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def add_xyz(arg):\n", + " \"\"\"Append letters to a list.\"\"\"\n", + " arg.extend([\"x\", \"y\", \"z\"])\n", + " return # None" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "add_xyz(letters)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['a', 'b', 'c', 'x', 'y', 'z']" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we call `add_xyz()` with `letters` as the argument again, we end up with an even longer `list` object." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "add_xyz(letters)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['a', 'b', 'c', 'x', 'y', 'z', 'x', 'y', 'z']" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letters" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Functions that only work on the argument passed in are called **modifiers**. Their primary purpose is to change the **state** of the argument. On the contrary, functions that have *no* side effects on the arguments are said to be **pure**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `tuple` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To create a `tuple` object, we can use the same literal notation as for `list` objects *without* the brackets and list all elements." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, to be clearer, many Pythonistas write out the optional parentheses `(` and `)`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers = (7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As before, `numbers` is an object on its own." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140248673535456" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "tuple" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While we could use empty parentheses `()` to create an empty `tuple` object ..." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "empty_tuple = ()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "empty_tuple" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "tuple" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(empty_tuple)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... we must use a *trailing comma* to create a `tuple` object holding one element. If we forget the comma, the parentheses are interpreted as the grouping operator and effectively useless!" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "one_tuple = (1,) # we could ommit the parentheses but not the comma" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1,)" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "one_tuple" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "tuple" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(one_tuple)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "no_tuple = (1)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "no_tuple" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "int" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(no_tuple)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we may use the [tuple() ](https://docs.python.org/3/library/functions.html#func-tuple) built-in that takes any iterable as its argument and creates a new `tuple` from its elements." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1,)" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tuple([1])" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "('i', 't', 'e', 'r', 'a', 'b', 'l', 'e')" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tuple(\"iterable\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Tuples are like \"Immutable Lists\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Most operations involving `tuple` objects work in the same way as with `list` objects. The main difference is that `tuple` objects are *immutable*. So, if our program does not depend on mutability, we may and should use `tuple` and not `list` objects to model sequential data. That way, we avoid the pitfalls seen above.\n", + "\n", + "`tuple` objects are *sequences* exhibiting the familiar *four* behaviors. So, `numbers` holds a *finite* number of elements ..." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... that we can obtain individually by looping over it in a predictable *forward* or *reverse* order." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 11 8 5 3 12 2 6 9 10 1 4 " + ] + } + ], + "source": [ + "for number in numbers:\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 1 10 9 6 2 12 3 5 8 11 7 " + ] + } + ], + "source": [ + "for number in reversed(numbers):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To check if a given object is *contained* in `numbers`, we use the `in` operator and conduct a linear search." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 in numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 in numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1.0 in numbers # in relies on == behind the scenes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may index and slice with the `[]` operator. The latter returns *new* `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2, 6, 9, 10, 1, 4)" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers[6:]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Index assignment does *not* work as tuples are *immutable* and results in a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'tuple' object does not support item assignment", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[43], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnumbers\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m99\u001b[39m\n", + "\u001b[0;31mTypeError\u001b[0m: 'tuple' object does not support item assignment" + ] + } + ], + "source": [ + "numbers[-1] = 99" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `+` and `*` operators work with `tuple` objects as well: They always create *new* `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4, 99)" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers + (99,)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4, 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 * numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Being immutable, `tuple` objects only provide the `.count()` and `.index()` methods of `Sequence` types. The `.append()`, `.extend()`, `.insert()`, `.reverse()`, `.pop()`, and `.remove()` methods of `MutableSequence` types are *not* available. The same holds for the `list`-specific `.sort()`, `.copy()`, and `.clear()` methods." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers.count(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers.index(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The relational operators work in the *same* way as for `list` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers == (7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + { + "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": [ + "numbers != (99, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + { + "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": [ + "numbers < (99, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While `tuple` objects are immutable, this only relates to the references they hold. If a `tuple` object contains references to mutable objects, the entire nested structure is *not* immutable as a whole!\n", + "\n", + "Consider the following stylized example `not_immutable`: It contains *three* elements, `1`, `[2, ..., 11]`, and `12`, and the elements of the nested `list` object may be changed. While it is not practical to mix data types in a `tuple` object that is used as an \"immutable list,\" we want to make the point that the mere usage of the `tuple` type does *not* guarantee a nested object to be immutable as a whole." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "not_immutable = (1, [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 12)" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 12)" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "not_immutable" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "not_immutable[1][:] = [99, 99, 99]" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, [99, 99, 99], 12)" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "not_immutable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Packing & Unpacking" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the \"*List Operations*\" section in the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb#List-Operations) of this chapter, the `*` symbol **unpacks** the elements of a `list` object into another one. This idea of *iterable unpacking* is built into Python at various places, even *without* the `*` symbol.\n", + "\n", + "For example, we may write variables on the left-hand side of a `=` statement in a literal `tuple` style. Then, any *finite* iterable on the right-hand side is unpacked. So, `numbers` is unpacked into *twelve* variables below." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12 = numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n1" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "11" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n2" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Having to type twelve variables on the left is already tedious. Furthermore, if the iterable on the right yields a number of elements *different* from the number of variables, we get a `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "too many values to unpack (expected 11)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[60], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11 \u001b[38;5;241m=\u001b[39m numbers\n", + "\u001b[0;31mValueError\u001b[0m: too many values to unpack (expected 11)" + ] + } + ], + "source": [ + "n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11 = numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "not enough values to unpack (expected 13, got 12)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[61], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13 \u001b[38;5;241m=\u001b[39m numbers\n", + "\u001b[0;31mValueError\u001b[0m: not enough values to unpack (expected 13, got 12)" + ] + } + ], + "source": [ + "n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13 = numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So, to make iterable unpacking useful, we prepend the `*` symbol to *one* of the variables on the left: That variable then becomes a `list` object holding the elements not captured by the other variables. We say that the excess elements from the iterable are **packed** into this variable.\n", + "\n", + "For example, let's get the `first` and `last` element of `numbers` and collect the rest in `middle`." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "first, *middle, last = numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[11, 8, 5, 3, 12, 2, 6, 9, 10, 1]" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "middle # always a list!" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "last" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We already used unpacking before this section without knowing it. Whenever we write a `for`-loop over the [zip() ](https://docs.python.org/3/library/functions.html#zip) built-in, that generates a new `tuple` object in each iteration that we unpack by listing several loop variables.\n", + "\n", + "So, the `name, position` below acts like a left-hand side of an `=` statement and unpacks the `tuple` objects generated from \"zipping\" the `names` list and the `positions` tuple together." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "names = [\"Berthold\", \"Oliver\", \"Carl\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "positions = (\"goalkeeper\", \"defender\", \"midfielder\", \"striker\", \"coach\")" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Berthold is a goalkeeper\n", + "Oliver is a defender\n", + "Carl is a midfielder\n" + ] + } + ], + "source": [ + "for name, position in zip(names, positions):\n", + " print(name, \"is a\", position)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Without unpacking, [zip() ](https://docs.python.org/3/library/functions.html#zip) generates a series of `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ('Berthold', 'goalkeeper')\n", + " ('Oliver', 'defender')\n", + " ('Carl', 'midfielder')\n" + ] + } + ], + "source": [ + "for pair in zip(names, positions):\n", + " print(type(pair), pair, sep=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Unpacking also works for nested objects. Below, we wrap [zip() ](https://docs.python.org/3/library/functions.html#zip) with the [enumerate() ](https://docs.python.org/3/library/functions.html#enumerate) built-in to have an index variable `number` inside the `for`-loop. In each iteration, a `tuple` object consisting of `number` and another `tuple` object is created. The inner one then holds the `name` and `position`." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Berthold (jersey #1) is a goalkeeper\n", + "Oliver (jersey #2) is a defender\n", + "Carl (jersey #3) is a midfielder\n" + ] + } + ], + "source": [ + "for number, (name, position) in enumerate(zip(names, positions), start=1):\n", + " print(f\"{name} (jersey #{number}) is a {position}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Swapping Variables" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A popular use case of unpacking is **swapping** two variables.\n", + "\n", + "Consider `a` and `b` below." + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a = 0\n", + "b = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Without unpacking, we must use a temporary variable `temp` to swap `a` and `b`." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "temp = a\n", + "a = b\n", + "b = temp\n", + "\n", + "del temp" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With unpacking, the solution is more elegant. *All* expressions on the right-hand side are evaluated *before* any assignment takes place." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "a, b = 0, 1" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "a, b = b, a" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 0)" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a, b" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Example: [Fibonacci Numbers ](https://en.wikipedia.org/wiki/Fibonacci_number) (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Unpacking allows us to rewrite the iterative `fibonacci()` function from [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#\"Hard-at-first-Glance\"-Example:-Fibonacci-Numbers-%28revisited%29) in a concise way." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def fibonacci(i):\n", + " \"\"\"Calculate the ith Fibonacci number.\n", + "\n", + " Args:\n", + " i (int): index of the Fibonacci number to calculate\n", + "\n", + " Returns:\n", + " ith_fibonacci (int)\n", + " \"\"\"\n", + " a, b = 0, 1\n", + "\n", + " for _ in range(i - 1):\n", + " a, b = b, a + b\n", + "\n", + " return b" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "144" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Function Definitions & Calls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The concepts of packing and unpacking are also helpful when writing and using functions.\n", + "\n", + "For example, let's look at the `product()` function below. Its implementation suggests that `args` must be a sequence type. Otherwise, it would not make sense to index into it with `[0]` or take a slice with `[1:]`. In line with the function's name, the `for`-loop multiplies all elements of the `args` sequence. So, what does the `*` do in the header line, and what is the exact data type of `args`?\n", + "\n", + "The `*` is again *not* an operator in this context but a special syntax that makes Python *pack* all *positional* arguments passed to `product()` into a single `tuple` object called `args`." + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = args[0]\n", + "\n", + " for arg in args[1:]:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So, we can pass an *arbitrary* (i.e., also none) number of *positional* arguments to `product()`.\n", + "\n", + "The product of just one number is the number itself." + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Passing in several numbers works as expected." + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, this implementation of `product()` needs *at least* one argument passed in due to the expression `args[0]` used internally. Otherwise, we see a *runtime* error, namely an `IndexError`. We emphasize that this error is *not* caused in the header line." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "IndexError", + "evalue": "tuple index out of range", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[83], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mproduct\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[80], line 3\u001b[0m, in \u001b[0;36mproduct\u001b[0;34m(*args)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mproduct\u001b[39m(\u001b[38;5;241m*\u001b[39margs):\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Multiply all arguments.\"\"\"\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43margs\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m arg \u001b[38;5;129;01min\u001b[39;00m args[\u001b[38;5;241m1\u001b[39m:]:\n\u001b[1;32m 6\u001b[0m result \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m=\u001b[39m arg\n", + "\u001b[0;31mIndexError\u001b[0m: tuple index out of range" + ] + } + ], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Another downside of this implementation is that we can easily generate *semantic* errors: For example, if we pass in an iterable object like the `one_hundred` list, *no* exception is raised. However, the return value is also not a numeric object as we expect. The reason for this is that during the function call, `args` becomes a `tuple` object holding *one* element, which is `one_hundred`, a `list` object. So, we created a nested structure by accident." + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "one_hundred = [2, 5, 10]" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[2, 5, 10]" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(one_hundred) # a semantic error!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This error does not occur if we unpack `one_hundred` upon passing it as the argument." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(*one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "That is the equivalent of writing out the following tedious expression. Yet, that does *not* scale for iterables with many elements in them." + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(one_hundred[0], one_hundred[1], one_hundred[2])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the \"*Packing & Unpacking with Functions*\" [exercise ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/04_exercises.ipynb), we look at `product()` in more detail.\n", + "\n", + "While we needed to unpack `one_hundred` above to avoid the semantic error, unpacking an argument in a function call may also be a convenience in general. For example, to print the elements of `one_hundred` in one line, we need to use a `for` statement, until now. With unpacking, we get away *without* a loop." + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[2, 5, 10]\n" + ] + } + ], + "source": [ + "print(one_hundred) # prints the tuple; we do not want that" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 5 10 " + ] + } + ], + "source": [ + "for number in one_hundred:\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 5 10\n" + ] + } + ], + "source": [ + "print(*one_hundred) # replaces the for-loop" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/07_sequences/04_exercises_un-packing.ipynb b/07_sequences/04_exercises_un-packing.ipynb new file mode 100644 index 0000000..3f783a2 --- /dev/null +++ b/07_sequences/04_exercises_un-packing.ipynb @@ -0,0 +1,663 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/04_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 7: Sequential Data (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [third part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb) of Chapter 7.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Packing & Unpacking with Functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the \"*Function Definitions & Calls*\" section in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb#Function-Definitions-&-Calls), we define the following function `product()`. In this exercise, you will improve it by making it more \"user-friendly.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = args[0]\n", + "\n", + " for arg in args[1:]:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `*` in the function's header line *packs* all *positional* arguments passed to `product()` into one *iterable* called `args`.\n", + "\n", + "**Q1**: What is the data type of `args` within the function's body?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because of the packing, we may call `product()` with an abitrary number of *positional* arguments: The product of just `42` remains `42`, while `2`, `5`, and `10` multiplied together result in `100`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, \"abitrary\" does not mean that we can pass *no* argument. If we do so, we get an `IndexError`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What line in the body of `product()` causes this exception? What is the exact problem?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Function-Definitions-&-Calls), we also pass a `list` object, like `one_hundred`, to `product()`, and *no* exception is raised." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "one_hundred = [2, 5, 10]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: What is wrong with that? What *kind* of error (cf., [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Formal-vs.-Natural-Languages)) is that conceptually? Describe precisely what happens to the passed in `one_hundred` in every line within `product()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Of course, one solution is to *unpack* `one_hundred` with the `*` symbol. We look at another solution further below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(*one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's continue with the issue when calling `product()` *without* any argument.\n", + "\n", + "This revised version of `product()` avoids the `IndexError` from before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = None\n", + "\n", + " for arg in args:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Describe why no error occurs by going over every line in `product()`!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unfortunately, the new version cannot process any arguments we pass in any more." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What line causes troubles now? What is the exact problem?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Replace the `None` in `product()` above with something reasonable that does *not* cause exceptions! Ensure that `product(42)` and `product(2, 5, 10)` return a correct result.\n", + "\n", + "Hints: It is ok if `product()` returns a result *different* from the `None` above. Look at the documentation of the built-in [sum() ](https://docs.python.org/3/library/functions.html#sum) function for some inspiration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = ...\n", + "\n", + " for arg in args:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, calling `product()` without any arguments returns what we would best describe as a *default* or *start* value. To be \"philosophical,\" what is the product of *no* numbers? We know that the product of *one* number is just the number itself, but what could be a reasonable result when multiplying *no* numbers? The answer is what you use as the initial value of `result` above, and there is only *one* way to make `product(42)` and `product(2, 5, 10)` work." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Rewrite `product()` so that it takes a *keyword-only* argument `start`, defaulting to the above *default* or *start* value, and use `start` internally instead of `result`!\n", + "\n", + "Hint: Remember that a *keyword-only* argument is any parameter specified in a function's header line after the first and only `*` (cf., [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Keyword-only-Arguments))." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, ...):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can call `product()` with a truly arbitrary number of *positional* arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Without any *positional* arguments but only the *keyword* argument `start`, for example, `start=0`, we can adjust the answer to the \"philosophical\" problem of multiplying *no* numbers. Because of the *keyword-only* syntax, there is *no* way to pass in a `start` number *without* naming it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(start=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We could use `start` to inject a multiplier, for example, to double the outcomes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42, start=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10, start=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is still one issue left: Because of the function's name, a user of `product()` may assume that it is ok to pass a *collection* of numbers, like `one_hundred`, which are then multiplied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: What is a **collection**? How is that different from a **sequence**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Rewrite the latest version of `product()` to check if the *only* positional argument is a *collection* type! If so, its elements are multiplied together. Otherwise, the logic remains the same.\n", + "\n", + "Hints: Use the built-in [len() ](https://docs.python.org/3/library/functions.html#len) and [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) functions to check if there is only *one* positional argument and if it is a *collection* type. Use the *abstract base class* `Collection` 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). You may want to *re-assign* `args` inside the body." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, ...):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " ...\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All *five* code cells below now return correct results. We may unpack `one_hundred` or not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(one_hundred)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(*one_hundred)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Side Note**: Above, we make `product()` work with a single *collection* type argument instead of a *sequence* type to keep it more generic: For example, we can pass in a `set` object, like `{2, 5, 10}` below, and `product()` continues to work correctly. The `set` type is introducted in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb#The-set-Type), and one essential difference to the `list` type is that objects of type `set` have *no* order regarding their elements. So, even though `[2, 5, 10]` and `{2, 5, 10}` look almost the same, the order implied in the literal notation gets lost in memory!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product([2, 5, 10]) # the argument is a collection that is also a sequence" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product({2, 5, 10}) # the argument is a collection that is NOT a sequence" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "isinstance({2, 5, 10}, abc.Sequence) # sets are NO sequences" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's continue to improve `product()` and make it more Pythonic. It is always a good idea to mimic the behavior of built-ins when writing our own functions. And, [sum() ](https://docs.python.org/3/library/functions.html#sum), for example, raises a `TypeError` if called *without* any arguments. It does *not* return the \"philosophical\" answer to adding *no* numbers, which would be `0`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sum()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: Adapt the latest version of `product()` to also raise a `TypeError` if called *without* any *positional* arguments!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, ...):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/07_sequences/05_appendix.ipynb b/07_sequences/05_appendix.ipynb new file mode 100644 index 0000000..f34771d --- /dev/null +++ b/07_sequences/05_appendix.ipynb @@ -0,0 +1,609 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/05_appendix.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 7: Sequential Data (Appendix)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the [third part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb#Tuples-are-like-\"Immutable-Lists\") of the chapter, we proposed the idea that `tuple` objects are like \"immutable lists.\" Often, however, we use `tuple` objects to represent a **record** of related **fields**. Then, each element has a *semantic* meaning (i.e., a descriptive name).\n", + "\n", + "As an example, think of a spreadsheet with information on students in a course. Each row represents a record and holds all the data associated with an individual student. The columns (e.g., matriculation number, first name, last name) are the fields that may come as *different* data types (e.g., `int` for the matriculation number, `str` for the names).\n", + "\n", + "A simple way of modeling a single student is as a `tuple` object, for example, `(123456, \"John\", \"Doe\")`. A disadvantage of this approach is that we must remember the order and meaning of the elements/fields in the `tuple` object.\n", + "\n", + "An example from a different domain is the representation of $(x, y)$-points in the $x$-$y$-plane. Again, we could use a `tuple` object like `current_position` below to model the point $(4, 2)$." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "current_position = (4, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We implicitly assume that the first element represents the $x$ and the second the $y$ coordinate. While that follows intuitively from convention in math, we should at least add comments somewhere in the code to document this assumption." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `namedtuple` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A better way is to create a *custom* data type. While that is covered in depth in [Chapter 11 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb), the [collections ](https://docs.python.org/3/library/collections.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides a [namedtuple() ](https://docs.python.org/3/library/collections.html#collections.namedtuple) **factory function** that creates \"simple\" custom data types on top of the standard `tuple` type." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from collections import namedtuple" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[namedtuple() ](https://docs.python.org/3/library/collections.html#collections.namedtuple) takes two arguments. The first argument is the name of the data type. That could be different from the variable `Point` we use to refer to the new type, but in most cases it is best to keep them in sync. The second argument is a sequence with the field names as `str` objects. The names' order corresponds to the one assumed in `current_position`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "Point = namedtuple(\"Point\", [\"x\", \"y\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `Point` object is a so-called **class**. That is what it means if an object is of type `type`. It can be used as a **factory** to create *new* `tuple`-like objects of type `Point`. In a way, [namedtuple() ](https://docs.python.org/3/library/collections.html#collections.namedtuple) gives us a way to create our own custom **constructors**." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94457911453856" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(Point)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "type" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(Point)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The value of `Point` is just itself in a *literal notation*." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.Point" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Point" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We write `Point(4, 2)` to create a *new* object of type `Point`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "current_position = Point(4, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, `current_position` has a somewhat nicer representation. In particular, the coordinates are named `x` and `y`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Point(x=4, y=2)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_position" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is *not* a `tuple` any more but an object of type `Point`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140376178109184" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(current_position)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.Point" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(current_position)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We use the dot operator `.` to access the defined attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_position.x" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_position.y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As before, we get an `AttributeError` if we try to access an undefined attribute." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'Point' object has no attribute 'z'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mcurrent_position\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mz\u001b[49m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'Point' object has no attribute 'z'" + ] + } + ], + "source": [ + "current_position.z" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`current_position` continues to work like a `tuple` object! That is why we can use `namedtuple` as a replacement for `tuple`. The underlying implementations exhibit the *same* computational efficiencies and memory usages.\n", + "\n", + "For example, we can index into or loop over `current_position` as it is still a sequence with the familiar four properties." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "4" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_position[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_position[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4\n", + "2\n" + ] + } + ], + "source": [ + "for number in current_position:\n", + " print(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "4\n" + ] + } + ], + "source": [ + "for number in reversed(current_position):\n", + " print(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(current_position)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/07_sequences/06_summary.ipynb b/07_sequences/06_summary.ipynb new file mode 100644 index 0000000..043ed03 --- /dev/null +++ b/07_sequences/06_summary.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 7: Sequential Data (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Sequences** are an *abstract* concept that summarizes *four* behaviors an object may or may not exhibit. Sequences are\n", + "- **finite** and\n", + "- **ordered**\n", + "- **containers** that we may\n", + "- **loop over**.\n", + "\n", + "Examples are the `list`, `tuple`, but also the `str` types.\n", + "\n", + "Objects that exhibit all behaviors *except* being ordered are referred to as **collections**.\n", + "\n", + "The objects inside a sequence are called its **elements** and may be labeled with a unique **index**, an `int` object in the range $0 \\leq \\text{index} < \\lvert \\text{sequence} \\rvert$.\n", + "\n", + "`list` objects are **mutable**. That means we can change the references to the other objects it contains, and, in particular, re-assign them.\n", + "\n", + "On the contrary, `tuple` objects are like **immutable** lists: We can use them in place of any `list` object as long as we do *not* need to mutate it. Often, `tuple` objects are also used to model **records** of related **fields**." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/07_sequences/07_review.ipynb b/07_sequences/07_review.ipynb new file mode 100644 index 0000000..57927bd --- /dev/null +++ b/07_sequences/07_review.ipynb @@ -0,0 +1,252 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 7: Sequential Data (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb), [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb), and the [third ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb) part of Chapter 7. Some questions regard the [Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/05_appendix.ipynb); that is indicated with a **\\***.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Answer the following questions *briefly*!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: We have seen **containers** and **iterables** before in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#Containers-vs.-Iterables). How do they relate to **sequences**? " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What are **abstract base classes**? How can we make use of the ones 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": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: How are the *abstract behaviors* of **reversibility** and **finiteness** essential for *indexing* and *slicing* sequences?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Explain the difference between **mutable** and **immutable** objects in Python with the examples of the `list` and `tuple` types!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What is the difference between a **shallow** and a **deep** copy of an object? How can one of them become a \"problem?\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Many **list methods** change `list` objects \"**in place**.\" What do we mean by that?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7.1**: `tuple` objects have *two* primary usages. First, they can be used in place of `list` objects where **mutability** is *not* required. Second, we use them to model data **records**.\n", + "\n", + "Describe why `tuple` objects are a suitable replacement for `list` objects in general!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7.2\\***: What do we mean by a **record**? How are `tuple` objects suitable to model records? How can we integrate a **semantic meaning** when working with records into our code?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: How is (iterable) **packing** and **unpacking** useful in the context of **function definitions** and **calls**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: `sequence` objects are *not* part of core Python but may be imported from the [standard library ](https://docs.python.org/3/library/index.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: The built-in [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) function takes a *finite* **iterable** as its argument an returns a *new* `list` object. On the contrary, the [sorted() ](https://docs.python.org/3/library/functions.html#sorted) method on `list` objects *mutates* them *in place*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Passing **mutable** objects as arguments to functions is not problematic because functions operate in a **local** scope without affecting the **global** scope." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/08_mfr/00_content.ipynb b/08_mfr/00_content.ipynb new file mode 100644 index 0000000..4a1ae86 --- /dev/null +++ b/08_mfr/00_content.ipynb @@ -0,0 +1,1381 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 8: Map, Filter, & Reduce" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this chapter, we continue the study of sequential data by looking at memory efficient ways to process the elements in a sequence. That is an important topic for the data science practitioner who must be able to work with data that does *not* fit into a single computer's memory.\n", + "\n", + "As shown in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#Containers-vs.-Iterables), both the `list` objects `[0, 1, 2, 3, 4]` and `[1, 3, 5, 7, 9]` on the one side and the `range` objects `range(5)` and `range(1, 10, 2)` on the other side allow us to loop over the same numbers. However, the latter two only create *one* `int` object in every iteration while the former two create *all* `int` objects before the loop even starts. In this aspect, we consider `range` objects to be \"rules\" in memory that know how to calculate the numbers *without* calculating them.\n", + "\n", + "In [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb#The-list-Type), we see how the built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor **materializes** the `range(1, 13)` object into the `list` object `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]`. In other words, we make `range(1, 13)` calculate *all* numbers at once and store them in a `list` object for further processing.\n", + "\n", + "In many cases, however, it is not necessary to do that, and, in this chapter, we look at other types of \"rules\" in memory and how we can compose different \"rules\" together to implement bigger computations.\n", + "\n", + "Next, we take a step back and continue with a simple example involving the familiar `numbers` list. Then, we iteratively exchange `list` objects with \"rule\"-like objects *without* changing the overall computation at all. As computations involving sequential data are commonly classified into three categories **map**, **filter**, or **reduce**, we do so too for our `numbers` example." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Mapping" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Mapping** refers to the idea of applying a transformation to every element in a sequence.\n", + "\n", + "For example, let's square each element in `numbers` and add `1` to the squares. In essence, we apply the transformation $y := x^2 + 1$ as expressed with the `transform()` function below." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def transform(element):\n", + " \"\"\"Map elements to their squares plus 1.\"\"\"\n", + " return (element ** 2) + 1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the syntax we know so far, we revert to a `for`-loop that iteratively appends the transformed elements to an initially empty `transformed_numbers` list." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "transformed_numbers = []\n", + "\n", + "for old in numbers:\n", + " new = transform(old)\n", + " transformed_numbers.append(new)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[50, 122, 65, 26, 10, 145, 5, 37, 82, 101, 2, 17]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transformed_numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As this kind of data processing is so common, Python provides the [map() ](https://docs.python.org/3/library/functions.html#map) built-in. In its simplest usage form, it takes two arguments: A transformation `function` that takes exactly *one* positional argument and an `iterable` that provides the objects to be mapped.\n", + "\n", + "We call [map() ](https://docs.python.org/3/library/functions.html#map) with a reference to the `transform()` function and the `numbers` list as the arguments and store the result in the variable `transformer` to inspect it." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "transformer = map(transform, numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We might expect to get back a materialized sequence (i.e., all elements exist in memory), and a `list` object would feel the most natural because of the type of the `numbers` argument. However, `transformer` is an object of type `map`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transformer" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "map" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(transformer)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Like `range` objects, `map` objects generate a series of objects \"on the fly\" (i.e., one by one), and we use the built-in [next() ](https://docs.python.org/3/library/functions.html#next) function to obtain the next object in line. So, we should think of a `map` object as a \"rule\" stored in memory that only knows how to calculate the next object of possibly *infinitely* many." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "50" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(transformer)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "122" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(transformer)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "65" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(transformer)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "It is essential to understand that by creating a `map` object with the [map() ](https://docs.python.org/3/library/functions.html#map) built-in, *nothing* happens in memory except the creation of the `map` object. In particular, no second `list` object derived from `numbers` is created. Also, we may view `range` objects as a special case of `map` objects: They are constrained to generating `int` objects only, and the `iterable` argument is replaced with `start`, `stop`, and `step` arguments.\n", + "\n", + "If we are sure that a `map` object generates a *finite* number of elements, we may materialize them into a `list` object with the built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor. Below, we \"pull out\" the remaining `int` objects from `transformer`, which itself is derived from a *finite* `list` object." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[26, 10, 145, 5, 37, 82, 101, 2, 17]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(transformer)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In summary, instead of creating an empty list first and appending it in a `for`-loop as above, we write the following one-liner and obtain an equal `transformed_numbers` list." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "transformed_numbers = list(map(transform, numbers))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[50, 122, 65, 26, 10, 145, 5, 37, 82, 101, 2, 17]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transformed_numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Filtering" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Filtering** refers to the idea of creating a subset of a sequence with a **boolean filter** `function` that indicates if an element should be kept (i.e., `True`) or not (i.e., `False`).\n", + "\n", + "In the example, let's only keep the even elements in `numbers`. The `is_even()` function implements that as a filter." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def is_even(element):\n", + " \"\"\"Filter out odd numbers.\"\"\"\n", + " if element % 2 == 0:\n", + " return True\n", + " return False" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As `element % 2 == 0` is already a boolean expression, we could shorten `is_even()` like so." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def is_even(element):\n", + " \"\"\"Filter out odd numbers.\"\"\"\n", + " return element % 2 == 0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As before, we first use a `for`-loop that appends the elements to be kept iteratively to an initially empty `even_numbers` list." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "even_numbers = []\n", + "\n", + "for number in transformed_numbers:\n", + " if is_even(number):\n", + " even_numbers.append(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[50, 122, 26, 10, 82, 2]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "even_numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogously to the `map` object above, we use the [filter() ](https://docs.python.org/3/library/functions.html#filter) built-in to create an object of type `filter` and assign it to `evens`." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "evens = filter(is_even, transformed_numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "evens" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "filter" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(evens)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`evens` works like `transformer` above: With the built-in [next() ](https://docs.python.org/3/library/functions.html#next) function we obtain the even numbers one by one. So, the \"next\" element in line is simply the next even `int` object the `filter` object encounters." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[50, 122, 65, 26, 10, 145, 5, 37, 82, 101, 2, 17]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transformed_numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "50" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(evens)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "122" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(evens)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "26" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(evens)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As above, we could create a materialized `list` object with the [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[50, 122, 26, 10, 82, 2]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(filter(is_even, transformed_numbers))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may also chain `map` and `filter` objects derived from the original `numbers` list. As the entire cell is *one* big expression consisting of nested function calls, we read it from the inside out." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[50, 122, 26, 10, 82, 2]" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(\n", + " filter(\n", + " is_even,\n", + " map(transform, numbers),\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Using the [map() ](https://docs.python.org/3/library/functions.html#map) and [filter() ](https://docs.python.org/3/library/functions.html#filter) built-ins, we can quickly switch the order: Filter first and then transform the remaining elements. This variant equals the \"*A simple Filter*\" example in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-A-simple-Filter). On the contrary, code with `for`-loops and `if` statements is more tedious to adapt. Additionally, `map` and `filter` objects loop \"at the C level\" and are a lot faster because of that. Because of that, experienced Pythonistas tend to *not* use explicit `for`-loops so often." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[65, 145, 5, 37, 101, 17]" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(\n", + " map(\n", + " transform,\n", + " filter(is_even, numbers),\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Reducing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, **reducing** sequential data means to summarize the elements into a single statistic.\n", + "\n", + "A simple example is the built-in [sum() ](https://docs.python.org/3/library/functions.html#sum) function." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(\n", + " map(\n", + " transform,\n", + " filter(is_even, numbers),\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Other straightforward examples are the built-in [min() ](https://docs.python.org/3/library/functions.html#min) or [max() ](https://docs.python.org/3/library/functions.html#max) functions." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "min(map(transform, filter(is_even, numbers)))" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "145" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max(map(transform, filter(is_even, numbers)))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[sum() ](https://docs.python.org/3/library/functions.html#sum), [min() ](https://docs.python.org/3/library/functions.html#min), and [max() ](https://docs.python.org/3/library/functions.html#max) can be regarded as special cases.\n", + "\n", + "The generic way of reducing a sequence is to apply a function of *two* arguments on a rolling horizon: Its first argument is the reduction of the elements processed so far, and the second the next element to be reduced.\n", + "\n", + "For illustration, let's replicate [sum() ](https://docs.python.org/3/library/functions.html#sum) as such a function, called `sum_alt()`. Its implementation only adds two numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def sum_alt(sum_so_far, next_number):\n", + " \"\"\"Reduce a sequence by addition.\"\"\"\n", + " return sum_so_far + next_number" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Further, we create a *new* `map` object derived from `numbers` ..." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "evens_transformed = map(transform, filter(is_even, numbers))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... and loop over all *but* the first element it generates. The latter is captured separately as the initial `result` with the [next() ](https://docs.python.org/3/library/functions.html#next) function. We know from above that `evens_transformed` generates *six* elements. That is why we see *five* growing `result` values resembling a [cumulative sum](http://mathworld.wolfram.com/CumulativeSum.html). The first `210` is the sum of the first two elements generated by `evens_transformed`, `65` and `145`.\n", + "\n", + "So, we also learn that `map` objects, and analogously `filter` objects, are *iterable* as we may loop over them." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "210 215 252 353 370 " + ] + } + ], + "source": [ + "result = next(evens_transformed)\n", + "\n", + "for number in evens_transformed:\n", + " result = sum_alt(result, number)\n", + " print(result, end=\" \") # line added for didactical purposes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The final `result` is the same `370` as above." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) function in the [functools ](https://docs.python.org/3/library/functools.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provides more convenience (and speed) replacing the `for`-loop. It takes two arguments, `function` and `iterable`, in the same way as the [map() ](https://docs.python.org/3/library/functions.html#map) and [filter() ](https://docs.python.org/3/library/functions.html#filter) built-ins.\n", + "\n", + "[reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) is **[eager ](https://en.wikipedia.org/wiki/Eager_evaluation)** meaning that all computations implied by the contained `map` and `filter` \"rules\" are executed immediately, and the code cell evaluates to `370`. On the contrary, [map() ](https://docs.python.org/3/library/functions.html#map) and [filter() ](https://docs.python.org/3/library/functions.html#filter) create **[lazy ](https://en.wikipedia.org/wiki/Lazy_evaluation)** `map` and `filter` objects, and we have to use the [next() ](https://docs.python.org/3/library/functions.html#next) function to obtain the elements, one by one." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from functools import reduce" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reduce(\n", + " sum_alt,\n", + " map(\n", + " transform,\n", + " filter(is_even, numbers),\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Lambda Expressions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[map() ](https://docs.python.org/3/library/functions.html#map), [filter() ](https://docs.python.org/3/library/functions.html#filter), and [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) take a `function` object as their first argument, and we defined `transform()`, `is_even()`, and `sum_alt()` to be used precisely for that.\n", + "\n", + "Often, such functions are used *only once* in a program. However, the primary purpose of functions is to *reuse* them. In such cases, it makes more sense to define them \"anonymously\" right at the position where the first argument goes.\n", + "\n", + "As mentioned in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Anonymous-Functions), we use `lambda` expressions to create `function` objects *without* a name referencing them.\n", + "\n", + "So, the above `sum_alt()` function could be rewritten as a `lambda` expression like so ..." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(sum_so_far, next_number)>" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lambda sum_so_far, next_number: sum_so_far + next_number" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... or even shorter." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(x, y)>" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lambda x, y: x + y" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the new concepts in this section, we can rewrite the entire example in just a few lines of code *without* any `for`, `if`, and `def` statements. The resulting code is concise, easy to read, quick to modify, and even faster in execution. Most importantly, it is optimized to handle big amounts of data as *no* temporary `list` objects are materialized in memory." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]\n", + "evens = filter(lambda x: x % 2 == 0, numbers)\n", + "transformed = map(lambda x: (x ** 2) + 1, evens)\n", + "sum(transformed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If `numbers` comes as a sorted sequence of whole numbers, we may use the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in and get away *without* any materialized `list` object in memory at all!" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers = range(1, 13)\n", + "evens = filter(lambda x: x % 2 == 0, numbers)\n", + "transformed = map(lambda x: (x ** 2) + 1, evens)\n", + "sum(transformed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To additionally save the temporary variables, `numbers`, `evens`, and `transformed`, we could write the entire computation as *one* expression." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(\n", + " map(\n", + " lambda x: (x ** 2) + 1,\n", + " filter(\n", + " lambda x: x % 2 == 0,\n", + " range(1, 13),\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "PythonTutor visualizes the differences in the number of computational steps and memory usage:\n", + "- [Version 1 ](http://pythontutor.com/visualize.html#code=def%20is_even%28element%29%3A%0A%20%20%20%20if%20element%20%25%202%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20True%0A%20%20%20%20return%20False%0A%0Adef%20transform%28element%29%3A%0A%20%20%20%20return%20%28element%20**%202%29%20%2B%201%0A%0Anumbers%20%3D%20list%28range%281,%2013%29%29%0A%0Aevens%20%3D%20%5B%5D%0Afor%20number%20in%20numbers%3A%0A%20%20%20%20if%20is_even%28number%29%3A%0A%20%20%20%20%20%20%20%20evens.append%28number%29%0A%0Atransformed%20%3D%20%5B%5D%0Afor%20number%20in%20evens%3A%0A%20%20%20%20transformed.append%28transform%28number%29%29%0A%0Aresult%20%3D%20sum%28transformed%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false): With `for`-loops, `if` statements, and named functions -> **116** steps and **3** `list` objects\n", + "- [Version 2 ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20range%281,%2013%29%0Aevens%20%3D%20filter%28lambda%20x%3A%20x%20%25%202%20%3D%3D%200,%20numbers%29%0Atransformed%20%3D%20map%28lambda%20x%3A%20%28x%20**%202%29%20%2B%201,%20evens%29%0Aresult%20%3D%20sum%28transformed%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false): With named `map` and `filter` objects -> **58** steps and **no** `list` object\n", + "- [Version 3 ](http://pythontutor.com/visualize.html#code=result%20%3D%20sum%28map%28lambda%20x%3A%20%28x%20**%202%29%20%2B%201,%20filter%28lambda%20x%3A%20x%20%25%202%20%3D%3D%200,%20range%281,%2013%29%29%29%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false): Everything in *one* expression -> **55** steps and **no** `list` object\n", + "\n", + "Versions 2 and 3 are the same, except for the three additional steps required to create the temporary variables. The *major* downside of Version 1 is that, in the worst case, it may need *three times* the memory as compared to the other two versions!\n", + "\n", + "An experienced Pythonista would probably go with Version 2 in a production system to keep the code readable and maintainable.\n", + "\n", + "The map-filter-reduce paradigm has caught attention in recent years as it enables **[parallel computing ](https://en.wikipedia.org/wiki/Parallel_computing)**, and this gets important when dealing with big amounts of data. The workings in the memory as shown in this section provide an idea why." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/08_mfr/01_content.ipynb b/08_mfr/01_content.ipynb new file mode 100644 index 0000000..38300c0 --- /dev/null +++ b/08_mfr/01_content.ipynb @@ -0,0 +1,2384 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/01_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After introducing the Map-Filter-Reduce paradigm in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb) of this chapter, we first see how `list` comprehensions can replace the [map() ](https://docs.python.org/3/library/functions.html#map) and [filter() ](https://docs.python.org/3/library/functions.html#filter) built-ins in many cases. Then, we learn how `generator` expressions are like `list` comprehensions *without* using the memory. We end this part with a short discussion of the built-in [all() ](https://docs.python.org/3/library/functions.html#all) and [any() ](https://docs.python.org/3/library/functions.html#any) functions." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## `list` Comprehensions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Consider again the original \"*A simple Filter*\" example from [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-A-simple-Filter), re-written such that both the mapping and the filtering are done in *one* `for`-loop instead of the *two* above." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "evens_transformed = []\n", + "\n", + "for number in numbers:\n", + " if number % 2 == 0:\n", + " evens_transformed.append((number ** 2) + 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(evens_transformed)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**`list` comprehensions** are *expressions* to derive *new* `list` objects out of *existing* ones (cf., [reference ](https://docs.python.org/3/reference/expressions.html#displays-for-lists-sets-and-dictionaries)). Practically, this means that we place the `for` and `if` inside brackets `[` and `]`.\n", + "\n", + "So, the example can be written in a single *expression* like below replacing the compound `for` *statement* above." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[65, 145, 5, 37, 101, 17]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[(n ** 2) + 1 for n in numbers if n % 2 == 0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A list comprehension may always be used in a place where otherwise a `list` object would work.\n", + "\n", + "For example, let's rewrite the \"*A simple Filter*\" example from [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-A-simple-Filter) in just one line. As a caveat, the code below *materializes* all elements in memory *before* summing them up, and may, therefore, cause a `MemoryError` when executed with a bigger `numbers` list. We see with [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20range%281,%2013%29%0Aresult%20%3D%20sum%28%5B%28n%20**%202%29%20%2B%201%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) how a `list` object exists in memory at step 17 and then \"gets lost\" right after. As the next section shows, this downside may be mitigated." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum([(n ** 2) + 1 for n in numbers if n % 2 == 0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Example: Nested Lists" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "List comprehensions may come with several `for`'s and `if`'s.\n", + "\n", + "The cell below creates a `list` object that contains three other `list` objects with a series of numbers in them. The first and last numbers in each inner `list` object are offset by `1`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "nested_numbers = [\n", + " [1, 2, 3, 4, 5, 6, 7],\n", + " [2, 3, 4, 5, 6, 7, 8],\n", + " [3, 4, 5, 6, 7, 8, 9],\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[1, 2, 3, 4, 5, 6, 7], [2, 3, 4, 5, 6, 7, 8], [3, 4, 5, 6, 7, 8, 9]]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To do something meaningful with the numbers, we have to remove the inner layer of `list` objects and **flatten** (i.e., \"un-nest\") the data.\n", + "\n", + "Without list comprehensions, we achieve that with two nested `for`-loops." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "flat_numbers = []\n", + "\n", + "for inner_numbers in nested_numbers:\n", + " for number in inner_numbers:\n", + " flat_numbers.append(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 2, 3, 4, 5, 6, 7, 8, 3, 4, 5, 6, 7, 8, 9]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flat_numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "That translates into a list comprehension like below. The order of the `for`'s may be confusing at first but is the *same* as writing out the nested `for`-loops as above." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 2, 3, 4, 5, 6, 7, 8, 3, 4, 5, 6, 7, 8, 9]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[number for inner_numbers in nested_numbers for number in inner_numbers]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, we may use the `list` object resulting from the list comprehension in any way we want.\n", + "\n", + "For example, to add up the flattened numbers with [sum() ](https://docs.python.org/3/library/functions.html#sum). The same caveat holds as before: The `list` object passed into [sum() ](https://docs.python.org/3/library/functions.html#sum) is *materialized* with *all* its elements *before* the sum is calculated!" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "105" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum([number for inner_numbers in nested_numbers for number in inner_numbers])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this particular example, however, we can exploit the fact that any sum of numbers can be expressed as the sum of sums of mutually exclusive and collectively exhaustive subsets of these numbers and get away with just *one* `for` in the list comprehension." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "105" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum([sum(inner_numbers) for inner_numbers in nested_numbers])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Working with Cartesian Products" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A popular use case of nested list comprehensions is applying a transformation to each $n$-tuple in the [Cartesian product ](https://en.wikipedia.org/wiki/Cartesian_product) created from elements of $n$ iterables. In the generic illustration below, a transformation $f(x, y)$ is applied to each $2$-tuple in the Cartesian product $X \\times Y$ where $x$ is an element in $X = \\{x_1, x_2, x_3\\}$ and $y$ is an element in $Y = \\{y_1, y_2, y_3\\}$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "|$Y$ \\ $X$| $x_1$ | $x_2$ | $x_3$ |\n", + "|---------|--------------|--------------|--------------|\n", + "| $y_1$ |f($x_1$,$y_1$)|f($x_2$,$y_1$)|f($x_3$,$y_1$)|\n", + "| $y_2$ |f($x_1$,$y_2$)|f($x_2$,$y_2$)|f($x_3$,$y_2$)|\n", + "| $y_3$ |f($x_1$,$y_3$)|f($x_2$,$y_3$)|f($x_3$,$y_3$)|" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For example, let's add each quotient obtained by taking the numerator $x$ from `[10, 20, 30]` and the denominator $y$ from `[40, 50, 60]` to `1`, and then find the overall product. This transformation can be described mathematically as the function $z = f(x, y)$ below." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "$$z = f(x, y) = \\prod{ (1 + \\frac{x}{y} )}$$" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Further, the table below visualizes the calculations: The result is the product of *nine* entries." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "|`y` \\ `x`|**10**|**20**|**30**|\n", + "|------|------|------|------|\n", + "|**40**| 1.25 | 1.50 | 1.75 |\n", + "|**50**| 1.20 | 1.40 | 1.60 |\n", + "|**60**| 1.17 | 1.33 | 1.50 |" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To express that in Python, we start by creating two `list` objects, `first` and `second`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "first = [10, 20, 30]\n", + "second = [40, 50, 60]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For a Cartesian product, we must loop over *all* possible $2$-tuples where one element is drawn from `first` and the other from `second`. We achieve that with two nested `for`-loops, in which we calculate each `quotient` and append it to an initially empty `cartesian_product` list." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1.25, 1.2, 1.1666666666666667, 1.5, 1.4, 1.3333333333333333, 1.75, 1.6, 1.5]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cartesian_product = []\n", + "\n", + "for numerator in first:\n", + " for denominator in second:\n", + " quotient = 1 + (numerator / denominator)\n", + " cartesian_product.append(quotient)\n", + "\n", + "cartesian_product" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Next, we convert the two explicit `for`-loops into one list comprehensions and use `x` and `y` as shorter variable names." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1.25, 1.2, 1.1666666666666667, 1.5, 1.4, 1.3333333333333333, 1.75, 1.6, 1.5]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[1 + (x / y) for x in first for y in second]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The order of the `for`'s is *important*: The list comprehension above divides numbers from `first` by numbers from `second`, whereas the list comprehension below does the opposite." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[5.0, 3.0, 2.333333333333333, 6.0, 3.5, 2.666666666666667, 7.0, 4.0, 3.0]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[1 + (x / y) for x in second for y in first]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To find the overall product, we *unpack* the list comprehension into the `product()` function from the \"*Function Definitions & Calls*\" sub-section in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb#Function-Definitions-&-Calls)." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def product(*args):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " result = args[0]\n", + "\n", + " for arg in args[1:]:\n", + " result *= arg\n", + "\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20.58" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(*[1 + (x / y) for x in first for y in second])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, we use a `lambda` expression with the [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) function from the [functools ](https://docs.python.org/3/library/functools.html) module." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "from functools import reduce" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20.58" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reduce(lambda x, y: x * y, [1 + (x / y) for x in first for y in second])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While this example is stylized, Cartesian products are hidden in many applications, and it shows how the various language features introduced in this chapter can be seamlessly combined to process sequential data." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## `generator` Expressions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because of the high memory consumption, Pythonistas avoid materialized `list` objects, and, thus, also `list` comprehensions, whenever possible. Instead, they prefer to work with **[`generator` expressions ](https://docs.python.org/3/reference/expressions.html#generator-expressions)**. Syntactically, they work like list comprehensions except that parentheses, `(` and `)`, replace brackets, `[` and `]`.\n", + "\n", + "Let's go back to the original \"*A simple Filter*\" example from [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb#Example:-A-simple-Filter) one more time, apply the transformation $y := x^2 + 1$ to all even `numbers`, and sum them up." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To filter and transform `numbers`, we wrote a list comprehension above ..." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[65, 145, 5, 37, 101, 17]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[(n ** 2) + 1 for n in numbers if n % 2 == 0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... that now becomes a `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + " at 0x7fcf703d84a0>" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "((n ** 2) + 1 for n in numbers if n % 2 == 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A `generator` expression evaluates to yet another \"rule\"-like object in memory that knows how to generate the individual objects in a series one by one. Whereas a `list` comprehension materializes its elements in memory *when* it is evaluated, the opposite holds true for `generator` expressions: *No* object is created in memory except the \"rule\" itself. Because of this behavior, we describe `generator` expressions as *lazy* and `list` comprehensions as *eager*.\n", + "\n", + "To materialize the elements specified by a `generator` expression, we use the [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor as seen above." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[65, 145, 5, 37, 101, 17]" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(((n ** 2) + 1 for n in numbers if n % 2 == 0))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whenever a `generator` expression is the only argument in a function call, we may merge the double parentheses, `((` and `))`, into just `(` and `)`." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[65, 145, 5, 37, 101, 17]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list((n ** 2) + 1 for n in numbers if n % 2 == 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A common use case is to reduce the elements into a single object instead, for example, by adding them up with [sum() ](https://docs.python.org/3/library/functions.html#sum) as in the original \"*A simple Filter*\" example. [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20range%281,%2013%29%0Asum_with_list%20%3D%20sum%28%5B%28n%20**%202%29%20%2B%201%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%29%0Asum_with_gen%20%3D%20sum%28%28n%20**%202%29%20%2B%201%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how the code cell below does *not* create a temporary `list` object in memory whereas a `list` comprehension would do so (cf., step 17)." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "370" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum((n ** 2) + 1 for n in numbers if n % 2 == 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's assign the object to which the `generator` expression below evaluates to to a variable `gen` and inspect it." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "gen = ((n ** 2) + 1 for n in numbers if n % 2 == 0)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + " at 0x7fcf703d8c10>" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gen" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Unsurprisingly, `generator` expressions create objects of type `generator`." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "generator" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(gen)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`generator` objects work just like the `map` and `filter` objects in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb) of this chapter. So, with the [next() ](https://docs.python.org/3/library/functions.html#next) function, we can generate elements one by one." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "65" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "145" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "37" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "101" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "17" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Once a `generator` object runs out of elements, it raises a `StopIteration` exception, and we say that the `generator` object is **exhausted**. To loop over its elements again, we must create a *new* one." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[36], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mgen\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[37], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mgen\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "next(gen)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Calling the [next() ](https://docs.python.org/3/library/functions.html#next) function repeatedly with the *same* `generator` object as the argument is essentially what a `for`-loop automates for us. So, `generator` objects are *iterable*. We look into this in detail further below in the \"*The `for` Statement (revisited)*\" section." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "65 145 5 37 101 17 " + ] + } + ], + "source": [ + "for number in ((n ** 2) + 1 for n in numbers if n % 2 == 0):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Example: Nested Lists (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we are only interested in a *reduction* of `nested_numbers` into a single statistic, as the overall sum in the \"*Nested Lists*\" example above, we should replace `list` objects or `list` comprehensions with `generator` expressions wherever possible. The result is the *same*, but no intermediate `list` objects are materialized! That makes our code scale to large amounts of data.\n", + "\n", + "Let's adapt the example `nested_numbers` from above." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[1, 2, 3, 4, 5, 6, 7], [2, 3, 4, 5, 6, 7, 8], [3, 4, 5, 6, 7, 8, 9]]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Compared to the implementation with the list comprehension above, we simply remove the brackets, `[` and `]`: The argument to [sum() ](https://docs.python.org/3/library/functions.html#sum) becomes a generator expression." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "105" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(number for inner_numbers in nested_numbers for number in inner_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "That also holds for the alternative formulation as a sum of sums." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "105" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(sum(inner_numbers) for inner_numbers in nested_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because `nested_numbers` has an internal structure (i.e., the inner `list` objects are series of consecutive `int` objects), we can even provide an effectively **memoryless** implementation by expressing it as a `generator` expression derived from `range` objects. [PythonTutor ](http://pythontutor.com/visualize.html#code=nested_numbers%20%3D%20%28%28range%28x,%20y%20%2B%201%29%29%20for%20x,%20y%20in%20zip%28range%281,%204%29,%20range%287,%2010%29%29%29%0Aresult%20%3D%20sum%28number%20for%20inner_numbers%20in%20nested_numbers%20for%20number%20in%20inner_numbers%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) confirms that no `list` objects materialize at any point in time." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "nested_numbers = ((range(x, y + 1)) for x, y in zip(range(1, 4), range(7, 10)))" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + " at 0x7fcf70347970>" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested_numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "105" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(number for inner_numbers in nested_numbers for number in inner_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We must be careful when assigning a `generator` object to a variable: If we use `nested_numbers` again, for example, in the alternative formulation below, [sum() ](https://docs.python.org/3/library/functions.html#sum) returns `0` because `nested_numbers` is exhausted after executing the previous code cell. [PythonTutor ](http://pythontutor.com/visualize.html#code=nested_numbers%20%3D%20%28%28range%28x,%20y%20%2B%201%29%29%20for%20x,%20y%20in%20zip%28range%281,%204%29,%20range%287,%2010%29%29%29%0Aresult%20%3D%20sum%28number%20for%20inner_numbers%20in%20nested_numbers%20for%20number%20in%20inner_numbers%29%0Ano_result%20%3D%20sum%28sum%28inner_numbers%29%20for%20inner_numbers%20in%20nested_numbers%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) also shows that." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(sum(inner_numbers) for inner_numbers in nested_numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Working with Cartesian Products (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's also rewrite the \"*Working with Cartesian Products*\" example from above with generator expressions.\n", + "\n", + "As a first optimization, we replace the materialized `list` objects, `first` and `second`, with memoryless `range` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "first = range(10, 31, 10) # \"==\" [10, 20, 30]\n", + "second = range(40, 61, 10) # \"==\" [40, 50, 60]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, the first of the two alternative solutions may be more appealing to many readers. In general, many practitioners seem to dislike `lambda` expressions.\n", + "\n", + "In the first solution, we *unpack* the elements produced by `(1 + (x / y) for x in first for y in second)` into the `product()` function from the \"*Function Definitions & Calls*\" sub-section in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb#Function-Definitions-&-Calls). However, inside `product()`, the elements are *packed* into `args`, a *materialized* `tuple` object! So, all the memory efficiency gained by using a generator expression is lost! [PythonTutor ](http://pythontutor.com/visualize.html#code=def%20product%28*args%29%3A%0A%20%20%20%20result%20%3D%20args%5B0%5D%0A%20%20%20%20for%20arg%20in%20args%5B1%3A%5D%3A%0A%20%20%20%20%20%20%20%20result%20*%3D%20arg%0A%20%20%20%20return%20result%0A%0Afirst%20%3D%20range%2810,%2031,%2010%29%0Asecond%20%3D%20range%2840,%2061,%2010%29%0A%0Aresult%20%3D%20product%28*%281%20%2B%20%28x%20/%20y%29%20for%20x%20in%20first%20for%20y%20in%20second%29%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how a `tuple` object exists in steps 38-58." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20.58" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product(*(1 + (x / y) for x in first for y in second)) # not memory efficient!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, the second solution with the [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) function from the [functools ](https://docs.python.org/3/library/functools.html) module and the `lambda` expression works *without* the elements materialized at the same time, and [PythonTutor ](http://pythontutor.com/visualize.html#code=from%20functools%20import%20reduce%0A%0Afirst%20%3D%20range%2810,%2031,%2010%29%0Asecond%20%3D%20range%2840,%2061,%2010%29%0A%0Aresult%20%3D%20reduce%28%0A%20%20%20%20lambda%20x,%20y%3A%20x%20*%20y,%0A%20%20%20%20%281%20%2B%20%28x%20/%20y%29%20for%20x%20in%20first%20for%20y%20in%20second%29%0A%29&cumulative=false&curstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) confirms that. So, only the second alternative is truly memory-efficient!" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "20.58" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reduce(lambda x, y: x * y, (1 + (x / y) for x in first for y in second))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In summary, we learn from this example that unpacking `generator` objects *may* be a *bad* idea." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Example: Averaging all even Numbers in a List (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the new concepts in this chapter, let's rewrite the book's introductory \"*Averaging all even Numbers in a List*\" example from [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List) such that it efficiently handles a large sequence of numbers. We continue from its latest implementation, the `average_evens()` function in the \"*Keyword-only Arguments*\" section in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Keyword-only-Arguments).\n", + "\n", + "We assume that `average_evens()` is called with a *finite* and *iterable* object that generates a **stream** of numeric objects that can be cast as `int` objects. After all, the idea of even and odd numbers makes sense only in the context of whole numbers.\n", + "\n", + "The `generator` expression `(round(n) for n in numbers)` implements the type casting, and, when it is evaluated during a function call, *nothing* happens except that a `generator` object is assigned to `integers`. Then, with the [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) function from the [functools ](https://docs.python.org/3/library/functools.html) module, we *simultaneously* add up *and* count the even numbers with the `generator` object to which the inner `generator` expression `((n, 1) for n in integers if n % 2 == 0)` evaluates to. That `generator` object takes the `integers` generator as its source and produces `tuple` objects consisting of the next *even* number in line and `1`. Two such `tuple` objects are then iteratively passed to the `function` object to which the `lambda` expression evaluates to. `x` represents the total and the count of the even numbers processed so far, while `y`'s first element, `y[0]`, is the next *even* number to be added to the running total. The result of calling [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) is again a `tuple` object, namely the final `total` and `count`. Lastly, we calculate the simple average and scale it.\n", + "\n", + "In summary, this implementation of `average_evens()` does *not* keep materialized `list` objects internally like its predecessors in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb) but processes the elements of the `numbers` argument on a one-by-one basis." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def average_evens(numbers, *, scalar=1):\n", + " \"\"\"Calculate the average of all even numbers.\n", + "\n", + " Args:\n", + " numbers (iterable): a finite stream of the numbers to be averaged;\n", + " if non-whole numbers are provided, they are rounded\n", + " scalar (float, optional): multiplies the average; defaults to 1\n", + "\n", + " Returns:\n", + " float: (scaled) average\n", + " \"\"\"\n", + " integers = (round(n) for n in numbers)\n", + " total, count = reduce(\n", + " lambda x, y: (x[0] + y[0], x[1] + y[1]),\n", + " ((n, 1) for n in integers if n % 2 == 0)\n", + " )\n", + " return scalar * total / count" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens([7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We may provide an optional `scalar` argument as before." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens([7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4], scalar=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "An argument with `float` objects works just as well." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "7.0" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens([7., 11., 8., 5., 3., 12., 2., 6., 9., 10., 1., 4.])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To show that `average_evens()` can process a large stream of data, we simulate `10_000_000` randomly drawn integers between `0` and `100` with the [randint() ](https://docs.python.org/3/library/random.html#random.randint) function from the [random ](https://docs.python.org/3/library/random.html) module. We use a `generator` expression derived from a `range` object as the `numbers` argument. So, at *no* point in time is there a materialized `list` object in memory. The result approaching `50` confirms that [randint() ](https://docs.python.org/3/library/random.html#random.randint) must be based on a uniform distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "49.994081434519636" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_evens(random.randint(0, 100) for _ in range(10_000_000))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To show that `average_evens()` filters out odd numbers, we simulate another stream of `10_000_000` randomly drawn odd integers between `1` and `99`. As no function in the [random ](https://docs.python.org/3/library/random.html) module does that \"out of the box,\" we must be creative: Doubling a number drawn from `random.randint(0, 49)` results in an even number between `0` and `98`, and adding `1` makes it odd. Then, `average_evens()` raises a `TypeError`, essentially because `(int(n) for n in numbers)` does not generate any element." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "reduce() of empty iterable with no initial value", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[56], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43maverage_evens\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mrandom\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrandint\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m49\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mrange\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m10_000_000\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[49], line 13\u001b[0m, in \u001b[0;36maverage_evens\u001b[0;34m(numbers, scalar)\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Calculate the average of all even numbers.\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \n\u001b[1;32m 4\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;124;03m float: (scaled) average\u001b[39;00m\n\u001b[1;32m 11\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 12\u001b[0m integers \u001b[38;5;241m=\u001b[39m (\u001b[38;5;28mround\u001b[39m(n) \u001b[38;5;28;01mfor\u001b[39;00m n \u001b[38;5;129;01min\u001b[39;00m numbers)\n\u001b[0;32m---> 13\u001b[0m total, count \u001b[38;5;241m=\u001b[39m \u001b[43mreduce\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 14\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mlambda\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 15\u001b[0m \u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43m(\u001b[49m\u001b[43mn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mintegers\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mn\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m%\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m==\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 16\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m scalar \u001b[38;5;241m*\u001b[39m total \u001b[38;5;241m/\u001b[39m count\n", + "\u001b[0;31mTypeError\u001b[0m: reduce() of empty iterable with no initial value" + ] + } + ], + "source": [ + "average_evens(2 * random.randint(0, 49) + 1 for _ in range(10_000_000))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## `tuple` Comprehensions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There is no syntax to derive *new* `tuple` objects out of existing ones. However, we can mimic such a construct by combining the built-in [tuple() ](https://docs.python.org/3/library/functions.html#func-tuple) constructor with a `generator` expression.\n", + "\n", + "So, to convert the `list` comprehension `[(n ** 2) + 1 for n in numbers if n % 2 == 0]` from above into a \"`tuple` comprehension,\" we write the following." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(65, 145, 5, 37, 101, 17)" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tuple((n ** 2) + 1 for n in numbers if n % 2 == 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Boolean Reducers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Besides [min() ](https://docs.python.org/3/library/functions.html#min), [max() ](https://docs.python.org/3/library/functions.html#max), and [sum() ](https://docs.python.org/3/library/functions.html#sum), Python provides two boolean reduce functions: [all() ](https://docs.python.org/3/library/functions.html#all) and [any() ](https://docs.python.org/3/library/functions.html#any).\n", + "\n", + "Let's look at straightforward examples involving `numbers` again." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[all() ](https://docs.python.org/3/library/functions.html#all) takes an `iterable` argument and returns `True` if *all* elements are *truthy*.\n", + "\n", + "For example, let's check if the square of each element in `numbers` is below `100` or `150`, respectively. We express the computation with a `generator` expression passed as the only argument to [all() ](https://docs.python.org/3/library/functions.html#all)." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all(x ** 2 < 100 for x in numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all(x ** 2 < 150 for x in numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[all() ](https://docs.python.org/3/library/functions.html#all) can be viewed as syntactic sugar replacing a `for`-loop: Internally, [all() ](https://docs.python.org/3/library/functions.html#all) implements the *short-circuiting* strategy explained in [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#Short-Circuiting), and we mimic that by testing for the *opposite* condition in the `if` statement and stopping the `for`-loop early with the `break` statement. In the worst case, if `threshold` were, for example, `150`, we would loop over *all* elements in the `iterable` argument, which must be *finite* for the code to work. So, [all() ](https://docs.python.org/3/library/functions.html#all) is a *linear search* in disguise." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "threshold = 100\n", + "\n", + "for number in numbers:\n", + " if number ** 2 >= threshold: # the opposite of what we are checking for\n", + " all_below_threshold = False\n", + " break\n", + "else:\n", + " all_below_threshold = True\n", + "\n", + "all_below_threshold" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The documentation of [all() ](https://docs.python.org/3/library/functions.html#all) shows in another way what it does with code: By placing a `return` statement in a `for`-loop's body inside a function, iteration is stopped prematurely once an element does *not* meet the condition. That is the familiar *early exit* pattern at work." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def all_alt(iterable):\n", + " \"\"\"Alternative implementation of the built-in all() function.\"\"\"\n", + " for element in iterable:\n", + " if not element: # the opposite of what we are checking for\n", + " return False\n", + " return True" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_alt(x ** 2 < 100 for x in numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_alt(x ** 2 < 150 for x in numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, [any() ](https://docs.python.org/3/library/functions.html#any) checks if *at least* one element in the `iterable` argument is *truthy*.\n", + "\n", + "To continue the example, let's check if the square of *any* element in `numbers` is above `100` or `150`, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "any(x ** 2 > 100 for x in numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "any(x ** 2 > 150 for x in numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The code cell below shows how [any() ](https://docs.python.org/3/library/functions.html#any) works internally: It also follows the *short-circuiting* strategy. Here, we do *not* need to check for the opposite condition." + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "threshold = 100\n", + "\n", + "for number in numbers:\n", + " if number ** 2 > threshold:\n", + " any_above_threshold = True\n", + " break\n", + "else:\n", + " any_above_threshold = False\n", + "\n", + "any_above_threshold" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The alternative formulation in the documentation of [any() ](https://docs.python.org/3/library/functions.html#any) is straightforward and also uses the early exit pattern." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def any_alt(iterable):\n", + " \"\"\"Alternative implementation of the built-in any() function.\"\"\"\n", + " for element in iterable:\n", + " if element:\n", + " return True\n", + " return False" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "any_alt(x ** 2 > 100 for x in numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "any_alt(x ** 2 > 150 for x in numbers)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/08_mfr/02_exercises_outliers.ipynb b/08_mfr/02_exercises_outliers.ipynb new file mode 100644 index 0000000..1ad6876 --- /dev/null +++ b/08_mfr/02_exercises_outliers.ipynb @@ -0,0 +1,760 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/02_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb) and [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/01_content.ipynb) part of Chapter 8.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Removing Outliers in Streaming Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's say we are given a `list` object with random integers like `sample` below, and we want to calculate some basic statistics on them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample = [\n", + " 45, 46, 40, 49, 36, 53, 49, 42, 25, 40, 39, 36, 38, 40, 40, 52, 36, 52, 40, 41,\n", + " 35, 29, 48, 43, 42, 30, 29, 33, 55, 33, 38, 50, 39, 56, 52, 28, 37, 56, 45, 37,\n", + " 41, 41, 37, 30, 51, 32, 23, 40, 53, 40, 45, 39, 99, 42, 34, 42, 34, 39, 39, 53,\n", + " 43, 37, 46, 36, 45, 42, 32, 38, 57, 34, 36, 44, 47, 51, 46, 39, 28, 40, 35, 46,\n", + " 41, 51, 41, 23, 46, 40, 40, 51, 50, 32, 47, 36, 38, 29, 32, 53, 34, 43, 39, 41,\n", + " 40, 34, 44, 40, 41, 43, 47, 57, 50, 42, 38, 25, 45, 41, 58, 37, 45, 55, 44, 53,\n", + " 82, 31, 45, 33, 32, 39, 46, 48, 42, 47, 40, 45, 51, 35, 31, 46, 40, 44, 61, 57,\n", + " 40, 36, 35, 55, 40, 56, 36, 35, 86, 36, 51, 40, 54, 50, 49, 36, 41, 37, 48, 41,\n", + " 42, 44, 40, 43, 51, 47, 46, 50, 40, 23, 40, 39, 28, 38, 42, 46, 46, 42, 46, 31,\n", + " 32, 40, 48, 27, 40, 40, 30, 32, 25, 31, 30, 43, 44, 29, 45, 41, 63, 32, 33, 58,\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: `list` objects are **sequences**. What *four* behaviors do they always come with?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Write a function `mean()` that calculates the simple arithmetic mean of a given `sequence` with numbers!\n", + "\n", + "Hints: You can solve this task with [built-in functions ](https://docs.python.org/3/library/functions.html) only. A `for`-loop is *not* needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def mean(sequence):\n", + " ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_mean = mean(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_mean" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Write a function `std()` that calculates the [standard deviation ](https://en.wikipedia.org/wiki/Standard_deviation) of a `sequence` of numbers! Integrate your `mean()` version from before and the [sqrt() ](https://docs.python.org/3/library/math.html#math.sqrt) function from the [math ](https://docs.python.org/3/library/math.html) module in the [standard library ](https://docs.python.org/3/library/index.html) provided to you below. Make sure `std()` calls `mean()` only *once* internally! Repeated calls to `mean()` would be a waste of computational resources.\n", + "\n", + "Hints: Parts of the code are probably too long to fit within the suggested 79 characters per line. So, use *temporary variables* inside your function. Instead of a `for`-loop, you may want to use a `list` comprehension or, even better, a memoryless `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from math import sqrt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def std(sequence):\n", + " ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_std = std(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_std" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Complete `standardize()` below that takes a `sequence` of numbers and returns a `list` object with the **[z-scores ](https://en.wikipedia.org/wiki/Standard_score)** of these numbers! A z-score is calculated by subtracting the mean and dividing by the standard deviation. Re-use `mean()` and `std()` from before. Again, ensure that `standardize()` calls `mean()` and `std()` only *once*! Further, round all z-scores with the built-in [round() ](https://docs.python.org/3/library/functions.html#round) function and pass on the keyword-only argument `digits` to it.\n", + "\n", + "Hint: You may want to use a `list` comprehension instead of a `for`-loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def standardize(sequence, *, digits=3):\n", + " ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores = standardize(sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The [pprint() ](https://docs.python.org/3/library/pprint.html#pprint.pprint) function from the [pprint ](https://docs.python.org/3/library/pprint.html) module in the [standard library ](https://docs.python.org/3/library/index.html) allows us to \"pretty print\" long `list` objects compactly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pprint(z_scores, compact=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We know that `standardize()` works correctly if the resulting z-scores' mean and standard deviation approach `0` and `1` for a long enough `sequence`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean(z_scores), std(z_scores)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Even though `standardize()` calls `mean()` and `std()` only once each, `mean()` is called *twice*! That is so because `std()` internally also re-uses `mean()`!\n", + "\n", + "**Q5.1**: Rewrite `std()` to take an optional keyword-only argument `seq_mean`, defaulting to `None`. If provided, `seq_mean` is used instead of the result of calling `mean()`. Otherwise, the latter is called.\n", + "\n", + "Hint: You must check if `seq_mean` is still the default value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def std(sequence, *, seq_mean=None):\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`std()` continues to work as before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_std = std(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample_std" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5.2**: Now, rewrite `standardize()` to pass on the return value of `mean()` to `std()`! In summary, `standardize()` calculates the z-scores for the numbers in the `sequence` with as few computational steps as possible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def standardize(sequence, *, digits=3):\n", + " ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores = standardize(sample)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean(z_scores), std(z_scores)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: With both `sample` and `z_scores` being materialized `list` objects, we can loop over pairs consisting of a number from `sample` and its corresponding z-score. Write a `for`-loop that prints out all the \"outliers,\" as which we define numbers with an absolute z-score above `1.96`. There are *four* of them in the `sample`.\n", + "\n", + "Hint: Use the [abs() ](https://docs.python.org/3/library/functions.html#abs) and [zip() ](https://docs.python.org/3/library/functions.html#zip) built-ins." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We provide a `stream` module with a `data` object that models an *infinite* **stream** of data (cf., the [*stream.py* ](https://github.com/webartifex/intro-to-python/blob/main/08_mfr/stream.py) file in the repository)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from stream import data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`data` is of type `generator` and has *no* length." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So, the only thing we can do with it is to pass it to the built-in [next() ](https://docs.python.org/3/library/functions.html#next) function and go over the numbers it streams one by one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "next(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: What happens if you call `mean()` with `data` as the argument? What is the problem?\n", + "\n", + "Hints: If you try it out, you may have to press the \"Stop\" button in the toolbar at the top. Your computer should *not* crash, but you will *have to* restart this Jupyter notebook with \"Kernel\" > \"Restart\" and import `data` again." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Write a function `take_sample()` that takes an `iterable` as its argument, like `data`, and creates a *materialized* `list` object out of its first `n` elements, defaulting to `1_000`!\n", + "\n", + "Hints: [next() ](https://docs.python.org/3/library/functions.html#next) and the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in may be helpful. You may want to use a `list` comprehension instead of a `for`-loop and write a one-liner. Audacious students may want to look at [isclice() ](https://docs.python.org/3/library/itertools.html#itertools.islice) in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](https://docs.python.org/3/library/index.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def take_sample(iterable, *, n=1_000):\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We take a `new_sample` from the stream of `data`, and its statistics are similar to the initial `sample`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "new_sample = take_sample(data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(new_sample)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mean(new_sample)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "std(new_sample)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Convert `standardize()` into a *new* function `standardized()` that implements the *same* logic but works on a possibly *infinite* stream of data, provided as an `iterable`, instead of a *finite* `sequence`.\n", + "\n", + "To calculate a z-score, we need the stream's overall mean and standard deviation, and that is *impossible* to calculate if we do not know how long the stream is, and, in particular, if it is *infinite*. So, `standardized()` first takes a sample from the `iterable` internally, and uses the sample's mean and standard deviation to calculate the z-scores.\n", + "\n", + "Hint: `standardized()` *must* return a `generator` object. So, use a `generator` expression as the return value; unless you know about the `yield` statement already (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement))." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def standardized(iterable, *, digits=3):\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`standardized()` works almost like `standardize()` except that we use it with [next() ](https://docs.python.org/3/library/functions.html#next) to obtain the z-scores one by one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores = standardized(data)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "z_scores" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(z_scores)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "next(z_scores)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.1**: `standardized()` allows us to go over an *infinite* stream of z-scores. What we want to do instead is to loop over the stream's raw numbers and skip the outliers. In the remainder of this exercise, you look at the parts that make up the `skip_outliers()` function below to achieve precisely that.\n", + "\n", + "The first steps in `skip_outliers()` are the same as in `standardized()`: We take a `sample` from the stream of `data` and calculate its statistics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample = ...\n", + "seq_mean = ...\n", + "seq_std = ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.2**: Just as in `standardized()`, write a `generator` expression that produces z-scores one by one! However, instead of just generating a z-score, the resulting `generator` object should produce `tuple` objects consisting of a \"raw\" number from `data` and its z-score.\n", + "\n", + "Hint: Look at the revisited \"*Averaging Even Numbers*\" example in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb#Example:-Averaging-all-even-Numbers-in-a-List-%28revisited%29) for some inspiration, which also contains a `generator` expression producing `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "standardizer = (... for ... in data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`standardizer` should produce `tuple` objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "next(standardizer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.3**: Write another `generator` expression that loops over `standardizer`. It contains an `if`-clause that keeps only numbers with an absolute z-score below the `threshold_z`. If you fancy, use `tuple` unpacking." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "threshold_z = 1.96" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "no_outliers = (... for ... in standardizer if ...)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`no_outliers` should produce `int` objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "next(no_outliers)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10.4**: Lastly, put everything together in the `skip_outliers()` function! Make sure you refer to `iterable` inside the function and not the global `data`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def skip_outliers(iterable, *, threshold_z=1.96):\n", + " sample = ...\n", + " seq_mean = ...\n", + " seq_std = ...\n", + " standardizer = ...\n", + " no_outliers = ...\n", + " return no_outliers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can create a `generator` object and loop over the `data` in the stream with outliers skipped. Instead of the default `1.96`, we use a `threshold_z` of only `0.05`: That filters out all numbers except `42`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "skipper = skip_outliers(data, threshold_z=0.05)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "skipper" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(skipper)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "next(skipper)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: You implemented the functions `mean()`, `std()`, `standardize()`, `standardized()`, and `skip_outliers()`. Which of them are **eager**, and which are **lazy**? How do these two concepts relate to **finite** and **infinite** data?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/08_mfr/03_exercises_un-packing.ipynb b/08_mfr/03_exercises_un-packing.ipynb new file mode 100644 index 0000000..5723ec6 --- /dev/null +++ b/08_mfr/03_exercises_un-packing.ipynb @@ -0,0 +1,455 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/03_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb) and [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/01_content.ipynb) part of Chapter 8.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Packing & Unpacking with Functions (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Copy your solution to **Q10** from the \"*Packing & Unpacking with Functions*\" exercise in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/04_exercises.ipynb) into the code cell below!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, ...):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Verify that all test cases below work (i.e., the `assert` statements must *not* raise an `AssertionError`)!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(42) == 42" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(2, 5, 10) == 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(2, 5, 10, start=2) == 200" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "one_hundred = [2, 5, 10]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(one_hundred) == 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "assert product(*one_hundred) == 100" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: Verify that `product()` raises a `TypeError` when called without any arguments!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This implementation of `product()` is convenient to use, in particular, because we can pass it any *collection* object with or without *unpacking* it.\n", + "\n", + "However, `product()` suffers from one last flaw: We cannot pass it a **stream** of data, as modeled, for example, with a `generator` object that produces elements on a one-by-one basis.\n", + "\n", + "**Q4**: Click through the following code cells and observe what they do!\n", + "\n", + "The [*stream.py* ](https://github.com/webartifex/intro-to-python/blob/main/08_mfr/stream.py) module in the book's repository provides a `make_finite_stream()` function. It is a *factory* function creating objects of type `generator` that we use to model *streaming* data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from stream import make_finite_stream" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = make_finite_stream()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`generator` objects are good for only *one* thing: Giving us the \"next\" element in a series of possibly *infinitely* many objects. While the `data` object is finite (i.e., execute the next code cell until you see a `StopIteration` exception), ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "next(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "... it has *no* concept of a \"length:\" The built-in [len() ](https://docs.python.org/3/library/functions.html#len) function raises a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can use the built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor to *materialize* all elements. However, in a real-world scenario, these may *not* fit into our machine's memory! If you get an empty `list` object below, you have to create a *new* `data` object above again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "list(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To be more realistic, `make_finite_stream()` creates `generator` objects producing a varying number of elements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "list(make_finite_stream())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "list(make_finite_stream())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "list(make_finite_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see what happens if we pass a `generator` object, as created by `make_finite_stream()`, instead of a materialized *collection*, like `one_hundred`, to `product()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(make_finite_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What line causes the `TypeError`? What line is really the problem in `product()`? Hint: These may be different lines. Describe what happens on each line in the function's body until the exception is raised!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Adapt `product()` one last time to make it work with `generator` objects, or more generallz *iterators*, as well!\n", + "\n", + "Hints: This task is as easy as replacing `Collection` with something else. Which of the three behaviors of *collections* do `generator` objects also exhibit? You may want to look at the documentations on the built-in [max() ](https://docs.python.org/3/library/functions.html#max), [min() ](https://docs.python.org/3/library/functions.html#min), and [sum() ](https://docs.python.org/3/library/functions.html#sum) functions: What kind of argument do they take?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def product(*args, ...):\n", + " \"\"\"Multiply all arguments.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " ...\n", + " ...\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The final version of `product()` behaves like built-ins in edge cases (i.e., `sum()` also raises a `TypeError` when called without arguments), ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "... works with the arguments passed either separately as *positional* arguments, *packed* together into a single *collection* argument, or *unpacked*, ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(42)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(2, 5, 10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product([2, 5, 10])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(*[2, 5, 10])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "... and can handle *streaming* data with *indefinite* \"length.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "product(make_finite_stream())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In real-world projects, the data science practitioner must decide if it is worthwhile to make a function usable in various different forms as we do in this exercise. This may be over-engineered.\n", + "\n", + "Yet, two lessons are important to take away:\n", + "- It is a good idea to *mimic* the behavior of *built-ins* when accepting arguments, and\n", + "- make functions capable of working with *streaming* data." + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/08_mfr/04_content.ipynb b/08_mfr/04_content.ipynb new file mode 100644 index 0000000..a04f016 --- /dev/null +++ b/08_mfr/04_content.ipynb @@ -0,0 +1,1672 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/04_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 8: Map, Filter, & Reduce" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly to how we classify different *concrete* data types like `list` or `str` by how they behave *abstractly* in a given context in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Collections-vs.-Sequences), we also do so for the data types we have introduced in this chapter." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Iterators vs. Iterables" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Here, the `map`, `filter`, and `generator` types all behave like \"rules\" in memory that govern how objects are produced \"on the fly.\" Their main commonality is their support for the built-in [next() ](https://docs.python.org/3/library/functions.html#next) function. In computer science terminology, such data types are called **[iterators ](https://en.wikipedia.org/wiki/Iterator)**, and the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module formalizes them with the `Iterator` ABC in Python.\n", + "\n", + "So, one example of an iterator is `evens_transformed` below, an object of type `generator`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "evens_transformed = ((x ** 2) + 1 for x in numbers if x % 2 == 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's first confirm that `evens_transformed` is indeed an `Iterator`, \"abstractly speaking.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(evens_transformed, abc.Iterator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In Python, iterators are *always* also iterables. The reverse is *not* true! To be precise, iterators are *specializations* of iterables. That is what the \"Inherits from\" column means in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module's documentation." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(evens_transformed, abc.Iterable)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Furthermore, we sharpen our definition of an *iterable* from [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Collections-vs.-Sequences): Just as we define an *iterator* to be any object that supports the [next() ](https://docs.python.org/3/library/functions.html#next) function, we define an *iterable* to be any object that supports the built-in [iter() ](https://docs.python.org/3/library/functions.html#iter) function.\n", + "\n", + "The confused reader may now be wondering how the two concepts relate to each other.\n", + "\n", + "In short, the [iter() ](https://docs.python.org/3/library/functions.html#iter) function is the general way to create an *iterator* object out of a given *iterable* object. Then, the *iterator* object manages the iteration over the *iterable* object. In real-world code, we hardly ever see [iter() ](https://docs.python.org/3/library/functions.html#iter) as Python calls it for us in the background. \n", + "\n", + "For illustration, let's do that ourselves and create *two* iterators out of the iterable `numbers` and see what we can do with them." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "iterator1 = iter(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "iterator2 = iter(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`iterator1` and `iterator2` are of type `list_iterator`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "list_iterator" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(iterator1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "*Iterators* are useful for only *one* thing: Get the next object from the associated *iterable*.\n", + "\n", + "By calling [next() ](https://docs.python.org/3/library/functions.html#next) three times with `iterator1` as the argument, we obtain the first three elements of `numbers`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(7, 11, 8)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(iterator1), next(iterator1), next(iterator1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`iterator1` and `iterator2` keep their *states* separate. So, we could loop over the same *iterable* several times in parallel." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(5, 7)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(iterator1), next(iterator2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We can also play a \"trick\" and exchange some elements in `numbers`. `iterator1` and `iterator2` do *not* see these changes and present us with the new elements. So, *iterators* not only have state on their own but also keep this separate from the underlying *iterable*." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers[1], numbers[4] = 99, 99" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(99, 99)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(iterator1), next(iterator2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's re-assign the elements in `numbers` so that they are in order. After that, the numbers returned from [next() ](https://docs.python.org/3/library/functions.html#next) also tell us how often [next() ](https://docs.python.org/3/library/functions.html#next) was called with `iterator1` or `iterator2`. We conclude that `list_iterator` objects work by simply keeping track of the *last* index obtained from the underlying *iterable*." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers[:] = list(range(1, 13))" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(6, 3)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(iterator1), next(iterator2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the concepts introduced in this section, we can now understand the first sentence in the documentation on the [zip() ](https://docs.python.org/3/library/functions.html#zip) built-in better: \"Make an *iterator* that aggregates elements from each of the *iterables*.\"\n", + "\n", + "Because *iterators* are always also *iterables*, we may pass `iterator1` and `iterator2` as arguments to [zip() ](https://docs.python.org/3/library/functions.html#zip).\n", + "\n", + "The returned `zipper` object is of type `zip` and, \"abstractly speaking,\" an `Iterator` as well." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "zipper = zip(iterator1, iterator2)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zipper" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "zip" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(zipper)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(zipper, abc.Iterator)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, we have always used [zip() ](https://docs.python.org/3/library/functions.html#zip) in a `for`-loop. That was our earlier definition of an *iterable*. Our revised definition in this section states that an *iterable* is an object that supports the [iter() ](https://docs.python.org/3/library/functions.html#iter) function. So, let's see what happens if we pass `zipper` to [iter() ](https://docs.python.org/3/library/functions.html#iter)." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "zipper_iterator = iter(zipper)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zipper_iterator" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`zipper_iterator` references the *same* object as `zipper`! That is true for *iterators* in general: Any *iterator* created from an existing *iterator* with [iter() ](https://docs.python.org/3/library/functions.html#iter) is the *iterator* itself." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zipper is zipper_iterator" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The Python core developers made that design decision so that *iterators* may also be looped over.\n", + "\n", + "The `for`-loop below prints out only *six* more `tuple` objects derived from the now ordered `numbers` because the `iterator1` object hidden inside `zipper` already returned its first *six* elements. So, the respective first elements of the `tuple` objects printed range from `7` to `12`. Similarly, as `iterator2` already returned its first *three* elements from `numbers`, we see the respective second elements in the range from `4` to `9`." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "7 and 4 8 and 5 9 and 6 10 and 7 11 and 8 12 and 9 " + ] + } + ], + "source": [ + "for x, y in zipper:\n", + " print(x, \"and\", y, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`zipper` is now *exhausted*. So, the `for`-loop below does *not* make any iteration at all." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "for x, y in zipper:\n", + " raise RuntimeError(\"We won't see this error\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We verify that `iterator1` is exhausted by passing it to [next() ](https://docs.python.org/3/library/functions.html#next) again, which raises a `StopIteration` exception." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "StopIteration", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mStopIteration\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[25], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43miterator1\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mStopIteration\u001b[0m: " + ] + } + ], + "source": [ + "next(iterator1)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, `iterator2` is *not* yet exhausted." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(iterator2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Understanding *iterators* and *iterables* is helpful for any data science practitioner that deals with large amounts of data. Even without that, these two terms occur everywhere in Python-related texts and documentation. So, a beginner should regularly review this section until it becomes second nature." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The `for` Statement (revisited)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#The-for-Statement), we argue that the `for` statement is syntactic sugar, replacing the `while` statement in many common scenarios. In particular, a `for`-loop saves us two tasks: Managing an index variable *and* obtaining the individual elements by indexing. In this sub-section, we look at a more realistic picture, using the new terminology as well.\n", + "\n", + "Let's print out the elements of a `list` object as the *iterable* being looped over." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "iterable = [0, 1, 2, 3, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 " + ] + } + ], + "source": [ + "for element in iterable:\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Our previous and equivalent formulation with a `while` statement is like so." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 " + ] + } + ], + "source": [ + "index = 0\n", + "\n", + "while index < len(iterable):\n", + " element = iterable[index]\n", + " print(element, end=\" \")\n", + " index += 1\n", + "\n", + "del index" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "What actually happens behind the scenes in the Python interpreter is shown below.\n", + "\n", + "First, Python calls [iter() ](https://docs.python.org/3/library/functions.html#iter) with the `iterable` to be looped over as the argument. The returned `iterator` contains the entire logic of how the `iterable` is looped over. In particular, the `iterator` may or may not pick the `iterable`'s elements in a predictable order. That is up to the \"rule\" it models.\n", + "\n", + "Second, Python enters an *indefinite* `while`-loop. It tries to obtain the next element with [next() ](https://docs.python.org/3/library/functions.html#next). If that succeeds, the `for`-loop's code block is executed. Below, that code is placed within the `else`-clause that runs only if *no* exception is raised in the `try`-clause. Then, Python jumps into the next iteration and tries to obtain the next element from the `iterator`, and so on. Once the `iterator` is exhausted, it raises a `StopIteration` exception, and Python stops the `while`-loop with the `break` statement." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0 1 2 3 4 " + ] + } + ], + "source": [ + "iterator = iter(iterable)\n", + "\n", + "while True:\n", + " try:\n", + " element = next(iterator)\n", + " except StopIteration:\n", + " break\n", + " else:\n", + " print(element, end=\" \")\n", + "\n", + "del iterator" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## sorted() vs. reversed()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now that we know the concept of an *iterator*, let's compare some of the built-ins introduced in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb) in detail and make sure we understand what is going on in memory. This section also serves as a summary of all the concepts in this chapter.\n", + "\n", + "We use two simple examples, `numbers` and `memoryless`: `numbers` creates *thirteen* objects in memory and `memoryless` only *one* (cf., [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0Amemoryless%20%3D%20range%281,%2013%29&cumulative=false&curstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false))." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "memoryless = range(1, 13)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function takes a *finite* `iterable` argument and *materializes* its elements into a *new* `list` object that is returned.\n", + "\n", + "The argument may already be materialized, as is the case with `numbers`, but may also be an *iterable* without any objects in it, such as `memoryless`. In both cases, we end up with materialized `list` objects with the elements sorted in *forward* order (cf., [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0Amemoryless%20%3D%20range%281,%2013%29%0Aresult1%20%3D%20sorted%28numbers%29%0Aresult2%20%3D%20sorted%28memoryless%29&cumulative=false&curstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false))." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sorted(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sorted(memoryless)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By adding a keyword-only argument `reverse=True`, the materialized `list` objects are sorted in *reverse* order (cf., [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0Amemoryless%20%3D%20range%281,%2013%29%0Aresult1%20%3D%20sorted%28numbers,%20reverse%3DTrue%29%0Aresult2%20%3D%20sorted%28memoryless,%20reverse%3DTrue%29&cumulative=false&curstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false))." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sorted(numbers, reverse=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sorted(memoryless, reverse=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The order in `numbers` remains *unchanged*, and `memoryless` is still *not* materialized." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "range(1, 13)" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "memoryless" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in takes a `sequence` argument and returns an *iterator*. The argument must be *finite* and *reversible* (i.e., *iterable* in *reverse* order) as otherwise [reversed() ](https://docs.python.org/3/library/functions.html#reversed) could neither determine the last element that becomes the first nor loop in a *predictable* backward fashion. [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0Amemoryless%20%3D%20range%281,%2013%29%0Aiterator1%20%3D%20reversed%28numbers%29%0Aiterator2%20%3D%20reversed%28memoryless%29&cumulative=false&curstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) confirms that [reversed() ](https://docs.python.org/3/library/functions.html#reversed) does *not* materialize any elements but only returns an *iterator*.\n", + "\n", + "**Side Note**: Even though `range` objects, like `memoryless` here, do *not* \"contain\" references to other objects, they count as *sequence* types, and as such, they are also *container* types. The `in` operator works with `range` objects because we can always cast the object to be checked as an `int` and check if that lies within the `range` object's `start` and `stop` values, taking a potential `step` value into account (cf., this [blog post](https://treyhunner.com/2018/02/python-range-is-not-an-iterator/) for more details on the [range() ](https://docs.python.org/3/library/functions.html#func-range) built-in)." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reversed(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reversed(memoryless)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To materialize the elements, we can pass the returned *iterators* to, for example, the [list() ](https://docs.python.org/3/library/functions.html#func-list) or [tuple() ](https://docs.python.org/3/library/functions.html#func-tuple) constructors. That creates *new* `list` and `tuple` objects (cf., [PythonTutor ](http://pythontutor.com/visualize.html#code=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0Amemoryless%20%3D%20range%281,%2013%29%0Aresult1%20%3D%20list%28reversed%28numbers%29%29%0Aresult2%20%3D%20tuple%28reversed%28memoryless%29%29&cumulative=false&curstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false)).\n", + "\n", + "To reiterate some more new terminology from this chapter, we describe [reversed() ](https://docs.python.org/3/library/functions.html#reversed) as *lazy* whereas [list() ](https://docs.python.org/3/library/functions.html#func-list) and [tuple() ](https://docs.python.org/3/library/functions.html#func-tuple) are *eager*. The former has no significant side effect in memory, while the latter may require a lot of memory." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[4, 1, 10, 9, 6, 2, 12, 3, 5, 8, 11, 7]" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(reversed(numbers))" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tuple(reversed(memoryless))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, we can also loop over the returned *iterators* instead.\n", + "\n", + "That works because *iterators* are always *iterable*; in particular, as the previous \"*The for Statement (revisited)*\" sub-section explains, the `for`-loops below call `iter(reversed(numbers))` and `iter(reversed(memoryless))` behind the scenes. However, the *iterators* returned by [iter() ](https://docs.python.org/3/library/functions.html#iter) are the *same* as the `reversed(numbers)` and `reversed(memoryless)` iterators passed in! In summary, the `for`-loops below involve many subtleties that together make Python the expressive language it is." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4 1 10 9 6 2 12 3 5 8 11 7 " + ] + } + ], + "source": [ + "for number in reversed(numbers):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12 11 10 9 8 7 6 5 4 3 2 1 " + ] + } + ], + "source": [ + "for element in reversed(memoryless):\n", + " print(element, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As with [sorted() ](https://docs.python.org/3/library/functions.html#sorted), the [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in does *not* mutate its argument." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "range(1, 13)" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "memoryless" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To point out the potentially obvious, we compare the results of *sorting* `numbers` in *reverse* order with *reversing* it: These are *different* concepts!" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sorted(numbers, reverse=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[4, 1, 10, 9, 6, 2, 12, 3, 5, 8, 11, 7]" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list(reversed(numbers))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whereas both [sorted() ](https://docs.python.org/3/library/functions.html#sorted) and [reversed() ](https://docs.python.org/3/library/functions.html#reversed) do *not* mutate their arguments, the *mutable* `list` type comes with two methods, [sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) and `reverse()`, that implement the same logic but mutate an object, like `numbers` below, *in place*. To indicate that all changes occur *in place*, the [sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) and `reverse()` methods always return `None`, which is not shown in JupyterLab." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `reverse()` method on the `list` type is *eager*, as opposed to the *lazy* [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in. That means the mutations caused by the `reverse()` method are written into memory right away." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers.reverse()" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[4, 1, 10, 9, 6, 2, 12, 3, 5, 8, 11, 7]" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "*Sorting* `numbers` in place occurs eagerly." + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers.sort(reverse=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers.sort()" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/08_mfr/05_summary.ipynb b/08_mfr/05_summary.ipynb new file mode 100644 index 0000000..28859bc --- /dev/null +++ b/08_mfr/05_summary.ipynb @@ -0,0 +1,75 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The operations we do with sequential data commonly follow the **map-filter-reduce paradigm**: We apply the same transformation to all elements, filter some of them out, and calculate summary statistics from the remaining ones.\n", + "\n", + "An essential idea in this chapter is that, in many situations, we need *not* have all the data **materialized** in memory. Instead, **iterators** allow us to process sequential data on a one-by-one basis.\n", + "\n", + "Examples for iterators are the `map`, `filter`, and `generator` types." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/08_mfr/06_review.ipynb b/08_mfr/06_review.ipynb new file mode 100644 index 0000000..0ebf0b5 --- /dev/null +++ b/08_mfr/06_review.ipynb @@ -0,0 +1,214 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 8: Map, Filter, & Reduce (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb), [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/01_content.ipynb), and [third ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/04_content.ipynb) part in Chapter 8.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: With the [map() ](https://docs.python.org/3/library/functions.html#map) and [filter() ](https://docs.python.org/3/library/functions.html#filter) built-ins and the [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) function from the [functools ](https://docs.python.org/3/library/functools.html) module in the [standard library ](https://docs.python.org/3/library/index.html), we can replace many tedious `for`-loops and `if` statements. What are some advantages of doing so?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Looking at the `lambda` expression inside [reduce() ](https://docs.python.org/3/library/functools.html#functools.reduce) below, what [built-in function ](https://docs.python.org/3/library/functions.html) is mimicked here?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "from functools import reduce\n", + "\n", + "numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]\n", + "\n", + "reduce(lambda x, y: x if x > y else y, numbers)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: What is the primary use case of **`list` comprehensions**? Why do we describe them as **eager**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: **`generator` expressions** may replace `list` objects and list comprehensions in many scenarios. When evaluated, they create a **lazy** `generator` object that does *not* **materialize** its elements right away. What do we mean by that? What does it mean for a `generator` object to be **exhausted**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What does it mean for the **boolean reducers**, the built-in [all() ](https://docs.python.org/3/library/functions.html#all) and [any() ](https://docs.python.org/3/library/functions.html#any) functions, to follow the **short-circuiting** strategy?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: What is an **iterator**? How does it relate to an **iterable**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: `lambda` expressions are useful in the context of the **map-filter-reduce** paradigm, where we often do *not* re-use a `function` object more than once." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Using **`generator` expressions** in place of **`list` comprehensions** wherever possible is a good practice as it makes our programs use memory more efficiently." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Just as **`list` comprehensions** create `list` objects, **`tuple` comprehensions** create `tuple` objects." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/08_mfr/stream.py b/08_mfr/stream.py new file mode 100644 index 0000000..6aa6cc7 --- /dev/null +++ b/08_mfr/stream.py @@ -0,0 +1,51 @@ +"""Simulation of random streams of data. + +This module defines: +- a generator object `data` modeling an infinite stream of integers +- a function `make_finite_stream()` that creates finite streams of data + +The probability distribution underlying the integers is Gaussian-like with a +mean of 42 and a standard deviation of 8. The left tail of the distribution is +cut off meaning that the streams only produce non-negative numbers. Further, +one in a hundred random numbers has an increased chance to be an outlier. +""" + +import itertools as _itertools +import random as _random + + +_random.seed(87) + + +def _infinite_stream(): + """Internal generator function to simulate an infinite stream of data.""" + while True: + number = max(0, int(_random.gauss(42, 8))) + if _random.randint(1, 100) == 1: + number *= 2 + yield number + + +def make_finite_stream(min_=5, max_=15): + """Simulate a finite stream of data. + + The returned stream is finite, but the number of elements to be produced + by it is still random. This default behavior may be turned off by passing + in `min_` and `max_` arguments with `min_ == max_`. + + Args: + min_ (optional, int): minimum numbers in the stream; defaults to 5 + max_ (optional, int): maximum numbers in the stream; defaults to 15 + + Returns: + finite_stream (generator) + + Raises: + ValueError: if max_ < min_ + """ + stream = _infinite_stream() + n = _random.randint(min_, max_) + yield from _itertools.islice(stream, n) + + +data = _infinite_stream() diff --git a/09_mappings/00_content.ipynb b/09_mappings/00_content.ipynb new file mode 100644 index 0000000..9926f40 --- /dev/null +++ b/09_mappings/00_content.ipynb @@ -0,0 +1,3773 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 9: Mappings & Sets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb) focuses on one special kind of *collection* types, namely *sequences*, this chapter introduces two more kinds: **Mappings** and **sets**. Both are presented in this chapter as they share the *same* underlying implementation.\n", + "\n", + "The `dict` type (cf, [documentation ](https://docs.python.org/3/library/stdtypes.html#dict)) introduced in the next section is an essential part in a data scientist's toolbox for two reasons: First, Python employs `dict` objects basically everywhere internally. Second, after the many concepts involving *sequential* data, *mappings* provide a different perspective on data and enhance our general problem solving skills." + ] + }, + { + "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 *value* every object has: In the \"bag\" analogy from [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Value-/-%28Semantic%29-\"Meaning\"), we describe an object's value to be the semantic meaning of the $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 *objects* on their own with distinct *values*.\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. Also, the commas `,` after the respective *last* items, `2: \"two\"` and `\"two\": 2`, are *not* a mistake although they *may* be left out. Besides easier reading, such a style has technical advantages that we do not go into detail about here (cf., [source ](https://www.python.org/dev/peps/pep-0008/#when-to-use-trailing-commas))." + ] + }, + { + "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": "fragment" + } + }, + "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." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139936685526208" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(to_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dict" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(to_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'zero', 1: 'one', 2: 'two'}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139936686018688" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(from_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dict" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(from_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from_words" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The built-in [dict() ](https://docs.python.org/3/library/functions.html#func-dict) constructor 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 obtain a *shallow* copy of a `dict` object or convert any other mapping object into a `dict` one." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dict(from_words)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Second, we may pass it a *finite* `iterable` providing *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": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dict([(\"zero\", 0), (\"one\", 1), (\"two\", 2)])" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 11, + "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": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dict(zero=0, one=1, two=2)" + ] + }, + { + "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 entities \"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. In the example, [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.\n", + "\n", + "`people` makes many implicit assumptions about the structure of the data. For example, there are a [one-to-many ](https://en.wikipedia.org/wiki/One-to-many_%28data_model%29) relationship between a person 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." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "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": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "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": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... but 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": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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/main/00_intro/00_content.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 build some intuition, we describe the underlying implementation details on a conceptual level.\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": 17, + "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)", + "Cell \u001b[0;32mIn[17], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m {\n\u001b[1;32m 2\u001b[0m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mzero\u001b[39m\u001b[38;5;124m\"\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mone\u001b[39m\u001b[38;5;124m\"\u001b[39m]: [\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m],\n\u001b[1;32m 3\u001b[0m }\n", + "\u001b[0;31mTypeError\u001b[0m: unhashable type: 'list'" + ] + } + ], + "source": [ + "{\n", + " [\"zero\", \"one\"]: [0, 1],\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 keeps the position of the *first* mention of the `\"zero\"` key while only the *last* mention of the corresponding values, `999`, survives." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 999, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{\n", + " \"zero\": 0,\n", + " \"one\": 1,\n", + " \"two\": 2,\n", + " \"zero\": 999, # to illustrate a point\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The reason for that is that the `dict` type is implemented with so-called [hash tables ](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 than needed to store the references to all the key and value objects. This bag is a **contiguous array** similar to the `list` type's implementation. Whereas in the `list` case the array is divided into equally sized *slots* capable of holding *one* reference, a `dict` object's array is divided into equally sized **buckets** with enough space to store *two* references each: One for an item's key and one for the mapped value. The buckets are labeled with *index* numbers. Because Python knows how wide each bucket, it can jump directly into *any* bucket by calculating its *offset* from the start.\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 index.\n", + "\n", + "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 and \"random\" `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 inside the object.\n", + "\n", + "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! Without this formal requirement an object may end up in *different* buckets depending on its current value. As the name of the `dict` type (i.e., \"dictionary\") suggests, a primary purpose of it is to insert objects and look them up later on. Without a *unique* bucket, this is of course not doable. 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\"`, an immutable `str` object. Hash values have *no* semantic meaning. Also, every time 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": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-85344695604937002" + ] + }, + "execution_count": 19, + "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": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hash(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hash(0.0)" + ] + }, + { + "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 semantic value. The converse statement does *not* hold: Two objects *may* (accidentally) have the *same* hash value and *not* compare equal. However, that rarely happens." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 == 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 23, + "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": 24, + "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)", + "Cell \u001b[0;32mIn[24], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mhash\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mzero\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mone\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: unhashable type: 'list'" + ] + } + ], + "source": [ + "hash([\"zero\", \"one\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we need keys composed of several objects, we can use `tuple` objects instead." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "-1616807732336770172" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hash((\"zero\", \"one\"))" + ] + }, + { + "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": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{('zero', 'one'): [0, 1]}" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{\n", + " (\"zero\", \"one\"): [0, 1],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After obtaining the key object's hash value, Python must still convert that into a bucket index. We do not cover this step in technical detail but provide a conceptual description of it.\n", + "\n", + "The `buckets()` function below shows how we can obtain indexes from the binary representation of a hash value by simply extracting its least significant `bits` and interpreting them as index numbers. Alternatively, the hash value may also be divided with the `%` operator by the number of available buckets. We show this idea in the `buckets_alt()` function that takes the number of buckets, `n_buckets`, as its second argument." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def buckets(mapping, *, bits):\n", + " \"\"\"Calculate the bucket indices for a mapping's keys.\"\"\"\n", + " for key in mapping: # cf., next 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[-8:], address, bucket, sep=\"\\t\")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "def buckets_alt(mapping, *, n_buckets):\n", + " \"\"\"Calculate the bucket indices for a mapping's keys.\"\"\"\n", + " for key in mapping: # cf., next section for details on looping\n", + " hash_value = hash(key)\n", + " bucket = hash_value % n_buckets\n", + " print(key, hash_value, bucket, sep=\"\\t\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With an infinite number of possible keys being mapped to a limited number of buckets, there is a realistic chance that two or more keys end up in the *same* bucket. That is called a **hash collision**. In such cases, Python uses a perturbation rule to rearrange the bits, and if the corresponding next bucket is empty, places an item there. Then, the nice offsetting logic from above breaks down and Python needs more time on average to place items into a hash table or look them up. The remedy is to use a bigger hash table as then the chance of collisions decreases. Python does all that for us in the background, and the main cost we pay for that is a *high* memory usage of `dict` objects in general.\n", + "\n", + "Because keys with the *same* semantic value have the *same* hash value, they end up in the *same* bucket. That is why the item that gets inserted last *overwrites* all previously inserted items whose keys compare equal, as we saw with the two `\"zero\"` keys above.\n", + "\n", + "Thus, to come up with indexes for 4 buckets, we need to extract 2 bits from the hash value (i.e., $2^2 = 4$)." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\t-85344695604937002\t0b...00101010\t10\t2\n", + "one\t6414592332130781825\t0b...10000001\t01\t1\n", + "two\t4316247523642253857\t0b...00100001\t01\t1\n" + ] + } + ], + "source": [ + "buckets(from_words, bits=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\t-85344695604937002\t2\n", + "one\t6414592332130781825\t1\n", + "two\t4316247523642253857\t1\n" + ] + } + ], + "source": [ + "buckets_alt(from_words, n_buckets=4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarly, 3 bits provide indexes for 8 buckets (i.e., $2^3 = 8$) ..." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\t-85344695604937002\t0b...00101010\t010\t2\n", + "one\t6414592332130781825\t0b...10000001\t001\t1\n", + "two\t4316247523642253857\t0b...00100001\t001\t1\n" + ] + } + ], + "source": [ + "buckets(from_words, bits=3)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\t-85344695604937002\t6\n", + "one\t6414592332130781825\t1\n", + "two\t4316247523642253857\t1\n" + ] + } + ], + "source": [ + "buckets_alt(from_words, n_buckets=8)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... while 4 bits do so for 16 buckets (i.e., $2^4 = 16$)." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\t-85344695604937002\t0b...00101010\t1010\t10\n", + "one\t6414592332130781825\t0b...10000001\t0001\t1\n", + "two\t4316247523642253857\t0b...00100001\t0001\t1\n" + ] + } + ], + "source": [ + "buckets(from_words, bits=4)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\t-85344695604937002\t6\n", + "one\t6414592332130781825\t1\n", + "two\t4316247523642253857\t1\n" + ] + } + ], + "source": [ + "buckets_alt(from_words, n_buckets=16)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Python allocates the memory for a `dict` object's hash table according to some internal heuristics: 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", + "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 of Key-Value Pairs" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Collections-vs.-Sequences), we see how a *sequence* is a special kind of a *collection*, and that collections can be described as\n", + "- *iterable*\n", + "- *containers*\n", + "- with a *finite* number of elements.\n", + "\n", + "The `dict` type is another *collection* type and has these three properties as well.\n", + "\n", + "For example, 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. In the terminology of 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), both are `Sized` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(to_words)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(from_words)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Also, `dict` objects may be looped over, for example, with the `for` statement. So, in the terminology of the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module, they are `Iterable` objects.\n", + "\n", + "Regarding the *iteration order* things are not that easy, and programmers seem to often be confused about this (e.g., this [discussion](https://stackoverflow.com/questions/58413076/why-are-python-dictionaries-not-reversible-for-python3-7)). The confusion usually comes from one of two reasons:\n", + "1. The internal implementation of the `dict` type has been changed over the last couple of minor release versions, and the communication thereof in the official release notes was done only in a later version. In a nutshell, before Python 3.6, the core developers did not care about the iteration order at all as the goal was to optimize `dict` objects for computational speed, primarily regarding key look-up (cf., the \"Indexing -> Key Look-up\" section below). That meant that looping over the *same* `dict` object several times during its lifetime could have resulted in *different* iteration orders. In Python 3.6, it was discovered that it is possible to make `dict` objects remember the order in that items have been inserted *without* giving up any computational speed or memory (cf., [Raymond Hettinger](https://github.com/rhettinger)'s talk in the [Further Resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/08_resources.ipynb#History-of-the-dict-Type) section at the end of the chapter. However, that change was kept an *implementation detail* and *not* made official in the release notes. That was then done in Python 3.7's release notes (cf., [Python 3.7 release notes ](https://www.python.org/downloads/release/python-370/)).\n", + "2. To make order an official part of a data type, it must adhere to the `Reversible` ABC in the [collections.abc ](https://docs.python.org/3/library/collections.abc.html) module and support the [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in. Even though the items' order inside a `dict` is remembered for Python 3.6 and 3.7, `dict` objects are *not* `Reversible`. That was then changed in Python 3.8, but again *not* officially communicated (cf., [Python 3.8 release notes](https://www.python.org/downloads/release/python-380/)).\n", + "\n", + "In summary, we can say that depending on the exact Python version a `dict` object *may* remember the **insertion order** of its items.\n", + "\n", + "However, that order is only apparent to us (i.e., we could look it up) if we put the data stored in a `dict` object into the *source code* itself. Then, we say that we \"hard code\" the data in our program. That is often *not* useful as we want our software load the data to be processed, for example, from a file or a database.\n", + "\n", + "Therefore, we suggest and adopt the following *best practices* in this book:\n", + "- We *assume* that the items in a `dict` object are *not* in a **predictable order** and *never* make the correctness of the logic in our code dependent on it.\n", + "- Whenever we want or need to model data in `dict`-like objects with an *explicit* order, we use the [OrderedDict ](https://docs.python.org/3/library/collections.html#collections.OrderedDict) type in the [collections ](https://docs.python.org/3/library/collections.html) module in the [standard library ](https://docs.python.org/3/library/index.html).\n", + "\n", + "If you installed Python, as recommended, via the Anaconda Distribution, 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*." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Python 3.12.2\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*\" section below shows how to loop over the *items* or the *values* instead." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "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": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\n", + "one\n", + "two\n" + ] + } + ], + "source": [ + "for word in from_words:\n", + " print(word)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For Python 3.8, `dict` objects are `Reversible` as well. So, passing a `dict` object to the [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in works. However, for ealier Python versions, the two next cells raise a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "1\n", + "0\n" + ] + } + ], + "source": [ + "for number in reversed(to_words):\n", + " print(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "two\n", + "one\n", + "zero\n" + ] + } + ], + "source": [ + "for word in reversed(from_words):\n", + " print(word)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, we may always use the built-in [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function to loop over, for example, `from_words` in a *predictable* order. However, that creates a temporary `list` object in memory and an order that has *nothing* to do with how the items are ordered inside the `dict` object." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "one\n", + "two\n", + "zero\n" + ] + } + ], + "source": [ + "for word in sorted(from_words):\n", + " print(word)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To show the `Container` behavior of *collection* types, we use the boolean `in` operator to check if a given object evaluates equal to a *key* in `to_words` or `from_words`." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1.0 in to_words # 1.0 is not a key but compares equal to a key" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-1 in to_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": [ + "\"one\" in from_words" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"ten\" in from_words" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Membership Testing: `list` vs. `dict`" + ] + }, + { + "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: 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. Then, the Python interpreter jumps right there in only *one* step. Because that is true no matter how many items are in the hash table, we call that a **constant time** operation.\n", + "\n", + "Conceptually, the overall behavior of the `in` operator is like comparing the searched object against *all* key objects with the `==` operator *without* doing it.\n", + "\n", + "To show the speed, we run an experiment. 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": 47, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(87)" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "needle = 42" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "random.shuffle(haystack)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[8126, 7370, 3735, 213, 7922, 1434, 8557, 9609, 9704, 9564]" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "haystack[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[7237, 886, 5945, 4014, 4998, 2055, 3531, 6919, 7875, 1944]" + ] + }, + "execution_count": 53, + "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 `haystack` a total of `10` times." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4.44 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 the `haystack` into the keys of a `magic_haystack`, a `dict` object. We use `None` as a dummy value for all items." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "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 `magic_haystack` 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": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "560 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 compares equal to the 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`." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "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": 58, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'zero'" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'zero'" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words_list[0]" + ] + }, + { + "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": 60, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 60, + "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": 61, + "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)", + "Cell \u001b[0;32mIn[61], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfrom_words\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mdrei\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\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.\n", + "\n", + "To access deeper 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": 62, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'name': 'Gilbert Strang', 'emails': ['gilbert@mit.edu']},\n", + " {'name': 'Leonhard Euler', 'emails': []}]" + ] + }, + "execution_count": 62, + "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": 63, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Gilbert Strang', 'emails': ['gilbert@mit.edu']}" + ] + }, + "execution_count": 63, + "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": 64, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Gilbert Strang'" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people[\"mathematicians\"][0][\"name\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... or his `\"emails\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['gilbert@mit.edu']" + ] + }, + "execution_count": 65, + "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": [ + "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* the references to the mapped value objects." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'zero', 1: 'one', 2: 'two'}" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "to_words[0] = \"null\"\n", + "to_words[1] = \"eins\"\n", + "to_words[2] = \"zwei\"" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'null', 1: 'eins', 2: 'zwei'}" + ] + }, + "execution_count": 68, + "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": 69, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "to_words[3] = \"drei\"\n", + "to_words[4] = \"vier\"" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'null', 1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" + ] + }, + "execution_count": 70, + "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": 71, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139936685526208" + ] + }, + "execution_count": 71, + "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": 72, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "del to_words[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" + ] + }, + "execution_count": 73, + "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": 74, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people[\"physicists\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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": 76, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Guido', 'emails': ['guido@python.org', 'guido@dropbox.com']}" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people[\"programmers\"][0]" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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": 78, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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": 79, + "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": "slide" + } + }, + "source": [ + "## `dict` 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 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": 80, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(from_words, abc.Mapping)" + ] + }, + { + "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(from_words, abc.MutableMapping)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While iteration over a mapping type already 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": 83, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero\n", + "one\n", + "two\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": 84, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['zero', 'one', 'two'])" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from_words.keys()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Views 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 as the newly created `list` object has a \"*predictable*\" order (i.e., indexes `0`, `1`, ...) created from an *unpredictable* one." + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['zero', 'one', 'two']" + ] + }, + "execution_count": 85, + "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* (i.e., type `dict_values`) on the value objects inside `from_words` without copying them." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "2\n" + ] + } + ], + "source": [ + "for number in from_words.values():\n", + " print(number)" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_values([0, 1, 2])" + ] + }, + "execution_count": 87, + "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 (i.e., type `dict_items`) 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": 88, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zero -> 0\n", + "one -> 1\n", + "two -> 2\n" + ] + } + ], + "source": [ + "for word, number in from_words.items():\n", + " print(f\"{word} -> {number}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_items([('zero', 0), ('one', 1), ('two', 2)])" + ] + }, + "execution_count": 89, + "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": 90, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: 'eins', 2: 'zwei', 3: 'drei', 4: 'vier'}" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "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)", + "Cell \u001b[0;32mIn[91], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mto_words\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\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": 92, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'n/a'" + ] + }, + "execution_count": 92, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words.get(0, \"n/a\")" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'eins'" + ] + }, + "execution_count": 93, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words.get(1, \"n/a\")" + ] + }, + { + "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) constructor allows, as well." + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "to_spanish = {\n", + " 0: \"cero\",\n", + " 1: \"uno\",\n", + " 2: \"dos\",\n", + " 3: \"tres\",\n", + " 4: \"cuatro\",\n", + " 5: \"cinco\", \n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "to_words.update(to_spanish)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: 'uno', 2: 'dos', 3: 'tres', 4: 'cuatro', 0: 'cero', 5: 'cinco'}" + ] + }, + "execution_count": 96, + "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." + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 97, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from_words" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "number = from_words.pop(\"zero\")" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 99, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "number" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'one': 1, 'two': 2}" + ] + }, + "execution_count": 100, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from_words" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With an optional `default` argument, the loud `KeyError` 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": "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)", + "Cell \u001b[0;32mIn[101], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mfrom_words\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mzero\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mKeyError\u001b[0m: 'zero'" + ] + } + ], + "source": [ + "from_words.pop(\"zero\")" + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 102, + "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": 103, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "word, number = from_words.popitem()" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "('two', 2)" + ] + }, + "execution_count": 104, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "word, number" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'one': 1}" + ] + }, + "execution_count": 105, + "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": 106, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "to_words.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 107, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_words" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from_words.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 109, + "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 also *mutates* the `dict` object.\n", + "\n", + "Consider the `people` example again and note how the `dict` object modeling `\"Albert Einstein\"` has *no* `\"emails\"` key in it." + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "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\"`. We cannot be sure if a `dict` object modeling a person has already a `\"emails\"` key or not. To play it safe, 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. Then, 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 first." + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "people[\"mathematicians\"][1].setdefault(\"emails\", []).append(\"leonhard@math.org\")" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "people[\"physicists\"][0].setdefault(\"emails\", []).append(\"albert@physics.org\")" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "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": [ + "`dict` objects also 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": 114, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "guido = people[\"programmers\"][0].copy()" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Guido van Rossum', 'emails': ['guido@python.org']}" + ] + }, + "execution_count": 115, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "guido" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we mutate `guido` and, 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": 116, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "guido[\"emails\"].clear()" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Guido van Rossum', 'emails': []}" + ] + }, + "execution_count": 117, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "guido" + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "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": [ + "## `dict` Comprehensions" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogous to `list` comprehensions in [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/01_content.ipynb#list-Comprehensions), `dict` comprehensions are a concise literal notation to derive new `dict` objects out of existing ones.\n", + "\n", + "For example, let's derive `from_words` out of `to_words` below by swapping the keys and values." + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "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": 120, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 120, + "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 curly braces `{}` replace the brackets `[]` and a colon `:` is added to separate the keys from the values." + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'zero': 0, 'one': 1, 'two': 2}" + ] + }, + "execution_count": 121, + "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": 122, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'ONE': 1, 'TWO': 2}" + ] + }, + "execution_count": 122, + "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`)." + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "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": 123, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{\n", + " (x, y): x * y\n", + " for x in range(1, 11) for y in range(1, 11)\n", + " if abs(x * y - 50) <= 5\n", + "}" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/09_mappings/01_exercises_nested-data.ipynb b/09_mappings/01_exercises_nested-data.ipynb new file mode 100644 index 0000000..fe7ae76 --- /dev/null +++ b/09_mappings/01_exercises_nested-data.ipynb @@ -0,0 +1,428 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 9: Mappings & Sets (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb) of Chapter 9.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Working with Nested Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's write some code to analyze the historic soccer game [Brazil vs. Germany ](https://en.wikipedia.org/wiki/Brazil_v_Germany_%282014_FIFA_World_Cup%29) during the 2014 World Cup.\n", + "\n", + "Below, `players` consists of two nested `dict` objects, one for each team, that hold `tuple` objects (i.e., records) with information on the players. Besides the jersey number, name, and position, each `tuple` objects contains a `list` object with the times when the player scored." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "players = {\n", + " \"Brazil\": [\n", + " (12, \"Júlio César\", \"Goalkeeper\", []),\n", + " (4, \"David Luiz\", \"Defender\", []),\n", + " (6, \"Marcelo\", \"Defender\", []),\n", + " (13, \"Dante\", \"Defender\", []),\n", + " (23, \"Maicon\", \"Defender\", []),\n", + " (5, \"Fernandinho\", \"Midfielder\", []),\n", + " (7, \"Hulk\", \"Midfielder\", []),\n", + " (8, \"Paulinho\", \"Midfielder\", []),\n", + " (11, \"Oscar\", \"Midfielder\", [90]),\n", + " (16, \"Ramires\", \"Midfielder\", []),\n", + " (17, \"Luiz Gustavo\", \"Midfielder\", []),\n", + " (19, \"Willian\", \"Midfielder\", []),\n", + " (9, \"Fred\", \"Striker\", []),\n", + " ],\n", + " \"Germany\": [\n", + " (1, \"Manuel Neuer\", \"Goalkeeper\", []),\n", + " (4, \"Benedikt Höwedes\", \"Defender\", []),\n", + " (5, \"Mats Hummels\", \"Defender\", []),\n", + " (16, \"Philipp Lahm\", \"Defender\", []),\n", + " (17, \"Per Mertesacker\", \"Defender\", []),\n", + " (20, \"Jérôme Boateng\", \"Defender\", []),\n", + " (6, \"Sami Khedira\", \"Midfielder\", [29]),\n", + " (7, \"Bastian Schweinsteiger\", \"Midfielder\", []),\n", + " (8, \"Mesut Özil\", \"Midfielder\", []),\n", + " (13, \"Thomas Müller\", \"Midfielder\", [11]),\n", + " (14, \"Julian Draxler\", \"Midfielder\", []),\n", + " (18, \"Toni Kroos\", \"Midfielder\", [24, 26]),\n", + " (9, \"André Schürrle\", \"Striker\", [69, 79]),\n", + " (11, \"Miroslav Klose\", \"Striker\", [23]),\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: Write a dictionary comprehension to derive a new `dict` object, called `brazilian_players`, that maps a Brazilian player's name to his position!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brazilian_players = {...: ...}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brazilian_players" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Generalize the code fragment into a `get_players()` function: Passed a `team` name, it returns a `dict` object like `brazilian_players`. Verify that the function works for the German team as well!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_players(team):\n", + " \"\"\"Creates a dictionary mapping the players' names to their position.\"\"\"\n", + " return {...: ...}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "get_players(\"Germany\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Often, we are given a `dict` object like the one returned from `get_players()`: Its main characteristic is that it maps a large set of unique keys (i.e., the players' names) onto a smaller set of non-unique values (i.e., the positions).\n", + "\n", + "**Q3**: Create a generic `invert()` function that swaps the keys and values of a `mapping` argument passed to it and returns them in a *new* `dict` object! Ensure that *no* key gets lost! Verify your implementation with the `brazilian_players` dictionary!\n", + "\n", + "Hints: Think of this as a grouping operation. The *new* values are `list` or `tuple` objects that hold the original keys. You may want to use either the [defaultdict ](https://docs.python.org/3/library/collections.html#collections.defaultdict) type from the [collections ](https://docs.python.org/3/library/collections.html) module in the [standard library ](https://docs.python.org/3/library/index.html) or the [.setdefault() ](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method on the ordinary `dict` type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def invert(mapping):\n", + " \"\"\"Invert the keys and values of a mapping argument.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "invert(brazilian_players)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Write a `score_at_minute()` function: It takes two arguments, `team` and `minute`, and returns the number of goals the `team` has scored up until this time in the game.\n", + "\n", + "Hints: The function may reference the global `players` for simplicity. Earn bonus points if you can write this in a one-line expression using some *reduction* function and a `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def score_at_minute(team, minute):\n", + " \"\"\"Determine the number of goals scored by a team until a given minute.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The score at half time was:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "score_at_minute(\"Brazil\", 45)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "score_at_minute(\"Germany\", 45)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The final score was:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "score_at_minute(\"Brazil\", 90)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "score_at_minute(\"Germany\", 90)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Write a `goals_by_player()` function that takes an argument like the global `players`, and returns a `dict` object mapping the players to the number of goals they scored!\n", + "\n", + "Hints: Do *not* \"hard code\" the names of the teams! Earn bonus points if you can solve it in a one-line `dict` comprehension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def goals_by_player(players):\n", + " \"\"\"Create a dictionary mapping the players' names to the number of goals.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "goals_by_player(players)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Write a `dict` comprehension to filter out the players who did *not* score from the preceding result.\n", + "\n", + "Hints: Reference the `goals_by_player()` function from before." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{...: ...}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Write a `all_goals()` function that takes one argument like the global `players` and returns a `list` object containing $2$-element `tuple` objects where the first element is the minute a player scored and the second his name! The list should be sorted by the time.\n", + "\n", + "Hints: You may want to use either the built-in [sorted() ](https://docs.python.org/3/library/functions.html#sorted) function or the `list` type's [.sort() ](https://docs.python.org/3/library/stdtypes.html#list.sort) method. Earn bonus points if you can write a one-line expression with a `generator` expression." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def all_goals(players):\n", + " \"\"\"Create a time table of the individual goals.\"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " return ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "all_goals(players)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: Lastly, write a `summary()` function that takes one argument like the global `players` and prints out a concise report of the goals, the score at the half, and the final result.\n", + "\n", + "Hints: Use the `all_goals()` and `score_at_minute()` functions from before.\n", + "\n", + "The output should look similar to this:\n", + "```\n", + "12' Gerd Müller scores\n", + "...\n", + "HALFTIME: TeamA 1 TeamB 2\n", + "77' Ronaldo scores\n", + "...\n", + "FINAL: TeamA 1 TeamB 3\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def summary(players):\n", + " \"\"\"Create a written summary of the game.\"\"\"\n", + " # Create two lists with the goals of either half.\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " # Print the goals of the first half.\n", + " ...\n", + " ...\n", + "\n", + " # Print the half time score.\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " # Print the goals of the second half.\n", + " ...\n", + " ...\n", + "\n", + " # Print the final score.\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "summary(players)" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/09_mappings/02_content.ipynb b/09_mappings/02_content.ipynb new file mode 100644 index 0000000..b2fe6a0 --- /dev/null +++ b/09_mappings/02_content.ipynb @@ -0,0 +1,1236 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/02_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 9: Mappings & Sets (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After introducing the `dict` type in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb) of this chapter, we first look at an extension of the packing and unpacking syntax that involves `dict` objects. Then, we see how mappings can help us write computationally more efficient implementations to recursive solutions of problems as introduced in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb#Recursion). In a way, this second part of the chapter \"finishes\" Chapter 4." + ] + }, + { + "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/main/07_sequences/03_content.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": 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": "fragment" + } + }, + "outputs": [], + "source": [ + "more_words = {\n", + " 2: \"TWO\", # 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": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0: 'zero', 1: 'one', 2: 'TWO', 3: 'three', 4: 'four'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{**to_words, **more_words}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "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": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def print_args1(*args, **kwargs):\n", + " \"\"\"Print out all arguments passed in.\"\"\"\n", + " for index, arg in enumerate(args):\n", + " print(\"position\", index, arg)\n", + "\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": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "print_args1()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "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": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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": [ + "We may even unpack `dict` and `list` objects." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "flags = {\"flag\": True, \"another_flag\": False}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "keyword flag True\n", + "keyword another_flag False\n" + ] + } + ], + "source": [ + "print_args1(**flags)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "position 0 42\n", + "position 1 87\n", + "keyword flag True\n", + "keyword another_flag False\n" + ] + } + ], + "source": [ + "print_args1(*[42, 87], **flags)" + ] + }, + { + "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 `positional` parameter, and one keyword argument, captured in `keyword`. Further, an optional keyword argument `default` may be passed in. Any other positional or keyword arguments are packed into either `args` or `kwargs`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def print_args2(positional, *args, keyword, default=True, **kwargs):\n", + " \"\"\"Print out all arguments passed in.\"\"\"\n", + " print(\"required positional\", positional)\n", + "\n", + " for index, arg in enumerate(args):\n", + " print(\"optional positional\", index, arg)\n", + "\n", + " print(\"required keyword\", keyword)\n", + " print(\"default keyword\", default)\n", + "\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": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "print_args2() missing 1 required positional argument: 'positional'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mprint_args2\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: print_args2() missing 1 required positional argument: 'positional'" + ] + } + ], + "source": [ + "print_args2()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "print_args2() missing 1 required keyword-only argument: 'keyword'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[14], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mprint_args2\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mp\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: print_args2() missing 1 required keyword-only argument: 'keyword'" + ] + } + ], + "source": [ + "print_args2(\"p\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "required positional p\n", + "required keyword k\n", + "default keyword True\n" + ] + } + ], + "source": [ + "print_args2(\"p\", keyword=\"k\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "required positional p\n", + "required keyword k\n", + "default keyword False\n" + ] + } + ], + "source": [ + "print_args2(\"p\", keyword=\"k\", default=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "required positional p\n", + "optional positional 0 x\n", + "optional positional 1 y\n", + "required keyword k\n", + "default keyword True\n", + "optional keyword flag True\n" + ] + } + ], + "source": [ + "print_args2(\"p\", \"x\", \"y\", keyword=\"k\", flag=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "required positional p\n", + "optional positional 0 x\n", + "optional positional 1 y\n", + "required keyword k\n", + "default keyword False\n", + "optional keyword flag True\n" + ] + } + ], + "source": [ + "print_args2(\"p\", \"x\", \"y\", keyword=\"k\", default=False, flag=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As above, we may unpack `list` or `dict` objects in a function call." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "positionals = [\"x\", \"y\", \"z\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "required positional p\n", + "optional positional 0 x\n", + "optional positional 1 y\n", + "optional positional 2 z\n", + "required keyword k\n", + "default keyword False\n", + "optional keyword flag True\n", + "optional keyword another_flag False\n" + ] + } + ], + "source": [ + "print_args2(\"p\", *positionals, keyword=\"k\", default=False, **flags)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Memoization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### \"Easy at first Glance\" Example: [Fibonacci Numbers ](https://en.wikipedia.org/wiki/Fibonacci_number) (repeated)" + ] + }, + { + "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/main/04_iteration/00_content.ipynb#\"Easy-at-first-Glance\"-Example:-Fibonacci-Numbers) takes long to compute for large Fibonacci numbers. For easier comparison, we show the old `fibonacci()` version here again." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "def fibonacci(i):\n", + " \"\"\"Calculate the ith Fibonacci number.\n", + "\n", + " Args:\n", + " i (int): index of the Fibonacci number to calculate\n", + "\n", + " Returns:\n", + " ith_fibonacci (int)\n", + " \"\"\"\n", + " if i == 0:\n", + " return 0\n", + " elif i == 1:\n", + " return 1\n", + " return fibonacci(i - 1) + fibonacci(i - 2)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "144" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "#### Efficiency of Algorithms" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Timing the code cells below with the `%%timeit` magic shows how doubling the input (i.e., `12` becomes `24`) more than doubles how long it takes `fibonacci()` to calculate the solution. This is actually an understatement as we see the time go up by roughly a factor of $1000$ (i.e., from nano-seconds to milli-seconds). That is an example of **exponential growth**." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.1 µs ± 1.26 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%%timeit -n 100\n", + "fibonacci(12)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12.1 ms ± 149 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n" + ] + } + ], + "source": [ + "%%timeit -n 100\n", + "fibonacci(24)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The computation graph below visualizes what the problem is and also suggests a solution: In the recursive implementation, the same function calls are made over and over again. For example, in the visualization the call `fibonacci(3)`, shown as $F(3)$, is made *twice* when the actual goal is to calculate `fibonacci(5)`, shown as $F(5)$. This problem \"grows\" if the initial argument (i.e., `5` in the example) is chosen to be larger as we see with the many `fibonacci(2)`, `fibonacci(1)` and `fibonacci(0)` calls.\n", + "\n", + "Instead of calculating the return value of the `fibonacci()` function for the *same* argument over and over again, it makes sense to **cache** (i.e., \"store\") the result and reuse it. This concept is called **[memoization ](https://en.wikipedia.org/wiki/Memoization)**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "" + ] + }, + { + "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": [ + "Below is a revision of the recursive `fibonacci()` implementation that uses a **globally** defined `dict` object, called `memo`, to store intermediate results and look them up.\n", + "\n", + "To be precise, the the revised `fibonacci()` first checks if the `i`th Fibonacci number has already been calculated before. If yes, it is in the `memo`. That number is then returned immediately *without* any more calculations. As `dict` objects are *optimized* for constant-time key look-ups, this takes essentially \"no\" time! With a `list` object, for example, the `in` operator would trigger a linear search, which takes longer the more elements are in the list. If the `i`th Fibonacci number has not been calculated before, there is no corresponding item in the `memo` and a recursive function call must be made. The result obtained by recursion is then inserted into the `memo`." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "memo = {\n", + " 0: 0,\n", + " 1: 1,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "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", + " 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": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "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)`, the left-most leaf node $F(1)$, 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. Effectively, this mirrors the *iterative* implementation in that the order of all computational steps are *identical* (cf., the \"*Hard at first Glance*\" example in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#\"Hard-at-first-Glance\"-Example:-Fibonacci-Numbers--(revisited))).\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": 27, + "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": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12, debug=True)" + ] + }, + { + "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": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "144" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(12, debug=True) # no more recursive calls needed" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "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": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "memo" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "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": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The slowest run took 252.65 times longer than the fastest. This could mean that an intermediate result is being cached.\n", + "6.68 µs ± 15.8 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1\n", + "fibonacci(99)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The slowest run took 3603.20 times longer than the fastest. This could mean that an intermediate result is being cached.\n", + "85.1 µs ± 208 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1\n", + "fibonacci(999)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The iterative implementation still has an advantage as the `RecursionError` shows for larger `i`." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "RecursionError", + "evalue": "maximum recursion depth exceeded", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[32], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mget_ipython\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_cell_magic\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtimeit\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m-n 1\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mfibonacci(9999)\u001b[39;49m\u001b[38;5;130;43;01m\\n\u001b[39;49;00m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/Repositories/intro-to-python/.venv/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541\u001b[0m, in \u001b[0;36mInteractiveShell.run_cell_magic\u001b[0;34m(self, magic_name, line, cell)\u001b[0m\n\u001b[1;32m 2539\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mbuiltin_trap:\n\u001b[1;32m 2540\u001b[0m args \u001b[38;5;241m=\u001b[39m (magic_arg_s, cell)\n\u001b[0;32m-> 2541\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 2543\u001b[0m \u001b[38;5;66;03m# The code below prevents the output from being displayed\u001b[39;00m\n\u001b[1;32m 2544\u001b[0m \u001b[38;5;66;03m# when using magics with decorator @output_can_be_silenced\u001b[39;00m\n\u001b[1;32m 2545\u001b[0m \u001b[38;5;66;03m# when the last Python token in the expression is a ';'.\u001b[39;00m\n\u001b[1;32m 2546\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(fn, magic\u001b[38;5;241m.\u001b[39mMAGIC_OUTPUT_CAN_BE_SILENCED, \u001b[38;5;28;01mFalse\u001b[39;00m):\n", + "File \u001b[0;32m~/Repositories/intro-to-python/.venv/lib/python3.12/site-packages/IPython/core/magics/execution.py:1189\u001b[0m, in \u001b[0;36mExecutionMagics.timeit\u001b[0;34m(self, line, cell, local_ns)\u001b[0m\n\u001b[1;32m 1186\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m time_number \u001b[38;5;241m>\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0.2\u001b[39m:\n\u001b[1;32m 1187\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[0;32m-> 1189\u001b[0m all_runs \u001b[38;5;241m=\u001b[39m \u001b[43mtimer\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrepeat\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrepeat\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnumber\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1190\u001b[0m best \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mmin\u001b[39m(all_runs) \u001b[38;5;241m/\u001b[39m number\n\u001b[1;32m 1191\u001b[0m worst \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mmax\u001b[39m(all_runs) \u001b[38;5;241m/\u001b[39m number\n", + "File \u001b[0;32m/usr/lib64/python3.12/timeit.py:208\u001b[0m, in \u001b[0;36mTimer.repeat\u001b[0;34m(self, repeat, number)\u001b[0m\n\u001b[1;32m 206\u001b[0m r \u001b[38;5;241m=\u001b[39m []\n\u001b[1;32m 207\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(repeat):\n\u001b[0;32m--> 208\u001b[0m t \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtimeit\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnumber\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 209\u001b[0m r\u001b[38;5;241m.\u001b[39mappend(t)\n\u001b[1;32m 210\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m r\n", + "File \u001b[0;32m~/Repositories/intro-to-python/.venv/lib/python3.12/site-packages/IPython/core/magics/execution.py:173\u001b[0m, in \u001b[0;36mTimer.timeit\u001b[0;34m(self, number)\u001b[0m\n\u001b[1;32m 171\u001b[0m gc\u001b[38;5;241m.\u001b[39mdisable()\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 173\u001b[0m timing \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minner\u001b[49m\u001b[43m(\u001b[49m\u001b[43mit\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtimer\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 174\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[1;32m 175\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m gcold:\n", + "File \u001b[0;32m:1\u001b[0m, in \u001b[0;36minner\u001b[0;34m(_it, _timer)\u001b[0m\n", + "Cell \u001b[0;32mIn[26], line 17\u001b[0m, in \u001b[0;36mfibonacci\u001b[0;34m(i, debug)\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m debug: \u001b[38;5;66;03m# added for didactical purposes\u001b[39;00m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfibonacci(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mi\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m) is calculated\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 17\u001b[0m recurse \u001b[38;5;241m=\u001b[39m \u001b[43mfibonacci\u001b[49m\u001b[43m(\u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdebug\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdebug\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;241m+\u001b[39m fibonacci(i \u001b[38;5;241m-\u001b[39m \u001b[38;5;241m2\u001b[39m, debug\u001b[38;5;241m=\u001b[39mdebug)\n\u001b[1;32m 18\u001b[0m memo[i] \u001b[38;5;241m=\u001b[39m recurse\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m recurse\n", + "Cell \u001b[0;32mIn[26], line 17\u001b[0m, in \u001b[0;36mfibonacci\u001b[0;34m(i, debug)\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m debug: \u001b[38;5;66;03m# added for didactical purposes\u001b[39;00m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfibonacci(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mi\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m) is calculated\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 17\u001b[0m recurse \u001b[38;5;241m=\u001b[39m \u001b[43mfibonacci\u001b[49m\u001b[43m(\u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdebug\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdebug\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;241m+\u001b[39m fibonacci(i \u001b[38;5;241m-\u001b[39m \u001b[38;5;241m2\u001b[39m, debug\u001b[38;5;241m=\u001b[39mdebug)\n\u001b[1;32m 18\u001b[0m memo[i] \u001b[38;5;241m=\u001b[39m recurse\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m recurse\n", + " \u001b[0;31m[... skipping similar frames: fibonacci at line 17 (2969 times)]\u001b[0m\n", + "Cell \u001b[0;32mIn[26], line 17\u001b[0m, in \u001b[0;36mfibonacci\u001b[0;34m(i, debug)\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m debug: \u001b[38;5;66;03m# added for didactical purposes\u001b[39;00m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfibonacci(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mi\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m) is calculated\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 17\u001b[0m recurse \u001b[38;5;241m=\u001b[39m \u001b[43mfibonacci\u001b[49m\u001b[43m(\u001b[49m\u001b[43mi\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdebug\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdebug\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;241m+\u001b[39m fibonacci(i \u001b[38;5;241m-\u001b[39m \u001b[38;5;241m2\u001b[39m, debug\u001b[38;5;241m=\u001b[39mdebug)\n\u001b[1;32m 18\u001b[0m memo[i] \u001b[38;5;241m=\u001b[39m recurse\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m recurse\n", + "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded" + ] + } + ], + "source": [ + "%%timeit -n 1\n", + "fibonacci(9999)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "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 as the programmer.\n", + "\n", + "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. As we are good citizens, we reset everything to the defaults after our hack is completed." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import sys" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "old_recursion_limit = sys.getrecursionlimit()" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3000" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "old_recursion_limit" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "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": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The slowest run took 50532.39 times longer than the fastest. This could mean that an intermediate result is being cached.\n", + "1.21 ms ± 2.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" + ] + } + ], + "source": [ + "%%timeit -n 1\n", + "fibonacci(9999)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "sys.setrecursionlimit(old_recursion_limit)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/09_mappings/03_exercises_fibonacci.ipynb b/09_mappings/03_exercises_fibonacci.ipynb new file mode 100644 index 0000000..c9738fe --- /dev/null +++ b/09_mappings/03_exercises_fibonacci.ipynb @@ -0,0 +1,248 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/03_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 9: Mappings & Sets (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [second part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb) of Chapter 9.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Memoization without Side Effects" + ] + }, + { + "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, in the \"*Easy at second Glance: Fibonacci Numbers*\" section in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb#\"Easy-at-second-Glance\"-Example:-Fibonacci-Numbers--%28revisited%29), we use a global `memo` to store the Fibonacci numbers that have already been calculated.\n", + "\n", + "That `memo` dictionary could be \"manipulated.\" More often than not, such things happen by accident: Imagine we wrote two independent recursive functions that both rely on memoization 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 remedy is to avoid global state and pass intermediate results \"down\" the recursion tree in a \"hidden\" argument. By convention, we prefix parameter names with a single leading underscore `_`, such as with `_memo` below, to indicate that the caller of our `fibonacci()` function *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` parameter is *not* mentioned in the docstring.\n", + "\n", + "Your task is to complete this version of `fibonacci()` so that the function works *without* any **side effects** in the global scope." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### \"Easy at third Glance\" Example: [Fibonacci Numbers ](https://en.wikipedia.org/wiki/Fibonacci_number)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "skip" + } + }, + "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", + " # answer to Q1\n", + " if ...:\n", + " ... = {\n", + " 0: 0,\n", + " 1: 1,\n", + " }\n", + "\n", + " # answer to Q2\n", + " if ...:\n", + " return ...\n", + "\n", + " if debug: # added for didactical purposes\n", + " print(f\"fibonacci({i}) is calculated\")\n", + "\n", + " # answer to Q3\n", + " recurse = (\n", + " fibonacci(...)\n", + " + fibonacci(...)\n", + " )\n", + " # answer to Q4\n", + " ... = ...\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Q1**: When `fibonacci()` is initially called, `_memo` is set to `None`. So, there is *no* `dict` object yet. Implement the *two* base cases in the first `if` statement!\n", + "\n", + "Hints: All you need to do is create a *new* `dict` object with the results for `i=0` and `i=1`. This object is then passed on in the recursive function calls. Use the `is` operator in the `if` statement." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: When `fibonacci()` is called for non-base cases (i.e., `i > 1`), it first checks if the result is already in the `_memo`. Implement that step in the second `if` statement!\n", + "\n", + "Hint: Use the early exit pattern." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3**: If `fibonacci()` is called for an `i` argument whose result is not yet in the `_memo`, it must calculate it with the usual recursive function calls. Fill in the arguments to the two recursive `fibonacci()` calls!\n", + "\n", + "Hint: You must pass on the hidden `_memo`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Lastly, after the two recursive calls have returned, `fibonacci()` must store the `recurse` result for the given `i` in the `_memo` *before* returning it. Implement that logic!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: What happens to the hidden `_memo` after the initial call to `fibonacci()` returned? How many hidden `_memo` objects exist in memory during the entire computation?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because `fibonacci()` is now independent of the *global state*, the same eleven recursive function calls are made each time! So, this `fibonacci()` is a **pure** function, meaning it has *no* side effects.\n", + "\n", + "**Q6**: Execute the following code cell a couple of times to observe that!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "fibonacci(12, debug=True) # = 13th Fibonacci number -> 11 recursive calls necessary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The runtime of `fibonacci()` is now stable: There is no message that \"an intermediate result is being cached\" as in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb#\"Easy-at-second-Glance\"-Example:-Fibonacci-Numbers--%28revisited%29).\n", + "\n", + "**Q7**: Execute the following code cells a couple of times to observe that!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit -n 1\n", + "fibonacci(99)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%timeit -n 1\n", + "fibonacci(999)" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/09_mappings/04_content.ipynb b/09_mappings/04_content.ipynb new file mode 100644 index 0000000..3f1f1d5 --- /dev/null +++ b/09_mappings/04_content.ipynb @@ -0,0 +1,1605 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/04_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 9: Mappings & Sets (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this part of the chapter, we look at the `set` type, a concrete example of the more abstract concept of *sets* as a specialization of collections." + ] + }, + { + "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 members. The redundant `7`s and `4`s below are discarded." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers = {7, 7, 7, 7, 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4, 4, 4, 4, 4}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`set` objects are objects on their own." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139739115782880" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "set" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 4, + "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 built-in [set() ](https://docs.python.org/3/library/functions.html#func-set) constructor as empty curly brackets, `{}`, already create an empty `dict` object." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "empty_dict = {}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "dict" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(empty_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "empty_set = set()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "set()" + ] + }, + "execution_count": 8, + "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) constructor 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": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'a', 'b', 'c', 'd', 'r'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "set(\"abracadabra\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sets are like Mappings 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) object and can only be contained in a set once due to the buckets logic." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "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)", + "Cell \u001b[0;32mIn[10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m {\u001b[38;5;241m0\u001b[39m, [\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m], \u001b[38;5;241m3\u001b[39m}\n", + "\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." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "12" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(numbers)" + ] + }, + { + "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": 12, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 2 3 4 5 6 7 8 9 10 11 12 " + ] + } + ], + "source": [ + "for number in numbers: # beware the non-order!\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We confirm that `set` objects are not `Reversible`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "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)", + "Cell \u001b[0;32mIn[13], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m number \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28;43mreversed\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mnumbers\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(number, end\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m \u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mTypeError\u001b[0m: 'set' object is not reversible" + ] + } + ], + "source": [ + "for number in reversed(numbers):\n", + " print(number, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The boolean `in` operator checks if a given and immutable object compares 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 without doing it." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "0 in numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 in numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2.0 in numbers # 2.0 is not a member itself but compares equal to a member" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There is are `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 (cf., the \"*Set Operations*\" section below). Further, the `MutableSet` ABC standardizes all the methods that mutate a `set` object in place (cf., the \"*Mutability & Set Methods*\" section below)." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import collections.abc as abc" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.Set)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(numbers, abc.MutableSet)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## No Indexing, Key Look-up, or Slicing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As `set` objects come without a *predictable* order, indexing and slicing are not supported and result 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": 20, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "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)", + "Cell \u001b[0;32mIn[21], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnumbers\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: 'set' object is not subscriptable" + ] + } + ], + "source": [ + "numbers[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "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)", + "Cell \u001b[0;32mIn[22], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnumbers\u001b[49m\u001b[43m[\u001b[49m\u001b[43m:\u001b[49m\u001b[43m]\u001b[49m\n", + "\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 mainly 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": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers.add(999)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 999}" + ] + }, + "execution_count": 24, + "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": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "numbers.update(range(5))" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 999}" + ] + }, + "execution_count": 26, + "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": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "numbers.remove(999)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "KeyError", + "evalue": "999", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[28], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mnumbers\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mremove\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m999\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mKeyError\u001b[0m: 999" + ] + } + ], + "source": [ + "numbers.remove(999)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "numbers.discard(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers.discard(0)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 31, + "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." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers.pop()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 33, + "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": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "numbers.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "set()" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "139739115782880" + ] + }, + "execution_count": 36, + "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 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": 37, + "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": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0}" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{2, 4, 6, 8, 10, 12}" + ] + }, + "execution_count": 40, + "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": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero | numbers # zero.union(numbers)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, the operators may be *chained*." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero | numbers | evens # zero.union(numbers).union(evens)" + ] + }, + { + "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": 43, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "set()" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "zero & numbers # zero.intersection(numbers)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{2, 4, 6, 8, 10, 12}" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers & evens # numbers.intersection(evens)" + ] + }, + { + "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": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 3, 5, 7, 9, 11}" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers - evens # numbers.difference(evens)" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "set()" + ] + }, + "execution_count": 46, + "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": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 3, 5, 7, 9, 11}" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "numbers ^ evens # numbers.symmetric_difference(evens)" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 3, 5, 7, 9, 11}" + ] + }, + "execution_count": 48, + "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 that works exactly like the one for `dict` comprehensions described in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb#dict-Comprehensions) of this chapter 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": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144}" + ] + }, + "execution_count": 49, + "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": 50, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{4, 16, 36, 64, 100, 144}" + ] + }, + "execution_count": 50, + "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": 51, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{81, 84, 88, 90, 96, 99, 100, 108, 110, 120, 121, 132, 144}" + ] + }, + "execution_count": 51, + "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 built-in [frozenset() ](https://docs.python.org/3/library/functions.html#func-frozenset) constructor. Even though `frozenset` objects are hashable, their elements are *not* ordered." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "frozenset({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "frozenset([7, 7, 7, 7, 7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4, 4, 4, 4, 4])" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/09_mappings/05_appendix.ipynb b/09_mappings/05_appendix.ipynb new file mode 100644 index 0000000..0b774af --- /dev/null +++ b/09_mappings/05_appendix.ipynb @@ -0,0 +1,970 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/05_appendix.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 9: Mappings & Sets (Appendix)" + ] + }, + { + "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 use cases." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## The `defaultdict` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The [defaultdict ](https://docs.python.org/3/library/collections.html#collections.defaultdict) type 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": 1, + "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": 2, + "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": 2, + "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": [ + "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": 3, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "from collections import defaultdict" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "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": 4, + "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": 5, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "collections.defaultdict" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(goals_by_player)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A reference to the factory function is stored in the `default_factory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "list" + ] + }, + "execution_count": 6, + "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) constructor." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "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": 7, + "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, created with a `lambda` expression, 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": 8, + "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": 8, + "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": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Germany': {'Müller': [11],\n", + " 'Klose': [23],\n", + " 'Kroos': [24, 26],\n", + " 'Khedira': [29],\n", + " 'Schürrle': [69, 79]},\n", + " 'Brazil': {'Oscar': [90]}}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{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": 10, + "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": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "goals" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "from collections import Counter" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "scorers = Counter(x[1] for x in goals)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({'Kroos': 2,\n", + " 'Schürrle': 2,\n", + " 'Müller': 1,\n", + " 'Klose': 1,\n", + " 'Khedira': 1,\n", + " 'Oscar': 1})" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scorers" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "collections.Counter" + ] + }, + "execution_count": 14, + "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": 15, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 15, + "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": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 16, + "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": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[('Kroos', 2), ('Schürrle', 2)]" + ] + }, + "execution_count": 17, + "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": 18, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "scorers.update([\"Lahm\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({'Kroos': 2,\n", + " 'Schürrle': 2,\n", + " 'Müller': 1,\n", + " 'Klose': 1,\n", + " 'Khedira': 1,\n", + " 'Oscar': 1,\n", + " 'Lahm': 1})" + ] + }, + "execution_count": 19, + "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": 20, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "scorers.update(\"Lahm\")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({'Kroos': 2,\n", + " 'Schürrle': 2,\n", + " 'Müller': 1,\n", + " 'Klose': 1,\n", + " 'Khedira': 1,\n", + " 'Oscar': 1,\n", + " 'Lahm': 1,\n", + " 'L': 1,\n", + " 'a': 1,\n", + " 'h': 1,\n", + " 'm': 1})" + ] + }, + "execution_count": 21, + "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": 22, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "to_words = {\n", + " 0: \"zero\",\n", + " 1: \"one\",\n", + " 2: \"two\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "more_words = {\n", + " 2: \"TWO\", # to illustrate a point\n", + " 3: \"three\",\n", + " 4: \"four\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "even_more_words = {\n", + " 4: \"FOUR\", # 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": 25, + "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": 26, + "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": 27, + "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": 28, + "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)", + "Cell \u001b[0;32mIn[28], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mchain\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m10\u001b[39;49m\u001b[43m]\u001b[49m\n", + "File \u001b[0;32m/usr/lib64/python3.12/collections/__init__.py:1014\u001b[0m, in \u001b[0;36mChainMap.__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 1012\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n\u001b[1;32m 1013\u001b[0m \u001b[38;5;28;01mpass\u001b[39;00m\n\u001b[0;32m-> 1014\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__missing__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/usr/lib64/python3.12/collections/__init__.py:1006\u001b[0m, in \u001b[0;36mChainMap.__missing__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 1005\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__missing__\u001b[39m(\u001b[38;5;28mself\u001b[39m, key):\n\u001b[0;32m-> 1006\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m(key)\n", + "\u001b[0;31mKeyError\u001b[0m: 10" + ] + } + ], + "source": [ + "chain[10]" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/09_mappings/06_summary.ipynb b/09_mappings/06_summary.ipynb new file mode 100644 index 0000000..ef620b7 --- /dev/null +++ b/09_mappings/06_summary.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 9: Mappings & Sets (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." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/09_mappings/07_review.ipynb b/09_mappings/07_review.ipynb new file mode 100644 index 0000000..7aed88f --- /dev/null +++ b/09_mappings/07_review.ipynb @@ -0,0 +1,247 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 9: Mappings & Sets (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb), [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb), and [third ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/04_content.ipynb) part of Chapter 9.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: `dict` objects are well-suited **to model** discrete mathematical **functions** and to approximate continuous ones. What property of dictionaries is the basis for that claim, and how does it relate to functions in the mathematical sense?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: Explain why **hash tables** are a **trade-off** between **computational speed** and **memory** usage!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3:** The `dict` type is an **iterable** that **contains** a **finite** number of key-value pairs. Despite that, why is it *not* considered a **sequence**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Whereas *key* **look-ups** in a `dict` object run in so-called **[constant time ](https://en.wikipedia.org/wiki/Time_complexity#Constant_time)** (i.e., *extremely* fast), that does not hold for *reverse* look-ups. Why is that?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Why is it conceptually correct that the Python core developers do not implement **slicing** with the `[]` operator for `dict` objects?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: **Memoization** is an essential concept to know to solve problems in the real world. Together with the idea of **recursion**, it enables us to solve problems in a \"backwards\" fashion *effectively*.\n", + "\n", + "Compare the **recursive** formulation of `fibonacci()` in [Chapter 9 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb#\"Easy-at-second-Glance\"-Example:-Fibonacci-Numbers--(revisited)), the \"*Easy at second Glance*\" example, with the **iterative** version in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb#\"Hard-at-first-Glance\"-Example:-Fibonacci-Numbers-%28revisited%29), the \"*Hard at first Glance*\" example!\n", + "\n", + "How are they similar and how do they differ? Also consider how the flow of execution behaves when the functions are being executed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: How are the `set` and the `dict` types related? How could we use the latter to mimic the former?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: We may *not* put `dict` objects inside other `dict` objects because they are **mutable**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: **Mutable** objects (e.g., `list`) may generally *not* be used as keys in a `dict` object. However, if we collect, for example, `list` objects in a `tuple` object, the composite object becomes **hashable**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: **Mutability** of a `dict` object works until the underlying hash table becomes too crowded. Then, we cannot insert any items any more making the `dict` object effectively **immutable**. Luckily, that almost never happens in practice." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: A `dict` object's [.update() ](https://docs.python.org/3/library/stdtypes.html#dict.update) method only inserts key-value pairs whose key is *not* yet in the `dict` object. So, it does *not* overwrite anything." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: The `set` type is both a mapping and a sequence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/09_mappings/08_resources.ipynb b/09_mappings/08_resources.ipynb new file mode 100644 index 0000000..b9dd3da --- /dev/null +++ b/09_mappings/08_resources.ipynb @@ -0,0 +1,231 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/08_resources.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 9: Mappings & Sets (Further Resources)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Here, we list some conference talks that go into great detail regarding the workings of the `dict` type." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## How `dict` objects work" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[Brandon Rhodes](https://github.com/brandon-rhodes) explains in great detail in his PyCon 2010 & 2017 talks how dictionaries work \"under the hood.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "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\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"C4Kc8xzcA68\", width=\"60%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"66P5FMkWoVU\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## History of the `dict` Type" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `dict` type's order has been worked on with 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\n", + "\n", + "[Raymond Hettinger](https://github.com/rhettinger), a Python core developer and one of the greatest Python teachers in the world, summaries the history of the `dict` type in his PyCon 2017 talk." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "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": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "YouTubeVideo(\"npw4s1QTmPg\", width=\"60%\")" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/09_mappings/static/fibonacci_call_graph.png b/09_mappings/static/fibonacci_call_graph.png new file mode 100644 index 0000000..64cba5d Binary files /dev/null and b/09_mappings/static/fibonacci_call_graph.png differ diff --git a/10_arrays/TODO.md b/10_arrays/TODO.md new file mode 100644 index 0000000..9902b8f --- /dev/null +++ b/10_arrays/TODO.md @@ -0,0 +1 @@ +Here, a new section on `numpy` Arrays and, maybe, `pandas` DataFrames will be added. \ No newline at end of file diff --git a/11_classes/00_content.ipynb b/11_classes/00_content.ipynb new file mode 100644 index 0000000..696eed2 --- /dev/null +++ b/11_classes/00_content.ipynb @@ -0,0 +1,2147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/00_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 11: Classes & Instances" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In contrast to all the built-in data types introduced in the previous chapters, **classes** allow us to create **user-defined data types**. They enable us to model **data** and its **associated behavior** in an *abstract* way. *Concrete* **instances** of these custom data types then **encapsulate** the **state** in a running program. Often, classes are **blueprints** modeling \"real world things.\"\n", + "\n", + "Classes and instances follow the **[object-oriented programming ](https://en.wikipedia.org/wiki/Object-oriented_programming)** (OOP) paradigm where a *large program* is broken down into many *small components* (i.e., the objects) that *reuse* code. This way, a program that is too big for a programmer to fully comprehend as a whole becomes maintainable via its easier to understand individual pieces.\n", + "\n", + "Often, we see the terminology \"classes & objects\" used instead of \"classes & instances\" in Python related texts. In this book, we are more precise as *both* classes and instances are objects as specified already in the \"*Objects vs. Types vs. Values*\" section in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Objects-vs.-Types-vs.-Values)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Example: Vectors & Matrices" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Neither core Python nor the standard library offer an implementation of common [linear algebra ](https://en.wikipedia.org/wiki/Linear_algebra) functionalities. While we introduce the popular third-party library [numpy](http://www.numpy.org/) in [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/10_arrarys/00_content.ipynb) as the de-facto standard for that and recommend to use it in real-life projects, we show how one could use Python's object-oriented language features to implement common matrix and vector operations throughout this chapter. Once we have achieved that, we compare our own library with [numpy](http://www.numpy.org/).\n", + "\n", + "Without classes, we could model a vector, for example, with a `tuple` or a `list` object, depending on if we want it to be mutable or not.\n", + "\n", + "Let's take the following vector $\\vec{x}$ as an example and model it as a `tuple`:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$\\vec{x} = \\begin{pmatrix} 1 \\\\ 2 \\\\ 3 \\end{pmatrix}$" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "x = (1, 2, 3)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 2, 3)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We can extend this approach and model a matrix as either a `tuple` holding other `tuple`s or as a `list` holding other `list`s or as a mixture of both. Then, we *must* decide if the inner objects represent rows or columns. A common convention is to go with the former.\n", + "\n", + "For example, let's model the matrix $\\bf{A}$ below as a `list` of row `list`s:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$\\bf{A} = \\begin{bmatrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{bmatrix}$" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[[1, 2, 3], [4, 5, 6], [7, 8, 9]]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While this way of representing vectors and matrices in memory keeps things simple, we cannot work with them easily as Python does not know about the **semantics** (i.e., \"rules\") of vectors and matrices modeled as `tuple`s and `list`s of `list`s.\n", + "\n", + "For example, we should be able to multiply $\\bf{A}$ with $\\vec{x}$ if their dimensions match. However, Python does not know how to do this and raises a `TypeError`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$\\bf{A} * \\vec{x} = \\begin{bmatrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{bmatrix} * \\begin{pmatrix} 1 \\\\ 2 \\\\ 3 \\end{pmatrix} = \\begin{pmatrix} 14 \\\\ 32 \\\\ 50 \\end{pmatrix}$" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "can't multiply sequence by non-int of type 'tuple'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[5], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mA\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: can't multiply sequence by non-int of type 'tuple'" + ] + } + ], + "source": [ + "A * x" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Throughout this chapter, we \"teach\" Python the rules of [linear algebra ](https://en.wikipedia.org/wiki/Linear_algebra)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Class Definition" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The compound `class` statement creates a new variable that references a **class object** in memory.\n", + "\n", + "Following the *header* line, the indented *body* syntactically consists of function definitions (i.e., `.dummy_method()`) and variable assignments (i.e., `.dummy_variable`). Any code put here is executed just as if it were outside the `class` statement. However, the class object acts as a **namespace**, meaning that all the names do *not* exist in the global scope but may only be accessed with the dot operator `.` on the class object. In this context, the names are called **class attributes**.\n", + "\n", + "Within classes, functions are referred to as **methods** that are **bound** to *future* **instance objects**. This binding process means that Python *implicitly* inserts a reference to a *concrete* instance object as the first argument to any **method invocation** (i.e., \"function call\"). By convention, we call this parameter `self` as it references the instance object on which the method is invoked. Then, as the method is executed, we can set and access attributes via the dot operator `.` on `self`. That is how we manage the *state* of a *concrete* instance within a *generically* written class. At the same time, the code within a method is reused whenever we invoke a method on *any* instance.\n", + "\n", + "As indicated by [PEP 257 ](https://www.python.org/dev/peps/pep-0257/) and also section 3.8.4 of the [Google Python Style Guide ](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#384-classes), we use docstrings to document relevant parts of the new data type. With respect to naming, classes are named according to the [CamelCase ](https://en.wikipedia.org/wiki/Camel_case) convention while instances are treated like normal variables and named in [snake\\_case ](https://en.wikipedia.org/wiki/Snake_case)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + " \"\"\"A one-dimensional vector from linear algebra.\"\"\"\n", + "\n", + " dummy_variable = \"I am a vector\"\n", + "\n", + " def dummy_method(self):\n", + " \"\"\"A dummy method for illustration purposes.\"\"\"\n", + " return self.dummy_variable" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Vector` is an object on its own with an identity, a type, and a value." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94113690586816" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(Vector)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Its type is `type` indicating that it represents a user-defined data type and it evaluates to its fully qualified name (i.e., `__main__` as it is defined in this Jupyter notebook).\n", + "\n", + "We have seen the type `type` before in the \"*Constructors*\" section in [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb#Constructors) and also in the \"*The `namedtuple` Type*\" section in [Chapter 7's Appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/05_appendix.ipynb#The-namedtuple-Type). In the latter case, we could also use a `Point` class but the [namedtuple() ](https://docs.python.org/3/library/collections.html#collections.namedtuple) function from the [collections ](https://docs.python.org/3/library/collections.html) module in the [standard library ](https://docs.python.org/3/library/index.html) is a convenient shortcut to create custom data types that can be derived out of a plain `tuple`.\n", + "\n", + "In all examples, if an object's type is `type`, we can simply view it as a blueprint for a \"family\" of objects." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "type" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(Vector)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.Vector" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The docstrings are transformed into convenient help texts." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mInit signature:\u001b[0m \u001b[0mVector\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;31mDocstring:\u001b[0m A one-dimensional vector from linear algebra.\n", + "\u001b[0;31mType:\u001b[0m type\n", + "\u001b[0;31mSubclasses:\u001b[0m " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Vector?" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class Vector in module __main__:\n", + "\n", + "class Vector(builtins.object)\n", + " | A one-dimensional vector from linear algebra.\n", + " |\n", + " | Methods defined here:\n", + " |\n", + " | dummy_method(self)\n", + " | A dummy method for illustration purposes.\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " |\n", + " | __dict__\n", + " | dictionary for instance variables\n", + " |\n", + " | __weakref__\n", + " | list of weak references to the object\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Data and other attributes defined here:\n", + " |\n", + " | dummy_variable = 'I am a vector'\n", + "\n" + ] + } + ], + "source": [ + "help(Vector)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We can use the built-in [vars() ](https://docs.python.org/3/library/functions.html#vars) function as an alternative to [dir() ](https://docs.python.org/3/library/functions.html#dir) to obtain a *brief* summary of the attributes on `Vector`. Whereas [vars() ](https://docs.python.org/3/library/functions.html#vars) returns a read-only `dict`-like overview on mostly the *explicitly* defined attributes, [dir() ](https://docs.python.org/3/library/functions.html#dir) also shows all *implicitly* added attributes in a `list`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "mappingproxy({'__module__': '__main__',\n", + " '__doc__': 'A one-dimensional vector from linear algebra.',\n", + " 'dummy_variable': 'I am a vector',\n", + " 'dummy_method': ,\n", + " '__dict__': ,\n", + " '__weakref__': })" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vars(Vector)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['__class__',\n", + " '__delattr__',\n", + " '__dict__',\n", + " '__dir__',\n", + " '__doc__',\n", + " '__eq__',\n", + " '__format__',\n", + " '__ge__',\n", + " '__getattribute__',\n", + " '__getstate__',\n", + " '__gt__',\n", + " '__hash__',\n", + " '__init__',\n", + " '__init_subclass__',\n", + " '__le__',\n", + " '__lt__',\n", + " '__module__',\n", + " '__ne__',\n", + " '__new__',\n", + " '__reduce__',\n", + " '__reduce_ex__',\n", + " '__repr__',\n", + " '__setattr__',\n", + " '__sizeof__',\n", + " '__str__',\n", + " '__subclasshook__',\n", + " '__weakref__',\n", + " 'dummy_method',\n", + " 'dummy_variable']" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir(Vector)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the dot operator `.` we access the class attributes." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'I am a vector'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector.dummy_variable" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector.dummy_method" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "However, invoking the `.dummy_method()` raises a `TypeError`. That makes sense as the method expects a *concrete* instance passed in as the `self` argument. However, we have not yet created one." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Vector.dummy_method() missing 1 required positional argument: 'self'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdummy_method\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: Vector.dummy_method() missing 1 required positional argument: 'self'" + ] + } + ], + "source": [ + "Vector.dummy_method()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "## Instantiation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To create a *new* instance, we need to **instantiate** one.\n", + "\n", + "In the `class` statement, we see a `.__init__()` method that contains all the validation logic that we require a `Vector` instance to adhere to. In a way, this method serves as a constructor-like function.\n", + "\n", + "`.__init__()` is an example of a so-called **special method** that we use to make new data types integrate with Python's language features. Their naming follows the dunder convention. In this chapter, we introduce some of the more common special methods, and we refer to the [language reference ](https://docs.python.org/3/reference/datamodel.html) for an exhaustive list of all special methods. Special methods not *explicitly* defined in a class are *implicitly* added with a default implementation.\n", + "\n", + "The `.__init__()` method (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__init__)) is responsible for **initializing** a *new* instance object immediately after its creation. That usually means setting up some **instance attributes**. In the example, a new `Vector` instance is created from some sequence object (e.g., a `tuple` like `x`) passed in as the `data` argument. The elements provided by the `data` argument are first cast as `float` objects and then stored in a `list` object named `._entries` on the *new* instance object. Together, the `float`s represent the state encapsulated within an instance.\n", + "\n", + "A best practice is to *separate* the way we use a data type (i.e., its \"behavior\") from how we implement it. By convention, attributes that should not be accessed from \"outside\" of an instance start with one leading underscore `_`. In the example, the instance attribute `._entries` is such an **implementation detail**: We could have decided to store a `Vector`'s entries in a `tuple` instead of a `list`. However, this decision should *not* affect how a `Vector` instance is to be used. Moreover, if we changed how the `._entries` are modeled later on, this must *not* break any existing code using `Vector`s. This idea is also known as **[information hiding ](https://en.wikipedia.org/wiki/Information_hiding)** in software engineering." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + " \"\"\"A one-dimensional vector from linear algebra.\n", + "\n", + " All entries are converted to floats.\n", + " \"\"\"\n", + "\n", + " def __init__(self, data):\n", + " \"\"\"Create a new vector.\n", + "\n", + " Args:\n", + " data (sequence): the vector's entries\n", + "\n", + " Raises:\n", + " ValueError: if no entries are provided\n", + " \"\"\"\n", + " self._entries = list(float(x) for x in data)\n", + " if len(self._entries) == 0:\n", + " raise ValueError(\"a vector must have at least one entry\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To create a `Vector` instance, we call the `Vector` class with the `()` operator. This call is forwarded to the `.__init__()` method behind the scenes. That is what we mean by saying \"make new data types integrate with Python's language features\" above: We use `Vector` just as any other built-in constructor." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`v` is an object as well." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140306181183328" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Unsurprisingly, the type of `v` is `Vector`. That is the main point of this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.Vector" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`v`'s semantic \"value\" is not so clear yet. We fix this in the next section." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "<__main__.Vector at 0x7f9b9416d760>" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Although the `.__init__()` method defines *two* parameters, we must call it with only *one* `data` argument. As noted above, Python implicitly inserts a reference to the newly created instance object (i.e., `v`) as the first argument as `self`.\n", + "\n", + "Calling a class object with a wrong number of arguments leads to generic `TypeError`s ..." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Vector.__init__() missing 1 required positional argument: 'data'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[22], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: Vector.__init__() missing 1 required positional argument: 'data'" + ] + } + ], + "source": [ + "Vector()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "Vector.__init__() takes 2 positional arguments but 4 were given", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[23], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: Vector.__init__() takes 2 positional arguments but 4 were given" + ] + } + ], + "source": [ + "Vector(1, 2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... while creating a `Vector` instance from an empty sequence raises a custom `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "a vector must have at least one entry", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[24], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mVector\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[17], line 18\u001b[0m, in \u001b[0;36mVector.__init__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mfloat\u001b[39m(x) \u001b[38;5;28;01mfor\u001b[39;00m x \u001b[38;5;129;01min\u001b[39;00m data)\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma vector must have at least one entry\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mValueError\u001b[0m: a vector must have at least one entry" + ] + } + ], + "source": [ + "Vector([])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Even though we can access the `._entries` attribute on the `v` object (i.e., \"from outside\"), we are *not* supposed to do that because of the underscore `_` convention. In other words, we should access `._entries` only from within a method via `self`." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[1.0, 2.0, 3.0]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v._entries # by convention not allowed" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Text Representations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For all the built-in data types, an object's value is represented in a *literal notation*, implying that we can simply copy and paste the value into another code cell to create a *new* object with the *same* value.\n", + "\n", + "The exact representation of the value does *not* have to be identical to the one used to create the object. For example, we can create a `tuple` object without using parentheses and Python still outputs its value with `(` and `)`. That was an *arbitrary* design decision by the core development team." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "x = 1, 2, 3" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 2, 3)" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 2, 3)" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(1, 2, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To control how objects of a user-defined data type are represented as text, we implement the `.__repr__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__repr__)) and `.__str__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__str__)) methods. Both take only a `self` argument and must return a `str` object." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self._entries)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __str__(self):\n", + " first, last = self._entries[0], self._entries[-1]\n", + " n_entries = len(self._entries)\n", + " return f\"Vector({first!r}, ..., {last!r})[{n_entries:d}]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, when `v` is evaluated in a code cell, we see the return value of the `.__repr__()` method.\n", + "\n", + "According to the specification, `.__repr__()` should return a `str` object that, when used as a literal, creates a *new* instance with the *same* state (i.e., their `._entries` attributes compare equal) as the original one. In other words, it should return a **text representation** of the object optimized for direct consumption by the Python interpreter. That is often useful when debugging or logging large applications.\n", + "\n", + "Our implementation of `.__repr__()` in the `Vector` class uses to a `tuple` notation for the `data` argument. So, even if we create `v` from a `list` object like `[1, 2, 3]` and even though the `_entries` are stored as a `list` object internally, a `Vector` instance's text representation \"defaults\" to `((` and `))` in the output. This decision is arbitrary and we could have used a `list` notation for the `data` argument as well." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we copy and paste the value of the `v` object into another code cell, we create a *new* `Vector` instance with the *same* state as `v`." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Alternatively, the built-in [repr()]( https://docs.python.org/3/library/functions.html#repr) function returns an object's value as a `str` object (i.e., with the quotes `'`)." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Vector((1.0, 2.0, 3.0))'" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "repr(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "On the contrary, the `.__str__()` method should return a *human-readable* text representation of the object, and we use the built-in [str() ](https://docs.python.org/3/library/functions.html#func-str) and [print() ](https://docs.python.org/3/library/functions.html#print) functions to obtain this representation explicitly.\n", + "\n", + "For our `Vector` class, this representation only shows a `Vector`'s first and last entries followed by the total number of entries in brackets. So, even for a `Vector` containing millions of entries, we could easily make sense of the representation.\n", + "\n", + "While [str() ](https://docs.python.org/3/library/functions.html#func-str) returns the text representation as a `str` object, ..." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Vector(1.0, ..., 3.0)[3]'" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "str(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... [print() ](https://docs.python.org/3/library/functions.html#print) does not show the enclosing quotes." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vector(1.0, ..., 3.0)[3]\n" + ] + } + ], + "source": [ + "print(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "From a theoretical point of view, the text representation provided by `.__repr__()` contains all the information (i.e., the $0$s and $1$s in memory) that is needed to model something in a computer. In a way, it is a natural extension from the binary (cf., [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb#Binary-Representations)), hexadecimal (cf., [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb#Hexadecimal-Representations)), and `bytes` (cf., [Chapter 6 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/02_content.ipynb#The-bytes-Type)) representations of information. After all, just like Unicode characters are encoded in `bytes`, the more \"complex\" objects in this chapter are encoded in Unicode characters via their text representations." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### The `Matrix` Class" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Below is a first implementation of the `Matrix` class that stores the `._entries` internally as a `list` of `list`s.\n", + "\n", + "The `.__init__()` method ensures that all the rows come with the same number of columns. Again, we do not allow `Matrix` instances without any entries." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " for row in self._entries[1:]:\n", + " if len(row) != len(self._entries[0]):\n", + " raise ValueError(\"rows must have the same number of entries\")\n", + " if len(self._entries) == 0:\n", + " raise ValueError(\"a matrix must have at least one entry\")\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " def __str__(self):\n", + " first, last = self._entries[0][0], self._entries[-1][-1]\n", + " m, n = len(self._entries), len(self._entries[0])\n", + " return f\"Matrix(({first!r}, ...), ..., (..., {last!r}))[{m:d}x{n:d}]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`Matrix` is an object as well." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "94113690738160" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(Matrix)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "type" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(Matrix)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.Matrix" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's create a new `Matrix` instance from a `list` of `tuple`s." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "140306180401856" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "id(m)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.Matrix" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The text representations work as above." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Matrix((1.0, ...), ..., (..., 9.0))[3x3]\n" + ] + } + ], + "source": [ + "print(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Passing an invalid `data` argument when instantiating a `Matrix` results in the documented exceptions." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "a matrix must have at least one entry", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[45], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mMatrix\u001b[49m\u001b[43m(\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[36], line 9\u001b[0m, in \u001b[0;36mMatrix.__init__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrows must have the same number of entries\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m----> 9\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma matrix must have at least one entry\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mValueError\u001b[0m: a matrix must have at least one entry" + ] + } + ], + "source": [ + "Matrix(())" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "rows must have the same number of entries", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[46], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mMatrix\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m3\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m4\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m5\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[36], line 7\u001b[0m, in \u001b[0;36mMatrix.__init__\u001b[0;34m(self, data)\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m row \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries[\u001b[38;5;241m1\u001b[39m:]:\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(row) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries[\u001b[38;5;241m0\u001b[39m]):\n\u001b[0;32m----> 7\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrows must have the same number of entries\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma matrix must have at least one entry\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mValueError\u001b[0m: rows must have the same number of entries" + ] + } + ], + "source": [ + "Matrix([(1, 2, 3), (4, 5)])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Instance Methods vs. Class Methods" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The methods we have seen so far are all **instance methods**. The characteristic idea behind an instance method is that the behavior it provides either depends on the state of a concrete instance or mutates it. In other words, an instance method *always* works with attributes on the `self` argument. If a method does *not* need access to `self` to do its job, it is conceptually *not* an instance method and we should probably convert it into another kind of method as shown below.\n", + "\n", + "An example of an instance method from linear algebra is the `.transpose()` method below that switches the rows and columns of an *existing* `Matrix` instance and returns a *new* `Matrix` instance based off that. It is implemented by passing the *iterator* created with the [zip() ](https://docs.python.org/3/library/functions.html#zip) built-in as the `data` argument to the `Matrix` constructor: The expression `zip(*self._entries)` may be a bit hard to understand because of the involved unpacking but simply flips a `Matrix`'s rows and columns. The built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor within the `.__init__()` method then materializes the iterator into the `._entries` attribute. Without a concrete `Matrix`'s rows and columns, `.transpose()` does not make sense, conceptually speaking.\n", + "\n", + "Also, we see that it is ok to reference a class from within one of its methods. While this seems trivial to some readers, others may find this confusing. The final versions of the `Vector` and `Matrix` classes in the [fourth part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/04_content.ipynb#The-final-Vector-and-Matrix-Classes) of this chapter show how this \"hard coded\" redundancy can be avoided." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " def transpose(self):\n", + " return Matrix(zip(*self._entries))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `.transpose()` method returns a *new* `Matrix` instance where the rows and columns are flipped." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 4.0, 7.0,), (2.0, 5.0, 8.0,), (3.0, 6.0, 9.0,)))" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.transpose()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Two invocations of `.transpose()` may be chained, which negates its overall effect but still creates a *new* instance object (i.e., `m is n` is `False`)." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "n = m.transpose().transpose()" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m is n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Unintuitively, the comparison operator `==` returns a wrong result as `m` and `n` have `_entries` attributes that compare equal. We fix this in the \"*Operator Overloading*\" section later in this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m == n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sometimes, it is useful to attach functionality to a class object that does *not* depend on the state of a concrete instance but on the class as a whole. Such methods are called **class methods** and can be created with the [classmethod() ](https://docs.python.org/3/library/functions.html#classmethod) built-in combined with the `@` **decorator** syntax. Then, Python adapts the binding process described above such that it implicitly inserts a reference to the class object itself instead of the instance when the method is invoked. By convention, we name this parameter `cls`.\n", + "\n", + "Class methods are often used to provide an alternative way to create instances, usually from a different kind of arguments. As an example, `.from_columns()` expects a sequence of columns instead of rows as its `data` argument. It forwards the invocation to the `.__init__()` method (i.e., what `cls(data)` does; `cls` references the *same* class object as `Matrix`), then calls the `.transpose()` method on the newly created instance, and lastly returns the instance created by `.transpose()`. Again, we are intelligently *reusing* a lot of code." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " def transpose(self):\n", + " return Matrix(zip(*self._entries))\n", + "\n", + " @classmethod\n", + " def from_columns(cls, data):\n", + " return cls(data).transpose()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We use the alternative `.from_columns()` constructor to create a `Matrix` equivalent to `m` above from a `list` of columns instead of rows." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix.from_columns([(1, 4, 7), (2, 5, 8), (3, 6, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "There is also a [staticmethod() ](https://docs.python.org/3/library/functions.html#staticmethod) built-in to be used with the `@` syntax to define methods that are *independent* from both the class and instance objects but nevertheless related semantically to a class. In this case, the binding process is disabled an no argument is implicitly inserted upon a method's invocation. Such **static methods** are not really needed most of the time and we omit them here fore brevity." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Computed Properties" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After creation, a `Matrix` instance exhibits certain properties that depend only on the concrete `data` encapsulated in it. For example, every `Matrix` instance implicitly has *two* dimensions: These are commonly denoted as $m$ and $n$ in math and represent the number of rows and columns.\n", + "\n", + "We would like our `Matrix` instances to have two attributes, `.n_rows` and `.n_cols`, that provide the correct dimensions as `int` objects. To achieve that, we implement two instance methods, `.n_rows()` and `.n_cols()`, and make them **derived attributes** by decorating them with the [property() ](https://docs.python.org/3/library/functions.html#property) built-in. They work like methods except that they do not need to be invoked with the call operator `()` but can be accessed as if they were instance variables.\n", + "\n", + "To reuse their code, we integrate the new properties already within the `.__init__()` method." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " for row in self._entries[1:]:\n", + " if len(row) != self.n_cols:\n", + " raise ValueError(\"rows must have the same number of entries\")\n", + " if self.n_rows == 0:\n", + " raise ValueError(\"a matrix must have at least one entry\")\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " @property\n", + " def n_rows(self):\n", + " return len(self._entries)\n", + "\n", + " @property\n", + " def n_cols(self):\n", + " return len(self._entries[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The revised `m` models a $2 \\times 3$ matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6)])" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(2, 3)" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.n_rows, m.n_cols" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In its basic form, properties are *read-only* attributes. This makes sense for `Matrix` instances where we can *not* \"set\" how many rows and columns there are while keeping the `_entries` unchanged." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "property 'n_rows' of 'Matrix' object has no setter", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[61], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mn_rows\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m3\u001b[39m\n", + "\u001b[0;31mAttributeError\u001b[0m: property 'n_rows' of 'Matrix' object has no setter" + ] + } + ], + "source": [ + "m.n_rows = 3" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/11_classes/01_exercises_tsp.ipynb b/11_classes/01_exercises_tsp.ipynb new file mode 100644 index 0000000..fbdbc1f --- /dev/null +++ b/11_classes/01_exercises_tsp.ipynb @@ -0,0 +1,1502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Run All*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *after* finishing the exercises to ensure that your solution runs top to bottom *without* any errors. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/01_exercises.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 11: Classes & Instances (Coding Exercises)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exercises below assume that you have read the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) of Chapter 11.\n", + "\n", + "The `...`'s in the code cells indicate where you need to fill in code snippets. The number of `...`'s within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Berlin Tourist Guide: A Traveling Salesman Problem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is a hands-on and tutorial-like application to show how to load data from web services like [Google Maps](https://developers.google.com/maps) and use them to solve a logistics problem, namely a **[Traveling Salesman Problem ](https://en.wikipedia.org/wiki/Traveling_salesman_problem)**.\n", + "\n", + "Imagine that a tourist lands at Berlin's [Tegel Airport ](https://en.wikipedia.org/wiki/Berlin_Tegel_Airport) in the morning and has his \"connecting\" flight from [Schönefeld Airport ](https://en.wikipedia.org/wiki/Berlin_Sch%C3%B6nefeld_Airport) in the evening. By the time, the flights were scheduled, the airline thought that there would be only one airport in Berlin.\n", + "\n", + "Having never been in Berlin before, the tourist wants to come up with a plan of sights that he can visit with a rental car on his way from Tegel to Schönefeld.\n", + "\n", + "With a bit of research, he creates a `list` of `sights` like below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "arrival = \"Berlin Tegel Airport (TXL), Berlin\"\n", + "\n", + "sights = [\n", + " \"Alexanderplatz, Berlin\",\n", + " \"Brandenburger Tor, Pariser Platz, Berlin\",\n", + " \"Checkpoint Charlie, Friedrichstraße, Berlin\",\n", + " \"Kottbusser Tor, Berlin\",\n", + " \"Mauerpark, Berlin\",\n", + " \"Siegessäule, Berlin\",\n", + " \"Reichstag, Platz der Republik, Berlin\",\n", + " \"Soho House Berlin, Torstraße, Berlin\",\n", + " \"Tempelhofer Feld, Berlin\",\n", + "]\n", + "\n", + "departure = \"Berlin Schönefeld Airport (SXF), Berlin\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With just the street addresses, however, he cannot calculate a route. He needs `latitude`-`longitude` coordinates instead. While he could just open a site like [Google Maps](https://www.google.com/maps) in a web browser, he wonders if he can download the data with a bit of Python code using a [web API ](https://en.wikipedia.org/wiki/Web_API) offered by [Google](https://www.google.com).\n", + "\n", + "So, in this notebook, we solve the entire problem with code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Geocoding" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to obtain coordinates for the given street addresses above, a process called **geocoding**, we use the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start).\n", + "\n", + "**Q1**: Familiarize yourself with this [documentation](https://developers.google.com/maps/documentation/geocoding/start), register a developer account, create a project, and [create an API key](https://console.cloud.google.com/apis/credentials) that is necessary for everything to work! Then, [enable the Geocoding API](https://console.developers.google.com/apis/library/geocoding-backend.googleapis.com) and link a [billing account](https://console.developers.google.com/billing)!\n", + "\n", + "Info: The first 200 Dollars per month are not charged (cf., [pricing page](https://cloud.google.com/maps-platform/pricing/)), so no costs will incur for this tutorial. You must sign up because Google simply wants to know the people using its services.\n", + "\n", + "**Q2**: Assign the API key as a `str` object to the `key` variable!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "key = \" < your API key goes here > \"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To use external web services, our application needs to make HTTP requests just like web browsers do when surfing the web.\n", + "\n", + "We do not have to implement this on our own. Instead, we use the official Python Client for the Google Maps Services provided by Google in one of its corporate [GitHub repositories ](https://github.com/googlemaps).\n", + "\n", + "**Q3**: Familiarize yourself with the [googlemaps ](https://github.com/googlemaps/google-maps-services-python) package! Then, install it with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install googlemaps" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: Finish the following code cells and instantiate a `Client` object named `api`! Use the `key` from above. `api` provides us with a lot of methods to talk to the API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import googlemaps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "api = googlemaps.Client(...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "api" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(api)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: Execute the next code cell to list the methods and attributes on the `api` object!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "[x for x in dir(api) if not x.startswith(\"_\")]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To obtain all kinds of information associated with a street address, we call the `geocode()` method with the address as the sole argument.\n", + "\n", + "For example, let's search for Brandenburg Gate. Its street address is `\"Brandenburger Tor, Pariser Platz, Berlin\"`.\n", + "\n", + "**Q6**: Execute the next code cell!\n", + "\n", + "Hint: If you get an error message, follow the instructions in it to debug it.\n", + "\n", + "If everything works, we receive a `list` with a single `dict` in it. That means the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) only knows about one place at the address. Unfortunately, the `dict` is pretty dense and hard to read." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: Capture the first and only search result in the `brandenburg_gate` variable and \"pretty print\" it with the help of the [pprint() ](https://docs.python.org/3/library/pprint.html#pprint.pprint) function in the [pprint ](https://docs.python.org/3/library/pprint.html) module in the [standard library ](https://docs.python.org/3/library/index.html)!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "response = api.geocode(\"Brandenburger Tor, Pariser Platz, Berlin\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `dict` has several keys that are of use for us: `\"formatted_address\"` is a cleanly formatted version of the address. `\"geometry\"` is a nested `dict` with several `lat`-`lng` coordinates representing the place where `\"location\"` is the one we need for our calculations. Lastly, `\"place_id\"` is a unique identifier that allows us to obtain further information about the address from other Google APIs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pprint import pprint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pprint(brandenburg_gate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To keep our code readable and maintainable, we create a `Place` class to manage the API results in a clean way.\n", + "\n", + "The `.__init__()` method takes a `street_address` (e.g., an element of `sights`) and a `client` argument (e.g., an object like `api`) and stores them on `self`. The place's `.name` is parsed out of the `street_address` as well: It is the part before the first comma. Also, the instance attributes `.latitude`, `.longitude`, and `.place_id` are initialized to `None`.\n", + "\n", + "**Q8**: Finish the `.__init__()` method according to the description!\n", + "\n", + "The `.sync_from_google()` method uses the internally kept `client` and synchronizes the place's state with the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start). In particular, it updates the `.address` with the `formatted_address` and stores the values for `.latitude`, `.longitude`, and `.place_id`. It enables method chaining.\n", + "\n", + "**Q9**: Implement the `.sync_from_google()` method according to the description!\n", + "\n", + "**Q10**: Add a read-only `.location` property on the `Place` class that returns the `.latitude` and `.longitude` as a `tuple`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answer to Q8\n", + " def __init__(self, street_address, *, client):\n", + " \"\"\"Create a new place.\n", + "\n", + " Args:\n", + " street_address (str): street address of the place\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " \"\"\"\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + " ...\n", + "\n", + " def __repr__(self):\n", + " cls, name = self.__class__.__name__, self.name\n", + " synced = \" [SYNCED]\" if self.place_id else \"\"\n", + " return f\"<{cls}: {name}{synced}>\"\n", + "\n", + " # answer to Q9\n", + " def sync_from_google(self):\n", + " \"\"\"Download the place's coordinates and other info.\"\"\"\n", + " response = ...\n", + " first_hit = ...\n", + " ... = first_hit[...]\n", + " ... = first_hit[...][...][...]\n", + " ... = first_hit[...][...][...]\n", + " ... = first_hit[...]\n", + " return ...\n", + "\n", + " # answer to Q10\n", + " ...\n", + " ...\n", + " ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: Verify that the instantiating a `Place` object works!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: What do the angle brackets `<` and `>` mean in the text representation?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can obtain the geo-data from the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/start) in a clean way. As we enabled method chaining for `.sync_from_google()`, we get back the instance after calling the method.\n", + "\n", + "**Q13**: Verify that the `.sync_from_google()` method works!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate.sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate.address" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate.place_id" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate.location" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class (continued): Batch Synchronization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: Add an alternative constructor method named `.from_addresses()` that takes an `addresses`, a `client`, and a `sync` argument! `addresses` is a finite iterable of `str` objects (e.g., like `sights`). The method returns a `list` of `Place`s, one for each `str` in `addresses`. All `Place`s are initialized with the same `client`. `sync` is a flag and defaults to `False`. If it is set to `True`, the alternative constructor invokes the `.sync_from_google()` method on the `Place`s before returning them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "code_folding": [] + }, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\n", + "\n", + " # answer to Q14\n", + " ...\n", + " def from_addresses(cls, addresses, *, client, sync=False):\n", + " \"\"\"Create new places in a batch.\n", + "\n", + " Args:\n", + " addresses (iterable of str's): the street addresses of the places\n", + " client (googlemaps.Client): access to the Google Maps Geocoding API\n", + " Returns:\n", + " list of Places\n", + " \"\"\"\n", + " places = ...\n", + " for address in addresses:\n", + " place = ...\n", + " if sync:\n", + " ...\n", + " ...\n", + " return places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q15**: Verify that the alternative constructor works with and without the `sync` flag!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Place.from_addresses(sights, client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For geo-data it always makes sense to plot them on a map. We use the third-party library [folium ](https://github.com/python-visualization/folium) to achieve that.\n", + "\n", + "**Q16**: Familiarize yourself with [folium ](https://github.com/python-visualization/folium) and install it with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install folium" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q17**: Execute the code cells below to create an empty map of Berlin!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import folium" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = folium.Map(location=(52.513186, 13.3944349), zoom_start=14)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(berlin)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`folium.Map` instances are shown as interactive maps in Jupyter notebooks whenever they are the last expression in a code cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to put something on the map, [folium ](https://github.com/python-visualization/folium) works with so-called `Marker` objects.\n", + "\n", + "**Q18**: Review its docstring and then create a marker `m` with the location data of Brandenburg Gate! Use the `brandenburg_gate` object from above!\n", + "\n", + "Hint: You may want to use HTML tags for the `popup` argument to format the text output on the map in a nicer way. So, instead of just passing `\"Brandenburger Tor\"` as the `popup` argument, you could use, for example, `\"Brandenburger Tor
(Pariser Platz, 10117 Berlin, Germany)\"`. Then, the name appears in bold and the street address is put on the next line. You could use an f-string to parametrize the argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "folium.Marker?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = folium.Marker(\n", + " location=...,\n", + " popup=...,\n", + " tooltip=...,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "type(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q19**: Execute the next code cells that add `m` to the `berlin` map!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.add_to(berlin)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class (continued): Marker Representation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q20**: Finish the `.as_marker()` method that returns a `Marker` instance when invoked on a `Place` instance! The method takes an optional `color` argument that uses [folium ](https://github.com/python-visualization/folium)'s `Icon` type to control the color of the marker." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\n", + "\n", + " # answer to Q20\n", + " def as_marker(self, *, color=\"blue\"):\n", + " \"\"\"Create a Marker representation of the place.\n", + "\n", + " Args:\n", + " color (str): color of the marker, defaults to \"blue\"\n", + "\n", + " Returns:\n", + " marker (folium.Marker)\n", + "\n", + " Raises:\n", + " RuntimeError: if the place is not synchronized with\n", + " the Google Maps Geocoding API\n", + " \"\"\"\n", + " if not self.place_id:\n", + " raise RuntimeError(\"must synchronize with Google first\")\n", + " return folium.Marker(\n", + " location=...,\n", + " popup=...,\n", + " tooltip=...,\n", + " icon=folium.Icon(color=color),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q21**: Execute the next code cells that create a new `Place` and obtain a `Marker` for it!\n", + "\n", + "Notes: Without synchronization, we get a `RuntimeError`. `.as_marker()` can be chained right after `.sync_from_google()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate = Place(\"Brandenburger Tor, Pariser Platz, Berlin\", client=api)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate.as_marker()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "brandenburg_gate.sync_from_google().as_marker()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q22**: Use the alternative `.from_addresses()` constructor to create a `list` named `places` with already synced `Place`s!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "places = Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Map` Class" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "subslide" + } + }, + "source": [ + "To make [folium ](https://github.com/python-visualization/folium)'s `Map` class work even better with our `Place` instances, we write our own `Map` class wrapping [folium ](https://github.com/python-visualization/folium)'s. Then, we add further functionality to the class throughout this tutorial.\n", + "\n", + "The `.__init__()` method takes mandatory `name`, `center`, `start`, `end`, and `places` arguments. `name` is there for convenience, `center` is the map's initial center, `start` and `end` are `Place` instances, and `places` is a finite iterable of `Place` instances. Also, `.__init__()` accepts an optional `initial_zoom` argument defaulting to `12`.\n", + "\n", + "Upon initialization, a `folium.Map` instance is created and stored as an implementation detail `_map`. Also, `.__init__()` puts markers for each place on the `_map` object: `\"green\"` and `\"red\"` markers for the `start` and `end` locations and `\"blue\"` ones for the `places` to be visited. To do that, `.__init__()` invokes another `.add_marker()` method on the `Map` class, once for every `Place` object. `.add_marker()` itself invokes the `.add_to()` method on the `folium.Marker` representation of a `Place` instance and enables method chaining.\n", + "\n", + "To keep the state in a `Map` instance consistent, all passed in arguments except `name` are treated as implementation details. Otherwise, a user of the `Map` class could, for example, change the `start` attribute, which would not be reflected in the internally kept `folium.Map` object.\n", + "\n", + "**Q23**: Implement the `.__init__()` and `.add_marker()` methods on the `Map` class as described!\n", + "\n", + "**Q24**: Add a `.show()` method on the `Map` class that simply returns the internal `folium.Map` object!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answer to Q23\n", + " def __init__(self, name, center, start, end, places, initial_zoom=12):\n", + " \"\"\"Create a new map.\n", + "\n", + " Args:\n", + " name (str): name of the map\n", + " center (float, float): coordinates of the map's center\n", + " start (Place): start of the tour\n", + " end (Place): end of the tour\n", + " places (iterable of Places): the places to be visitied\n", + " initial_zoom (integer): zoom level according to folium's\n", + " specifications; defaults to 12\n", + " \"\"\"\n", + " self.name = name\n", + " ...\n", + " ...\n", + " ...\n", + " ... = folium.Map(...)\n", + "\n", + " # Add markers to the map.\n", + " ...\n", + " ...\n", + " for place in places:\n", + " ...\n", + "\n", + " def __repr__(self):\n", + " return f\"\"\n", + "\n", + " # answer to Q24\n", + " def show(self):\n", + " \"\"\"Return a folium.Map representation of the map.\"\"\"\n", + " return ...\n", + "\n", + " # answer to Q23\n", + " def add_marker(self, marker):\n", + " \"\"\"Add a marker to the map.\n", + "\n", + " Args:\n", + " marker (folium.Marker): marker to be put on the map\n", + " \"\"\"\n", + " ...\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's put all the sights, the two airports, and three more places, the [Bundeskanzleramt ](https://en.wikipedia.org/wiki/German_Chancellery), the [Olympic Stadium ](https://en.wikipedia.org/wiki/Olympiastadion_%28Berlin%29), and the [East Side Gallery ](https://en.wikipedia.org/wiki/East_Side_Gallery), on the map.\n", + "\n", + "**Q25**: Execute the next code cells to create a map of Berlin with all the places on it!\n", + "\n", + "Note: Because we implemented method chaining everywhere, the code below is only *one* expression written over several lines. It almost looks like a self-explanatory and compact \"language\" on its own." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = (\n", + " Map(\n", + " \"Sights in Berlin\",\n", + " center=(52.5015154, 13.4066838),\n", + " start=Place(arrival, client=api).sync_from_google(),\n", + " end=Place(departure, client=api).sync_from_google(),\n", + " places=places,\n", + " initial_zoom=10,\n", + " )\n", + " .add_marker(\n", + " Place(\"Bundeskanzleramt, Willy-Brandt-Straße, Berlin\", client=api)\n", + " .sync_from_google()\n", + " .as_marker(color=\"orange\")\n", + " )\n", + " .add_marker(\n", + " Place(\"Olympiastadion Berlin\", client=api)\n", + " .sync_from_google()\n", + " .as_marker(color=\"orange\")\n", + " )\n", + " .add_marker(\n", + " Place(\"East Side Gallery, Berlin\", client=api)\n", + " .sync_from_google()\n", + " .as_marker(color=\"orange\")\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Distance Matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we can find out the best order in which to visit all the sights, we must calculate the pairwise distances between all points. While Google also offers a [Directions API](https://developers.google.com/maps/documentation/directions/start) and a [Distance Matrix API](https://developers.google.com/maps/documentation/distance-matrix/start), we choose to calculate the air distances using the third-party library [geopy ](https://github.com/geopy/geopy).\n", + "\n", + "**Q26**: Familiarize yourself with the [documentation](https://geopy.readthedocs.io/en/stable/) and install [geopy ](https://github.com/geopy/geopy) with the `pip` command line tool!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install geopy" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use [geopy ](https://github.com/geopy/geopy) primarily for converting the `latitude`-`longitude` coordinates into a [distance matrix ](https://en.wikipedia.org/wiki/Distance_matrix).\n", + "\n", + "Because the [earth is not flat ](https://en.wikipedia.org/wiki/Flat_Earth), [geopy ](https://github.com/geopy/geopy) provides a `great_circle()` function that calculates the so-called [orthodromic distance ](https://en.wikipedia.org/wiki/Great-circle_distance) between two places on a sphere." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from geopy.distance import great_circle" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q27**: For quick reference, read the docstring of `great_circle()` and execute the code cells below to calculate the distance between the `arrival` and the `departure`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "great_circle?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tegel = Place(arrival, client=api).sync_from_google()\n", + "schoenefeld = Place(departure, client=api).sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "great_circle(tegel.location, schoenefeld.location)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "great_circle(tegel.location, schoenefeld.location).km" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "great_circle(tegel.location, schoenefeld.location).meters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Place` Class (continued): Distance to another `Place`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q28**: Finish the `distance_to()` method in the `Place` class that takes a `other` argument and returns the distance in meters! Adhere to the given docstring!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Place:\n", + " \"\"\"A place connected to the Google Maps Geocoding API.\"\"\"\n", + "\n", + " # answers from above\n", + "\n", + " # answer to Q28\n", + " def distance_to(self, other):\n", + " \"\"\"Calculate the distance to another place in meters.\n", + "\n", + " Args:\n", + " other (Place): the other place to calculate the distance to\n", + "\n", + " Returns:\n", + " distance (int)\n", + "\n", + " Raises:\n", + " RuntimeError: if one of the places is not synchronized with\n", + " the Google Maps Geocoding API\n", + " \"\"\"\n", + " if not self.place_id or not other.place_id:\n", + " raise RuntimeError(\"must synchronize places with Google first\")\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q29**: Execute the code cells below to test the new feature!\n", + "\n", + "Note: If done right, object-oriented code reads almost like plain English." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tegel = Place(arrival, client=api).sync_from_google()\n", + "schoenefeld = Place(departure, client=api).sync_from_google()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tegel.distance_to(schoenefeld)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q30**: Execute the next code cell to instantiate the `Place`s in `sights` again!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "places = Place.from_addresses(sights, client=api, sync=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "places" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Map` Class (continued): Pairwise Distances" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we add a read-only `distances` property on our `Map` class. As we are working with air distances, these are *symmetric* which reduces the number of distances we must calculate.\n", + "\n", + "To do so, we use the [combinations() ](https://docs.python.org/3/library/itertools.html#itertools.combinations) generator function in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](https://docs.python.org/3/library/index.html). That produces all possible `r`-`tuple`s from an `iterable` argument. `r` is `2` in our case as we are looking at `origin`-`destination` pairs.\n", + "\n", + "Let's first look at an easy example of [combinations() ](https://docs.python.org/3/library/itertools.html#itertools.combinations) to understand how it works: It gives us all the `2`-`tuple`s from a `list` of five `numbers` disregarding the order of the `tuple`s' elements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import itertools" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numbers = [1, 2, 3, 4, 5]\n", + "\n", + "for x, y in itertools.combinations(numbers, 2):\n", + " print(x, y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`distances` uses the internal `._start`, `._end`, and `._places` attributes and creates a `dict` with the keys consisting of all pairs of `Place`s and the values being their distances in meters. As this operation is rather costly, we cache the distances the first time we calculate them into a hidden instance attribute `._distances`, which must be initialized with `None` in the `.__init__()` method.\n", + "\n", + "**Q31**: Finish the `.distances` property as described!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answers from above with a tiny adjustment\n", + "\n", + " # answer to Q31\n", + " ...\n", + " def distances(self):\n", + " \"\"\"Return a dict with the pairwise distances of all places.\n", + "\n", + " Implementation note: The results of the calculations are cached.\n", + " \"\"\"\n", + " if not self._distances:\n", + " distances = ...\n", + " all_pairs = itertools.combinations(\n", + " ...,\n", + " r=2,\n", + " )\n", + " for origin, destination in all_pairs:\n", + " distance = ...\n", + " distances[origin, destination] = distance\n", + " distances[destination, origin] = distance\n", + " self._distances = ...\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We pretty print the total distance matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = Map(\n", + " \"Berlin\",\n", + " center=(52.5015154, 13.4066838),\n", + " start=Place(arrival, client=api).sync_from_google(),\n", + " end=Place(departure, client=api).sync_from_google(),\n", + " places=places,\n", + " initial_zoom=10,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pprint(berlin.distances)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How can we be sure the matrix contains all possible pairs? As we have 9 `sights` plus the `start` and the `end` of the tour, we conclude that there must be `11 * 10 = 110` distances excluding the `0` distances of a `Place` to itself that are not in the distance matrix." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "n_places = len(places) + 2\n", + "\n", + "n_places * (n_places - 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "len(berlin.distances)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Route Optimization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us find the cost minimal order of traveling from the `arrival` airport to the `departure` airport while visiting all the `sights`.\n", + "\n", + "This problem can be expressed as finding the shortest so-called [Hamiltonian path ](https://en.wikipedia.org/wiki/Hamiltonian_path) from the `start` to `end` on the `Map` (i.e., a path that visits each intermediate node exactly once). With the \"hack\" of assuming the distance of traveling from the `end` to the `start` to be `0` and thereby effectively merging the two airports into a single node, the problem can be viewed as a so-called [traveling salesman problem ](https://en.wikipedia.org/wiki/Traveling_salesman_problem) (TSP).\n", + "\n", + "The TSP is a hard problem to solve but also well studied in the literature. Assuming symmetric distances, a TSP with $n$ nodes has $\\frac{(n-1)!}{2}$ possible routes. $(n-1)$ because any node can be the `start` / `end` and divided by $2$ as the problem is symmetric.\n", + "\n", + "Starting with about $n = 20$, the TSP is almost impossible to solve exactly in a reasonable amount of time. Luckily, we do not have that many `sights` to visit, and so we use a [brute force ](https://en.wikipedia.org/wiki/Brute-force_search) approach and simply loop over all possible routes to find the shortest.\n", + "\n", + "In the case of our tourist, we \"only\" need to try out `181_440` possible routes because the two airports are effectively one node and $n$ becomes $10$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "math.factorial(len(places) + 1 - 1) // 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Analyzing the problem a bit further, all we need is a list of [permutations ](https://en.wikipedia.org/wiki/Permutation) of the sights as the two airports are always the first and last location.\n", + "\n", + "The [permutations() ](https://docs.python.org/3/library/itertools.html#itertools.permutations) generator function in the [itertools ](https://docs.python.org/3/library/itertools.html) module in the [standard library ](https://docs.python.org/3/library/index.html) helps us with the task. Let's see an example to understand how it works." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "numbers = [1, 2, 3]\n", + "\n", + "for permutation in itertools.permutations(numbers):\n", + " print(permutation)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, if we use [permutations() ](https://docs.python.org/3/library/itertools.html#itertools.permutations) as is, we try out *redundant* routes. For example, transferred to our case, the tuples `(1, 2, 3)` and `(3, 2, 1)` represent the *same* route as the distances are symmetric and the tourist could be going in either direction. To obtain the *unique* routes, we use an `if`-clause in a \"hacky\" way by only accepting routes where the first node has a smaller value than the last. Thus, we keep, for example, `(1, 2, 3)` and discard `(3, 2, 1)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for permutation in itertools.permutations(numbers):\n", + " if permutation[0] < permutation[-1]:\n", + " print(permutation)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to compare `Place`s as numbers, we would have to implement, among others, the `.__eq__()` special method. Otherwise, we get a `TypeError`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Place(arrival, client=api) < Place(departure, client=api)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a quick and dirty solution, we use the `.location` property on a `Place` to do the comparison." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Place(arrival, client=api).location < Place(departure, client=api).location" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As the code cell below shows, combining [permutations() ](https://docs.python.org/3/library/itertools.html#itertools.permutations) with an `if`-clause results in the correct number of routes to be looped over." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sum(\n", + " 1\n", + " for route in itertools.permutations(places)\n", + " if route[0].location < route[-1].location\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To implement the brute force algorithm, we split the logic into two methods.\n", + "\n", + "First, we create an `.evaluate()` method that takes a `route` argument that is a sequence of `Place`s and returns the total distance of the route. Internally, this method uses the `.distances` property repeatedly, which is why we built in caching above.\n", + "\n", + "**Q32**: Finish the `.evaluate()` method as described!\n", + "\n", + "Second, we create a `.brute_force()` method that needs no arguments. It loops over all possible routes to find the shortest. As the `start` and `end` of a route are fixed, we only need to look at `permutation`s of inner nodes. Each `permutation` can then be traversed in a forward and a backward order. `.brute_force()` enables method chaining as well.\n", + "\n", + "**Q33**: Finish the `.brute_force()` method as described!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The `Map` Class (continued): Brute Forcing the TSP" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Map:\n", + " \"\"\"A map with plotting and routing capabilities.\"\"\"\n", + "\n", + " # answers from above\n", + "\n", + " # answer to Q32\n", + " def evaluate(self, route):\n", + " \"\"\"Calculate the total distance of a route.\n", + "\n", + " Args:\n", + " route (sequence of Places): the ordered nodes in a tour\n", + "\n", + " Returns:\n", + " cost (int)\n", + " \"\"\"\n", + " cost = ...\n", + " # Loop over all pairs of consecutive places.\n", + " origin = ...\n", + " for destination in ...:\n", + " cost += self.distances[...]\n", + " ...\n", + "\n", + " return ...\n", + "\n", + " # answer to Q33\n", + " def brute_force(self):\n", + " \"\"\"Calculate the shortest route by brute force.\n", + "\n", + " The route is plotted on the folium.Map.\n", + " \"\"\"\n", + " # Assume a very high cost to begin with.\n", + " min_cost = ...\n", + " best_route = None\n", + "\n", + " # Loop over all permutations of the intermediate nodes to visit.\n", + " for permutation in ...:\n", + " # Skip redundant permutations.\n", + " if ...:\n", + " ...\n", + " # Travel through the routes in both directions.\n", + " for route in (permutation, permutation[::-1]):\n", + " # Add start and end to the route.\n", + " route = (..., *route, ...)\n", + " # Check if a route is cheaper than all routes seen before.\n", + " cost = ...\n", + " if ...:\n", + " min_cost = ...\n", + " best_route = ...\n", + "\n", + " # Plot the route on the map\n", + " folium.PolyLine(\n", + " [x.location for x in best_route],\n", + " color=\"orange\", weight=3, opacity=1\n", + " ).add_to(self._map)\n", + "\n", + " return ..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q34**: Find the best route for our tourist by executing the code cells below!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin = Map(\n", + " \"Berlin\",\n", + " center=(52.4915154, 13.4066838),\n", + " start=Place(arrival, client=api).sync_from_google(),\n", + " end=Place(departure, client=api).sync_from_google(),\n", + " places=places,\n", + " initial_zoom=12,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "berlin.brute_force().show()" + ] + } + ], + "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.12.2" + }, + "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": "320px" + }, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/11_classes/02_content.ipynb b/11_classes/02_content.ipynb new file mode 100644 index 0000000..5ab6d81 --- /dev/null +++ b/11_classes/02_content.ipynb @@ -0,0 +1,1983 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/02_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 11: Classes & Instances (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this second part of the chapter, we learn how we make our `Vector` and `Matrix` instances behave like Python's built-in sequence types, for example, `list` or `tuple` objects." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Sequence Emulation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As discussed in detail in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb#Collections-vs.-Sequences), a sequence is any finite and iterable container type with a *predictable* order of its elements such that we can label each element with an index in the range `0 <= index < len(sequence)`.\n", + "\n", + "To make `Vector` and `Matrix` instances emulate sequences, we implement the `.__len__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__len__)) and `.__getitem__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__getitem__)) methods. While the former returns the total number of elements in a container and is automatically invoked on any object passed to the built-in [len() ](https://docs.python.org/3/library/functions.html#len) function, the latter is invoked by the interpreter behind the scenes when we use the indexing operator `[]`.\n", + "\n", + "In the example, both `.__len__()` and `.__getitem__()` delegate parts of the work to the embedded `list` object named `._entries`. This is a design principle known as [delegation ](https://en.wikipedia.org/wiki/Delegation_%28object-oriented_programming%29) in software engineering. Also, we implicitly invoke the `.__len__()` method inside the `.__init__()` method already via the `len(self)` expression. This reuses code and also ensures that we calculate the number of entries in one way only within the entire class." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(float(x) for x in data)\n", + " if len(self) == 0:\n", + " raise ValueError(\"a vector must have at least one entry\")\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self._entries)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __len__(self):\n", + " return len(self._entries)\n", + "\n", + " def __getitem__(self, index):\n", + " return self._entries[index]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, we may obtain the number of elements with [len() ](https://docs.python.org/3/library/functions.html#len) and index into `Vector` instances." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(v)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Negative indexes work \"out of the box\" because of the delegation to the internal `list` object." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v[-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Somehow \"magically\" we can loop over `v` with a `for` statement. This works as Python simply loops over the indexes implied by `len(v)` and obtains the entries one by one." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0 2.0 3.0 " + ] + } + ], + "source": [ + "for entry in v:\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`v` may also be looped over in reverse order with the [reversed() ](https://docs.python.org/3/library/functions.html#reversed) built-in." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.0 2.0 1.0 " + ] + } + ], + "source": [ + "for entry in reversed(v):\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Membership testing with the `in` operator also comes \"for free.\" Here, Python compares the object to be searched to each element with the `==` operator and stops early once one compares equal. That constitutes a [linear search ](https://en.wikipedia.org/wiki/Linear_search) as seen before." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 in v" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "99 in v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So far, indexing is a *read-only* operation." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "'Vector' object does not support item assignment", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mv\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\u001b[43m]\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m99\u001b[39m\n", + "\u001b[0;31mTypeError\u001b[0m: 'Vector' object does not support item assignment" + ] + } + ], + "source": [ + "v[0] = 99" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because a `Matrix` is two-dimensional, we must decide how we *flatten* the `._entries`. We *choose* to loop over the first row, then the second row, and so on. This is called a **[row major approach ](https://en.wikipedia.org/wiki/Row-_and_column-major_order)**.\n", + "\n", + "In addition to indexing by `int` objects, we also implement indexing by 2-`tuple`s of `int`s where the first element indicates the row and the second the column. Deciding what to do inside a method depending on the *type* of an argument is known as **type dispatching**. We achieve that with the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function.\n", + "\n", + "Lastly, we ensure that integer indexing also works with negative values as we are used to from sequences in general.\n", + "\n", + "Note how all of the methods work together:\n", + "- `.__init__()`, `.__len__()`, and `.__getitem__()` reuse the `.n_rows` and `.n_cols` properties, and\n", + "- `.__init__()` and `.__getitem__()` invoke `.__len__()` via the `len(self)` expressions." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " for row in self._entries[1:]:\n", + " if len(row) != self.n_cols:\n", + " raise ValueError(\"rows must have the same number of entries\")\n", + " if len(self) == 0:\n", + " raise ValueError(\"a matrix must have at least one entry\")\n", + "\n", + " @property\n", + " def n_rows(self):\n", + " return len(self._entries)\n", + "\n", + " @property\n", + " def n_cols(self):\n", + " return len(self._entries[0])\n", + "\n", + " def __len__(self):\n", + " return self.n_rows * self.n_cols\n", + "\n", + " def __getitem__(self, index):\n", + " if isinstance(index, int):\n", + " if index < 0:\n", + " index += len(self)\n", + " if not (0 <= index < len(self)):\n", + " raise IndexError(\"integer index out of range\")\n", + " row, col = divmod(index, self.n_cols)\n", + " return self._entries[row][col]\n", + " elif (\n", + " isinstance(index, tuple) and len(index) == 2\n", + " and isinstance(index[0], int) and isinstance(index[1], int)\n", + " ):\n", + " return self._entries[index[0]][index[1]]\n", + " raise TypeError(\"index must be either an int or a tuple of two int's\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now, we may use a `Matrix` instance just like any other sequence ..." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(m)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m[0] # entry in the upper left corner" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m[-1] # entry in the lower right corner" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... but also index in the two dimensions separately." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m[0, 2] # first row, third column" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9.0" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m[-1, -1] # last row, last column / lower right corner" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As before, Python figures out the iteration on its own ..." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 " + ] + } + ], + "source": [ + "for entry in m:\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9.0 8.0 7.0 6.0 5.0 4.0 3.0 2.0 1.0 " + ] + } + ], + "source": [ + "for entry in reversed(m):\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... and makes the `in` operator do a linear search." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 in m" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "99 in m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### The Python Data Model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sequence emulation itself is *not* a property of object-oriented languages in general. Instead, it is a behavior any data type may or may not exhibit in Python.\n", + "\n", + "The collection of all such behaviors a programming language offers is commonly referred to as its **object model**. In Python, the term **data model** is used instead and all possible behaviors are documented in the [language reference ](https://docs.python.org/3/reference/datamodel.html), in particular, in the section on special methods. We can think of the data model as the collection of all the behaviors we can make our user-defined data types follow. Pythonistas also use the term **protocol** instead of behavior, for example, we may say that \"the `Vector` and `Matrix` classes follow the sequence protocol.\"\n", + "\n", + "So, merely defining the *two* `.__len__()` and `.__getitem__()` methods is enough to make instances of any user-defined type behave like the built-in sequences in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb). Yet, there we defined sequences as all objects having the *four* properties of being finite, iterable, and ordered container types. And, these properties correspond to special methods by the names of `.__len__()`, `.__iter__()`, `.__reversed__()`, and `.__contains__()` as we see in the next section. Thus, Python \"magically\" knows how to derive the logic for the `.__iter__()`, `.__reversed__()`, and `.__contains__()` methods from the combination of the `.__len__()` and `.__getitem__()` methods. In general, while some special methods are related, others are not. Understanding these relationships means understanding the Python data model and vice versa. That is what every aspiring data scientist should aim for.\n", + "\n", + "On the contrary, we could also look at special methods individually. Whereas `.__len__()` is invoked on the object passed to [len() ](https://docs.python.org/3/library/functions.html#len), Python \"translates\" the indexing operator applied on any name like `a[i]`, for example, into the method invocation `a.__getitem__(i)`. So, in both cases, the special methods are executed according to a deterministic rule of the language. In that sense, they act as some sort of syntactic sugar. Thus, they even work if only one of them is defined. For example, without `.__len__()`, iteration with a `for`-loop still works but only in forward direction." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### More on Iteration" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "When implementing the sequence protocol for our `Matrix` class, we had to make the assumption that the user of our class wants to loop over the entries in a rows first fashion. While such assumptions can often be justified by referring to popular conventions (e.g., mathematicians usually look at matrices also in a \"row by column\" way), we could instead provide several iteration methods such that the user may choose one, just like `dict` objects come with several built-in methods that provide iteration.\n", + "\n", + "In the revised `Matrix` class below, we add the `.rows()`, `.cols()`, and `.entries()` methods that return `generator`s providing different and memory efficient ways of looping over the entries. `.rows()` and `.cols()` sequentially produce `Vector` instances representing individual rows and columns. This is in line with a popular idea in linear algebra to view a matrix as a collection of either row or column vectors. Further, `.entries()` by default produces the entries in the matrix one by one in a flat and row major fashion. Called with the optional `row_major=False` flag, it does the same in a column major fashion. The optional `reverse=True` flag allows iteration in backwards order.\n", + "\n", + "We also implement the `.__iter__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__iter__)) and `.__reversed__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__reversed__)) methods that immediately forward invocation to `.entries()`. So, Python does not need to fall back to `.__len__()` and `.__getitem__()` as we described above." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " @property\n", + " def n_rows(self):\n", + " return len(self._entries)\n", + "\n", + " @property\n", + " def n_cols(self):\n", + " return len(self._entries[0])\n", + "\n", + " def rows(self):\n", + " return (Vector(r) for r in self._entries)\n", + "\n", + " def cols(self):\n", + " return (\n", + " Vector(self._entries[r][c] for r in range(self.n_rows)) for c in range(self.n_cols)\n", + " )\n", + "\n", + " def entries(self, *, reverse=False, row_major=True):\n", + " if reverse:\n", + " rows, cols = (range(self.n_rows - 1, -1, -1), range(self.n_cols - 1, -1, -1))\n", + " else:\n", + " rows, cols = range(self.n_rows), range(self.n_cols)\n", + " if row_major:\n", + " return (self._entries[r][c] for r in rows for c in cols)\n", + " return (self._entries[r][c] for c in cols for r in rows)\n", + "\n", + " def __iter__(self):\n", + " return self.entries()\n", + "\n", + " def __reversed__(self):\n", + " return self.entries(reverse=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The revised version of `Vector` below also works without `.__len__()` and `.__getitem__()` methods and leaves the creation of memory efficient `generator`s up to the embedded `list` object in `._entries` by using the built-in [iter() ](https://docs.python.org/3/library/functions.html#iter) and [reversed() ](https://docs.python.org/3/library/functions.html#reversed) functions. Also, `.__repr__()` now relies on the sequence protocol as the instance loops over \"itself\" with `for x in self`, a subtle reuse of code again. " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __iter__(self):\n", + " return iter(self._entries)\n", + "\n", + " def __reversed__(self):\n", + " return reversed(self._entries)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Iteration works as before ..." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 " + ] + } + ], + "source": [ + "for entry in m:\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9.0 8.0 7.0 6.0 5.0 4.0 3.0 2.0 1.0 " + ] + } + ], + "source": [ + "for entry in reversed(m):\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... but now we have some ways of customizing it." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vector((1.0, 2.0, 3.0)) Vector((4.0, 5.0, 6.0)) Vector((7.0, 8.0, 9.0)) " + ] + } + ], + "source": [ + "for row_vector in m.rows():\n", + " print(row_vector, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vector((1.0, 4.0, 7.0)) Vector((2.0, 5.0, 8.0)) Vector((3.0, 6.0, 9.0)) " + ] + } + ], + "source": [ + "for col_vector in m.cols():\n", + " print(col_vector, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 " + ] + } + ], + "source": [ + "for entry in m.entries():\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.0 4.0 7.0 2.0 5.0 8.0 3.0 6.0 9.0 " + ] + } + ], + "source": [ + "for entry in m.entries(row_major=False):\n", + " print(entry, end=\" \")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Mutability vs. Immutability" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the above implementations, the instance attribute `._entries` on a `Vector` or `Matrix` instance references either a `list` or a `list` of row `list`s , which is by the convention of the leading underscore `_` an implementation detail. If users of our classes adhere to this convention, `Vector` and `Matrix` instances can be regarded as *immutable*.\n", + "\n", + "In line with the implied immutability, we implemented the `.transpose()` method such that it returns a *new* `Matrix` instance. Instead, we could make the method change the internal `self._entries` attribute *in place* as we do in the next example. To indicate this mutation to the user of the `Matrix` class clearly, the revised `.transpose()` method returns `None`. That mirrors, for example, how the mutating methods of the built-in `list` type behave (cf., [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb#List-Methods)).\n", + "\n", + "Such decisions are better made consciously when designing a custom data type. The main trade-off is that immutable data types are typically easier to reason about when reading code whereas mutable data types tend to be more memory efficient and make programs faster as less copying operations take place in memory. However, this trade-off only becomes critical when we deal with big amounts of data." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " def transpose(self):\n", + " self._entries = list(list(float(x) for x in r) for r in (zip(*self._entries)))\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Transposing `m` has *no* cell output ..." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "m.transpose()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... so we must look at `m` again." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 4.0, 7.0,), (2.0, 5.0, 8.0,), (3.0, 6.0, 9.0,)))" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A downside of returning `None` is that we can *not* chain repeated invocations of `.transpose()`." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "'NoneType' object has no attribute 'transpose'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[37], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtranspose\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtranspose\u001b[49m()\n", + "\u001b[0;31mAttributeError\u001b[0m: 'NoneType' object has no attribute 'transpose'" + ] + } + ], + "source": [ + "m.transpose().transpose()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Enabling Method Chaining" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To fix the missing method chaining, we end the `.transpose()` method with `return self`, which returns a reference to the instance on which the method is invoked." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(list(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " def __iter__(self): # adapted for brevity; uses parts of entries()\n", + " rows, cols = range(len(self._entries)), range(len(self._entries[0]))\n", + " return (self._entries[r][c] for r in rows for c in cols)\n", + "\n", + " def transpose(self):\n", + " self._entries = list(list(float(x) for x in r) for r in (zip(*self._entries)))\n", + " return self" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The downside of this approach is that a user may unknowingly end up with *two* references to the *same* instance. That can only be mitigated by clear documentation." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "n = m.transpose()" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 4.0, 7.0,), (2.0, 5.0, 8.0,), (3.0, 6.0, 9.0,)))" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 4.0, 7.0,), (2.0, 5.0, 8.0,), (3.0, 6.0, 9.0,)))" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m is n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### More on Indexing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Analogous to the `.__getitem__()` method above, there are also the `.__setitem__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__setitem__)) and `.__delitem__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__delitem__)) methods that assign a new element to or delete an existing element from a sequence.\n", + "\n", + "Whereas deleting an individual entry in a `Vector` or `Matrix` instance may *not* really make sense semantically, we interpret this as setting the corresponding entry to \"unknown\" (i.e., `NaN`). Also, we implement changing individual entries via index assignment. Here, `.__setitem__()` delegates the assignment to the embedded `list` object after casting the assigned value as a `float`. While the example below only allows indexing by an integer, it could be generalized to slicing as well." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = list(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __getitem__(self, index):\n", + " return self._entries[index]\n", + "\n", + " def __setitem__(self, index, value):\n", + " self._entries[index] = float(value)\n", + "\n", + " def __delitem__(self, index):\n", + " self._entries[index] = float(\"NaN\")" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`v` can now be changed in place." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "del v[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((nan, 2.0, 3.0))" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "v[0] = 99" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((99.0, 2.0, 3.0))" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "After this discussion of mutable `Vector` and `Matrix` classes, we continue with immutable implementations in the rest of this chapter. To lower the chance that we accidently design parts of our classes to be mutable, we replace the built-in [list() ](https://docs.python.org/3/library/functions.html#func-list) constructor with [tuple() ](https://docs.python.org/3/library/functions.html#func-tuple) in the `.__init__()` methods. As we learn in [Chapter 7 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb#Tuples-are-like-%22Immutable-Lists%22), `tuple`s are like immutable `list`s." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Polymorphism" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A function is considered **polymorphic** if it can work with *different* data types. The main advantage is reuse of the function's code. Polymorphism goes hand in hand with the concept of [duck typing ](https://en.wikipedia.org/wiki/Duck_typing), first mentioned in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb#Duck-Typing) in the context of input validation.\n", + "\n", + "We know polymorphic functions already: The built-in [sum() ](https://docs.python.org/3/library/functions.html#sum) function is a trivial example that works with all kinds of `iterable` arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum((1, 2, 3, 4))" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum([1, 2, 3, 4])" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum({1, 2, 3, 4})" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum({1: 996, 2: 997, 3: 998, 4: 999}) # loops over the keys" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As we implemented the `Vector` and `Matrix` classes to be iterable, we may pass them to [sum() ](https://docs.python.org/3/library/functions.html#sum) as well." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10.0" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(Vector([1, 2, 3, 4]))" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "10.0" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(Matrix([(1, 2), (3, 4)]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A polymorphic function with a semantic meaning in the context of linear algebra would be one that calculates the [Euclidean norm ](https://en.wikipedia.org/wiki/Norm_%28mathematics%29#Euclidean_norm) for vectors, which is a generalization of the popular [Pythagorean theorem ](https://en.wikipedia.org/wiki/Pythagorean_theorem). Extending the same kind of computation to a matrix results in the even more general [Frobenius norm ](https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm):" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "$$\\lVert \\bf{X} \\rVert_F = \\sqrt{ \\sum_{i=1}^m \\sum_{j=1}^n x_{ij}^2 }$$" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `norm()` function below can handle both a `Vector` or a `Matrix` instance and is therefore polymorphic. In this sense, `Vector` and `Matrix` instances \"walk\" and \"quack\" alike. In particular, they they both can provide all their entries as a flat sequence." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "def norm(vec_or_mat):\n", + " \"\"\"Calculate the Frobenius or Euclidean norm of a matrix or vector.\n", + "\n", + " Args:\n", + " vec_or_mat (Vector / Matrix): object whose entries are squared and summed up\n", + "\n", + " Returns:\n", + " norm (float)\n", + " \"\"\"\n", + " return math.sqrt(sum(x ** 2 for x in vec_or_mat))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "While `norm()` is intended to work with `Vector` or `Matrix` instances ..." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5.477225575051661" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "norm(Vector([1, 2, 3, 4]))" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5.477225575051661" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "norm(Matrix([(1, 2), (3, 4)]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... it also works for any sequence of numbers." + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5.477225575051661" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "norm([1, 2, 3, 4])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "An important criterion if different classes are compatible in the sense that the same polymorphic function can work with them is that they implement the same **interface**.\n", + "\n", + "Whereas many other programming languages formalize this [concept ](https://en.wikipedia.org/wiki/Protocol_%28object-oriented_programming%29), in Python the term refers to the loose idea that different classes define the same attributes and implement the various protocols behind the special methods in a consistent way. This is what it means to \"walk\" and \"quack\" alike." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/11_classes/03_content.ipynb b/11_classes/03_content.ipynb new file mode 100644 index 0000000..8689e63 --- /dev/null +++ b/11_classes/03_content.ipynb @@ -0,0 +1,2413 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/03_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 11: Classes & Instances (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The implementations of our `Vector` and `Matrix` classes so far do not know any of the rules of [linear algebra ](https://en.wikipedia.org/wiki/Linear_algebra). In this third part of the chapter, we add a couple of typical vector and matrix operations, for example, vector addition or matrix-vector multiplication, and some others. Before we do so, we briefly talk about how we can often model the *same* underlying data with a *different* data type." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Representations of Data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "> \"If you change the way you look at things, the things you look at change.\"\n", + "> -- philosopher and personal coach [Dr. Wayne Dyer ](https://en.wikipedia.org/wiki/Wayne_Dyer)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Sometimes, it is helpful to view a `Vector` as a `Matrix` with either one row or one column. On the contrary, such a `Matrix` can always be interpreted as a `Vector` again. Changing the representation of the same underlying data (i.e., the `_entries`) can be viewed as \"changing\" an object's data type, for which, however, there is no built-in syntax.\n", + "\n", + "Thus, we implement the `.as_matrix()` and `.as_vector()` methods below that create *new* `Matrix` or `Vector` instances out of existing `Vector` or `Matrix` instances, respectively. Internally, both methods rely on the sequence protocol again (i.e., `for x in self`). Also, `.as_matrix()` interprets the `Vector` instance as a column vector by default (i.e., the `column=True` flag)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = tuple(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __iter__(self):\n", + " return iter(self._entries)\n", + "\n", + " def as_matrix(self, *, column=True):\n", + " if column:\n", + " return Matrix([x] for x in self)\n", + " return Matrix([(x for x in self)])" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = tuple(tuple(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " @property\n", + " def n_rows(self):\n", + " return len(self._entries)\n", + "\n", + " @property\n", + " def n_cols(self):\n", + " return len(self._entries[0])\n", + "\n", + " def __iter__(self): # adapted for brevity; uses parts of .entries()\n", + " return (self._entries[r][c] for r in range(self.n_rows) for c in range(self.n_cols))\n", + "\n", + " def as_vector(self):\n", + " if not (self.n_rows == 1 or self.n_cols == 1):\n", + " raise RuntimeError(\"one dimension (m or n) must be 1\")\n", + " return Vector(x for x in self)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's interpret `v` as a column vector and create a matrix with dimension $3 \\times 1$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = v.as_matrix()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0,), (2.0,), (3.0,)))" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(3, 1)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.n_rows, m.n_cols" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By chaining `.as_matrix()` and `.as_vector()` we get a *new* `Vector` instance back that is equivalent to the given `v`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v.as_matrix().as_vector()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the same way, we can also interpret `v` as a row vector and create a $1 \\times 3$ matrix." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = v.as_matrix(column=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,)))" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, 3)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.n_rows, m.n_cols" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v.as_matrix(column=False).as_vector()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Interpreting a matrix as a vector only works if one of the two dimensions, $m$ or $n$, is $1$. If this requirement is not satisfied, we get the `RuntimeError` raised in `.as_vector()` above." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "one dimension (m or n) must be 1", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[14], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mas_vector\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[2], line 24\u001b[0m, in \u001b[0;36mMatrix.as_vector\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mas_vector\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 23\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mn_rows \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mn_cols \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m1\u001b[39m):\n\u001b[0;32m---> 24\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mone dimension (m or n) must be 1\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 25\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Vector(x \u001b[38;5;28;01mfor\u001b[39;00m x \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m)\n", + "\u001b[0;31mRuntimeError\u001b[0m: one dimension (m or n) must be 1" + ] + } + ], + "source": [ + "m.as_vector()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Operator Overloading" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "By implementing special methods such as `.__add__()`, `.__sub__()`, `.__mul__()`, and some others, we can make user-defined data types emulate how numeric types operate with each other (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types)): Then, `Vector` and `Matrix` instances can be added together, subtracted from one another, or be multiplied together. We use them to implement the arithmetic rules from linear algebra.\n", + "\n", + "The OOP concept behind this is **[operator overloading ](https://en.wikipedia.org/wiki/Operator_overloading)** as first mentioned in the context of `str` concatenation in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb#Operator-Overloading)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Arithmetic Operators" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To understand the protocol behind arithmetic operators, we first look at the simple case of how an `int` object and a `float` object are added. The expression `1 + 2.0` is \"translated\" by Python into a method invocation of the form `1.__add__(2.0)`. This is why all the special methods behind binary operators take two arguments, `self` and, by convention, `other`. To allow binary operators to work with objects of *different* data types, Python expects the `.__add__()` method on the `1` object to return `NotImplemented` if it does not know how to deal with the `2.0` object and then proceeds by invoking the *reverse* special method `2.0.__radd__(1)`. With this protocol, one can create *new* data types that know how to execute arithmetic operations with *existing* data types *without* having to change the latter. By convention, the result of a binary operation should always be a *new* instance object and *not* a mutation of an existing one." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 + 2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Before implementing the arithmetic operators, we must first determine what other data types are allowed to interact with our `Vector` and `Matrix` instances and also how the two interact with each other. Conceptually, this is the same as to ask how strict we want the rules from linear algebra to be enforced in our model world. For example, while it is obvious that two vectors with the same number of entries may be added or subtracted, we could also allow a scalar value to be added to a vector. That seems awkward at first because it is an illegal operation in linear algebra. However, for convenience in our programs, we could interpret any scalar as a \"constants\" vector of the \"right size\" and add it to each entry in a `Vector`. This idea can be generalized into what is called **[broadcasting](https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html)** in [numpy](https://docs.scipy.org/doc/numpy/index.html). We often see \"dirty hacks\" like this in code. They are no bugs but features supposed to make the user of a library more productive.\n", + "\n", + "In this chapter, we model the following binary arithmetic operations:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "- **Addition** / **Subtraction**\n", + " - `Vector` with `Vector` (if number of entries match; commutative)\n", + " - `Matrix` with `Matrix` (if dimensions $m$ and $n$ match; commutative)\n", + " - `Matrix` / `Vector` with scalar (the scalar is broadcasted; non-commutative for subtraction)\n", + "- **Multiplication**\n", + " - `Vector` with `Vector` ([dot product ](https://en.wikipedia.org/wiki/Dot_product) if number of entries match; commutative)\n", + " - `Matrix` with `Vector` (if dimensions are compatible; vector interpreted as column vector; non-commutative)\n", + " - `Vector` with `Matrix` (if dimensions are compatible; vector interpreted as row vector; non-commutative)\n", + " - `Matrix` / `Vector` with scalar ([scalar multiplication ](https://en.wikipedia.org/wiki/Scalar_multiplication); commutative)\n", + " - `Matrix` with `Matrix` ([matrix-matrix multiplication ](https://en.wikipedia.org/wiki/Matrix_multiplication) if dimensions are compatible; generally non-commutative)\n", + "- **Division**\n", + " - `Matrix` / `Vector` by a scalar (inverse of [scalar multiplication ](https://en.wikipedia.org/wiki/Scalar_multiplication); non-commutative)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "This listing shows the conceptual complexity behind the task of writing a \"little\" linear algebra library. Not to mention that some of the operations are [commutative ](https://en.wikipedia.org/wiki/Commutative_property) while others are not.\n", + "\n", + "As the available special methods correspond to the high-level grouping in the listing, we must implement a lot of **type dispatching** within them. This is why you see the built-in [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) function in most of the methods below. We use it to check if the `other` argument passed in is a `Vector` or `Matrix` instance or a scalar." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(m, Vector)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(v, Vector)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To check if `other` is a scalar, we need to specify what data type constitutes a scalar. We use a goose typing strategy as explained in [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/02_content.ipynb#Goose-Typing): Any object that behaves like a `numbers.Number` from the [numbers ](https://docs.python.org/3/library/numbers.html) module in the [standard library ](https://docs.python.org/3/library/index.html) is considered a scalar.\n", + "\n", + "For example, the integer `1` is an instance of the built-in `int` type. At the same time, [isinstance() ](https://docs.python.org/3/library/functions.html#isinstance) also confirms that it is a `numbers.Number` in the abstract sense." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1, int)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "import numbers" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(1, numbers.Number)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Now with all the preparation work done, let's look at a \"minimal\" implementation of `Vector` that supports all the arithmetic operations specified above. *None* of the special methods inside the `Vector` class is aware that the `Matrix` class exists! Thus, all operations involving at least one `Matrix` instance are implemented only in the `Matrix` class." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "code_folding": [], + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = tuple(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __len__(self):\n", + " return len(self._entries)\n", + "\n", + " def __iter__(self):\n", + " return iter(self._entries)\n", + "\n", + " def __add__(self, other):\n", + " if isinstance(other, Vector): # vector addition\n", + " if len(self) != len(other):\n", + " raise ValueError(\"vectors must be of the same length\")\n", + " return Vector(x + y for (x, y) in zip(self, other))\n", + " elif isinstance(other, numbers.Number): # broadcasting addition\n", + " return Vector(x + other for x in self)\n", + " return NotImplemented\n", + "\n", + " def __radd__(self, other):\n", + " return self + other\n", + "\n", + " def __sub__(self, other):\n", + " if isinstance(other, Vector): # vector subtraction\n", + " if len(self) != len(other):\n", + " raise ValueError(\"vectors must be of the same length\")\n", + " return Vector(x - y for (x, y) in zip(self, other))\n", + " elif isinstance(other, numbers.Number): # broadcasting subtraction\n", + " return Vector(x - other for x in self)\n", + " return NotImplemented\n", + "\n", + " def __rsub__(self, other):\n", + " # Reverse vector subtraction is already handled in __sub__().\n", + " if isinstance(other, numbers.Number): # broadcasting subtraction\n", + " return Vector(other - x for x in self)\n", + " return NotImplemented\n", + "\n", + " def __mul__(self, other):\n", + " if isinstance(other, Vector): # dot product\n", + " if len(self) != len(other):\n", + " raise ValueError(\"vectors must be of the same length\")\n", + " return sum(x * y for (x, y) in zip(self, other))\n", + " elif isinstance(other, numbers.Number): # scalar multiplication\n", + " return Vector(x * other for x in self)\n", + " return NotImplemented\n", + "\n", + " def __rmul__(self, other):\n", + " return self * other\n", + "\n", + " def __truediv__(self, other):\n", + " if isinstance(other, numbers.Number):\n", + " return self * (1 / other)\n", + " return NotImplemented\n", + "\n", + " def as_matrix(self, *, column=True):\n", + " if column:\n", + " return Matrix([x] for x in self)\n", + " return Matrix([(x for x in self)])" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "w = Vector([4, 5])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`.__mul__()` implements both scalar multiplication and the dot product of two `Vector`s. As both operations are commutative, `.__rmul__()` dispatches to `.__mul__()` via the `self * other` expression." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((2.0, 4.0, 6.0))" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "2 * v" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((3.0, 6.0, 9.0))" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v * 3" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "14.0" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v * v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If two `Vector`s do *not* have a matching number of entries, a `ValueError` is raised." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "vectors must be of the same length", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[27], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mv\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mw\u001b[49m\n", + "Cell \u001b[0;32mIn[21], line 47\u001b[0m, in \u001b[0;36mVector.__mul__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 45\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, Vector): \u001b[38;5;66;03m# dot product\u001b[39;00m\n\u001b[1;32m 46\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mlen\u001b[39m(other):\n\u001b[0;32m---> 47\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mvectors must be of the same length\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 48\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28msum\u001b[39m(x \u001b[38;5;241m*\u001b[39m y \u001b[38;5;28;01mfor\u001b[39;00m (x, y) \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(\u001b[38;5;28mself\u001b[39m, other))\n\u001b[1;32m 49\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, numbers\u001b[38;5;241m.\u001b[39mNumber): \u001b[38;5;66;03m# scalar multiplication\u001b[39;00m\n", + "\u001b[0;31mValueError\u001b[0m: vectors must be of the same length" + ] + } + ], + "source": [ + "v * w" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`.__truediv__()` implements the ordinary division operator `/` while `.__floordiv__()` would implement the integer division operator `//`. Here, `.__truediv__()` dispatches to `.__mul__()` after inverting the `other` argument via the `self * (1 / other)` expression." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((0.3333333333333333, 0.6666666666666666, 1.0))" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v / 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`.__add__()` and `.__sub__()` implement vector addition and subtraction according to standard linear algebra rules, meaning that both `Vector`s must have the same number of entries or a `ValueError` is raised. Furthermore, both methods are able to broadcast the `other` argument to the dimension of a `Vector` and then execute either vector addition or subtraction. As addition is commutative, `.__radd__()` dispatches to `.__add__()`. For now, we have to explicitly implement `.__rsub__()`. Further below, we see how it can be re-factored to be commutative." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((2.0, 4.0, 6.0))" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v + v" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((0.0, 0.0, 0.0))" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v - v" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "vectors must be of the same length", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[31], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mv\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mw\u001b[49m\n", + "Cell \u001b[0;32mIn[21], line 20\u001b[0m, in \u001b[0;36mVector.__add__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, Vector): \u001b[38;5;66;03m# vector addition\u001b[39;00m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mlen\u001b[39m(other):\n\u001b[0;32m---> 20\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mvectors must be of the same length\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Vector(x \u001b[38;5;241m+\u001b[39m y \u001b[38;5;28;01mfor\u001b[39;00m (x, y) \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(\u001b[38;5;28mself\u001b[39m, other))\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, numbers\u001b[38;5;241m.\u001b[39mNumber): \u001b[38;5;66;03m# broadcasting addition\u001b[39;00m\n", + "\u001b[0;31mValueError\u001b[0m: vectors must be of the same length" + ] + } + ], + "source": [ + "v + w" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((101.0, 102.0, 103.0))" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v + 100" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((99.0, 98.0, 97.0))" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "100 - v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For `Matrix` instances, the implementation is a bit more involved as we need to distinguish between matrix-matrix, matrix-vector, vector-matrix, and scalar multiplication and check for compatible dimensions. To review the underlying rules, check this [article ](https://en.wikipedia.org/wiki/Matrix_multiplication) or watch the video below." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAUDBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIChALCAgOCggIDRUNDhERExMTCAsWGBYSGBASExIBBQUFCAcIDwkJDhIODw8SEhIVEhISFRISEhISEhIVEhISFhISEhISEhISEhISEhISEhISEhISEhISEhISEhIeEv/AABEIAWgB4AMBIgACEQEDEQH/xAAdAAEAAgIDAQEAAAAAAAAAAAAABQgGBwEDBAIJ/8QAURAAAQQBAgMCCQcIBggGAgMAAQACAwQFBhESEyEHMQgUFSI0QVF0swkyVGFxktQjNkJSdYGRtCQ1cqGxwRY3Q2J2grK1M1Njc4PRRKIXJWT/xAAbAQEBAQEBAQEBAAAAAAAAAAAAAgMBBAYHBf/EADMRAQACAQMBBQYEBgMAAAAAAAABEQIDITESBEFRYXEFMoGRsfAGE9HxFCIjQsHhM1Jy/9oADAMBAAIRAxEAPwCmSIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICKXt4ZkUkkT71QPje6NwDbxHExxa7Yir1G4K6vJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VPJ0P0+p9y9+FQRqKS8nQ/T6n3L34VdtTDMlkjiZeqF8j2xtBbeA4nuDW7k1eg3IQdWqPTrvvdn4z1GqS1R6dd97s/Geo1AREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQFJaX9Ope91vjMUapLS/p1L3ut8ZiBqj06773Z+M9RqktUenXfe7PxnqNQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBSWl/TqXvdb4zFGqS0v6dS97rfGYgao9Ou+92fjPUapLVHp133uz8Z6jUBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAUlpf06l73W+MxRqktL+nUve63xmIGqPTrvvdn4z1GqS1R6dd97s/Geo1AREQEREBERAREQEREBERBy0E9y7Z6ksY3fHIwb7bvY5o39gJHepDRx//saHvtX48a/VztR0dVz+Ju4m2PydyEsa/YOdBMNnQWGD9eOQMd9fDsehKD8jUUrq3AWcXet464wx2qc8tednXbjicW8TCQOOJw2c13c5rmkdCvPgcVYvWq9OrG6azamjrwRN24pJZXhkbBv0G7iOp6DvQdENSV+/BG94HQljHOAPsJaOhXU4EEgggg7EHoQR3gj2r9Z+xrQ1fTeFpYivwuNePexM0bGzbk8+zYO/XZ0hdsCT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uDm9CNd9ofbzPb1fV1Xiq7qctavXhFew4StlayORliKXl7cUMjZXs6bEDYjY7baURBbzJeFNpmR7cu3SLHakZG3l2pxUfHFM1vAx/jgbz3tYOgPLa7YbAt7xqjth7cp8vqPF6lx0L8fcxtKpCGvc2RpsQTWZpSAO+u/xlzOE9S3ffvWmkQW4n8JnSWQMORzOjmWc3CxoEzWU54XOi6xnnz7SBgPUB7Hlm/Qnbc6u7Ou2etjdZWdUSYpsUFnxvfHUHhrYTZjDd2PlGznFwL3HZoLpHEBo2aNMIg2/gO1yvW12/Vzqczq7rt2z4mJGc7htVJ6zW8wjg4gZg4/2SFzle12vNrxurhTmFYXK1nxMyM5/DBUiqlvMA4OImMu/eAtPog2x2r64l1bq+DI4mCWpZtz4yrRjkkY6VtthhhgfxNHCN5uEjvVlPlB9eOp4ejgGSDxnKPbYu8HQeKVHNcBw77tbLa4SO/pWkCp92Sa0dp7L1cuypBclp810UNhz2x8ySJ8Qk3jIPE3jJH1gH1Ar0dsvaHc1RlpstcayJ744YYoIi4xV4YWcLY4y/wA7YvMkh3/Sld9iDDEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAUlpf06l73W+MxRqktL+nUve63xmIGqPTrvvdn4z1Grvv2XTSyzOADpZHyODdw0OkcXENBJO259q6EBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAUlpf06l73W+MxRq76Fl0MsUzQC6KRkjQ7ctLo3BwDgCDtuPag6EREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQf/2Q==\n", + "text/html": [ + "\n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import YouTubeVideo\n", + "YouTubeVideo(\"OMA2Mwo0aZg\", width=\"60%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To summarize the video, the multiplication of two matrices $\\bf{A}$ and $\\bf{B}$ with dimensions $m$ by $n$ and $n$ by $p$ yields a matrix $\\bf{C}$ with dimensions $m$ and $p$. To obtain an entry $c_{ij}$ of matrix $\\bf{C}$ where $i$ and $j$ represent the index labels of the rows and columns, we have to calculate the dot product of the $i$th row vector of $\\bf{A}$ with the $j$th column vector of $\\bf{B}$. So, it makes a difference if we multiply $\\bf{A}$ with $\\bf{B}$ from the right or left.\n", + "\n", + "When multiplying a `Matrix` with a `Vector`, we follow the convention that a `Vector` on the left is interpreted as a row vector and a `Vector` on the right as a column vector. The `Vector`'s length must match the `Matrix`'s corresponding dimension. As row and column vectors can be viewed as a `Matrix` as well, matrix-vector multiplication is a special case of matrix-matrix multiplication." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In the revised `Matrix` class, the `.__add__()`, `.__radd__()`, `.__sub__()`, `.__rsub__()`, and `.__truediv__()` methods follow the same logic as in the `Vector` class above.\n", + "\n", + "Besides implementing scalar multiplication, `.__mul__()` and `.__rmul__()` are responsible for converting `Vector`s into `Matrix`s and back. In particular, all the different ways of performing a multiplication are reduced into *one* generic form, which is a matrix-matrix multiplication. That is achieved by the `._matrix_multiply()` method, another implementation detail." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Matrix:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = tuple(tuple(float(x) for x in r) for r in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(\"(\" + \", \".join(repr(c) for c in r) + \",)\" for r in self._entries)\n", + " return f\"Matrix(({args}))\"\n", + "\n", + " @property\n", + " def n_rows(self):\n", + " return len(self._entries)\n", + "\n", + " @property\n", + " def n_cols(self):\n", + " return len(self._entries[0])\n", + "\n", + " def rows(self):\n", + " return (Vector(r) for r in self._entries)\n", + "\n", + " def cols(self):\n", + " return (\n", + " Vector(self._entries[r][c] for r in range(self.n_rows)) for c in range(self.n_cols)\n", + " )\n", + "\n", + " def __iter__(self): # adapted for brevity; uses parts of entries()\n", + " return (self._entries[r][c] for r in range(self.n_rows) for c in range(self.n_cols))\n", + "\n", + " def __add__(self, other):\n", + " if isinstance(other, Matrix): # matrix addition\n", + " if (self.n_rows != other.n_rows) or (self.n_cols != other.n_cols):\n", + " raise ValueError(\"matrices must have the same dimensions\")\n", + " return Matrix((s_col + o_col for (s_col, o_col) in zip(s_row, o_row))\n", + " for (s_row, o_row) in zip(self._entries, other._entries))\n", + " elif isinstance(other, numbers.Number): # broadcasting addition\n", + " return Matrix((c + other for c in r) for r in self._entries)\n", + " return NotImplemented\n", + "\n", + " def __radd__(self, other):\n", + " if isinstance(other, Vector):\n", + " raise TypeError(\"vectors and matrices cannot be added\")\n", + " return self + other\n", + "\n", + " def __sub__(self, other):\n", + " if isinstance(other, Matrix): # matrix subtraction\n", + " if (self.n_rows != other.n_rows) or (self.n_cols != other.n_cols):\n", + " raise ValueError(\"matrices must have the same dimensions\")\n", + " return Matrix((s_col - o_col for (s_col, o_col) in zip(s_row, o_row))\n", + " for (s_row, o_row) in zip(self._entries, other._entries))\n", + " elif isinstance(other, numbers.Number): # broadcasting subtraction\n", + " return Matrix((c - other for c in r) for r in self._entries)\n", + " return NotImplemented\n", + "\n", + " def __rsub__(self, other):\n", + " if isinstance(other, Vector):\n", + " raise TypeError(\"vectors and matrices cannot be subtracted\")\n", + " # Reverse matrix subtraction is already handled in __sub__().\n", + " if isinstance(other, numbers.Number): # broadcasting subtraction\n", + " return Matrix((other - c for c in r) for r in self._entries)\n", + " return NotImplemented\n", + " \n", + " def _matrix_multiply(self, other):\n", + " if self.n_cols != other.n_rows:\n", + " raise ValueError(\"matrices must have compatible dimensions\")\n", + " return Matrix((rv * cv for cv in other.cols()) for rv in self.rows())\n", + "\n", + " def __mul__(self, other):\n", + " if isinstance(other, numbers.Number):\n", + " return Matrix((x * other for x in r) for r in self._entries)\n", + " elif isinstance(other, Vector):\n", + " return self._matrix_multiply(other.as_matrix()).as_vector()\n", + " elif isinstance(other, Matrix):\n", + " return self._matrix_multiply(other)\n", + " return NotImplemented\n", + "\n", + " def __rmul__(self, other):\n", + " if isinstance(other, numbers.Number):\n", + " return self * other\n", + " elif isinstance(other, Vector):\n", + " return other.as_matrix(column=False)._matrix_multiply(self).as_vector()\n", + " return NotImplemented\n", + "\n", + " def __truediv__(self, other):\n", + " if isinstance(other, numbers.Number):\n", + " return self * (1 / other)\n", + " return NotImplemented\n", + "\n", + " def as_vector(self):\n", + " if not (self.n_rows == 1 or self.n_cols == 1):\n", + " raise RuntimeError(\"one dimension (m or n) must be 1\")\n", + " return Vector(x for x in self)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])\n", + "n = Matrix([(10, 11, 12), (13, 14, 15)])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Scalar multiplication, addition, and subtraction work as before." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((10.0, 20.0, 30.0,), (40.0, 50.0, 60.0,), (70.0, 80.0, 90.0,)))" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10 * m" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(2 * m + m * 3) / 5" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((0.0, 0.0, 0.0,), (0.0, 0.0, 0.0,), (0.0, 0.0, 0.0,)))" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m - m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Matrix-matrix multiplication works if the dimensions are compatible ..." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((138.0, 171.0, 204.0,), (174.0, 216.0, 258.0,)))" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n * m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... and results in a `ValueError` otherwise." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "matrices must have compatible dimensions", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[41], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mn\u001b[49m\n", + "Cell \u001b[0;32mIn[35], line 74\u001b[0m, in \u001b[0;36mMatrix.__mul__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 72\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_matrix_multiply(other\u001b[38;5;241m.\u001b[39mas_matrix())\u001b[38;5;241m.\u001b[39mas_vector()\n\u001b[1;32m 73\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, Matrix):\n\u001b[0;32m---> 74\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_matrix_multiply\u001b[49m\u001b[43m(\u001b[49m\u001b[43mother\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 75\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mNotImplemented\u001b[39m\n", + "Cell \u001b[0;32mIn[35], line 65\u001b[0m, in \u001b[0;36mMatrix._matrix_multiply\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_matrix_multiply\u001b[39m(\u001b[38;5;28mself\u001b[39m, other):\n\u001b[1;32m 64\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mn_cols \u001b[38;5;241m!=\u001b[39m other\u001b[38;5;241m.\u001b[39mn_rows:\n\u001b[0;32m---> 65\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmatrices must have compatible dimensions\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 66\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Matrix((rv \u001b[38;5;241m*\u001b[39m cv \u001b[38;5;28;01mfor\u001b[39;00m cv \u001b[38;5;129;01min\u001b[39;00m other\u001b[38;5;241m.\u001b[39mcols()) \u001b[38;5;28;01mfor\u001b[39;00m rv \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrows())\n", + "\u001b[0;31mValueError\u001b[0m: matrices must have compatible dimensions" + ] + } + ], + "source": [ + "m * n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The same holds for matrix-vector and vector-matrix multiplication. These operations always return `Vector` instances in line with standard linear algebra. If a `Vector`'s length is not compatible with the respective dimension of a `Matrix`, we receive a `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((14.0, 32.0, 50.0))" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m * v" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((30.0, 36.0, 42.0))" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v * m" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((68.0, 86.0))" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n * v" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "matrices must have compatible dimensions", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[45], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mv\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mn\u001b[49m\n", + "Cell \u001b[0;32mIn[35], line 81\u001b[0m, in \u001b[0;36mMatrix.__rmul__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 79\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m \u001b[38;5;241m*\u001b[39m other\n\u001b[1;32m 80\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, Vector):\n\u001b[0;32m---> 81\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mother\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mas_matrix\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcolumn\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_matrix_multiply\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241m.\u001b[39mas_vector()\n\u001b[1;32m 82\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mNotImplemented\u001b[39m\n", + "Cell \u001b[0;32mIn[35], line 65\u001b[0m, in \u001b[0;36mMatrix._matrix_multiply\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_matrix_multiply\u001b[39m(\u001b[38;5;28mself\u001b[39m, other):\n\u001b[1;32m 64\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mn_cols \u001b[38;5;241m!=\u001b[39m other\u001b[38;5;241m.\u001b[39mn_rows:\n\u001b[0;32m---> 65\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmatrices must have compatible dimensions\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 66\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Matrix((rv \u001b[38;5;241m*\u001b[39m cv \u001b[38;5;28;01mfor\u001b[39;00m cv \u001b[38;5;129;01min\u001b[39;00m other\u001b[38;5;241m.\u001b[39mcols()) \u001b[38;5;28;01mfor\u001b[39;00m rv \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mrows())\n", + "\u001b[0;31mValueError\u001b[0m: matrices must have compatible dimensions" + ] + } + ], + "source": [ + "v * n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, the broadcasting addition and subtraction also work for our `Matrix` instances." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((101.0, 102.0, 103.0,), (104.0, 105.0, 106.0,), (107.0, 108.0, 109.0,)))" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m + 100" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((99.0, 98.0, 97.0,), (96.0, 95.0, 94.0,), (93.0, 92.0, 91.0,)))" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "100 - m" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We do not allow addition or subtraction of matrices with vectors and raise a `TypeError` instead. Alternatively, we could have implemented broadcasting here as well, just like [numpy](https://www.numpy.org/) does." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "vectors and matrices cannot be added", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[48], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mm\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mv\u001b[49m\n", + "Cell \u001b[0;32mIn[21], line 27\u001b[0m, in \u001b[0;36mVector.__radd__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__radd__\u001b[39m(\u001b[38;5;28mself\u001b[39m, other):\n\u001b[0;32m---> 27\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mother\u001b[49m\n", + "Cell \u001b[0;32mIn[35], line 42\u001b[0m, in \u001b[0;36mMatrix.__radd__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__radd__\u001b[39m(\u001b[38;5;28mself\u001b[39m, other):\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(other, Vector):\n\u001b[0;32m---> 42\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mvectors and matrices cannot be added\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 43\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m \u001b[38;5;241m+\u001b[39m other\n", + "\u001b[0;31mTypeError\u001b[0m: vectors and matrices cannot be added" + ] + } + ], + "source": [ + "m + v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Relational Operators" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As we have seen before, two different `Vector`s with the same `._entries` do *not* compare equal. The reason is that for user-defined types Python by default only assumes two instances to be equal if they are actually the same object. This is, of course, semantically wrong for our `Vector`s and `Matrix`s." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])\n", + "w = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v == w" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v == v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We implement the `.__eq__()` (cf., [reference ](https://docs.python.org/3/reference/datamodel.html#object.__eq__)) method to control how the comparison operator `==` is carried out. For brevity, we show this only for the `Vector` class. The `.__eq__()` method exits early as soon as the first pair of entries does not match. Also, for reasons discussed in [Chapter 5 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/01_content.ipynb#Imprecision), we compare the absolute difference of two corresponding entries to a very small `zero_threshold` that is stored as a class attribute shared among all `Vector` instances. If the `Vector`s differ in their numbers of entries, we fail loudly and raise a `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " zero_threshold = 1e-12\n", + "\n", + " def __init__(self, data):\n", + " self._entries = tuple(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __len__(self):\n", + " return len(self._entries)\n", + "\n", + " def __iter__(self):\n", + " return iter(self._entries)\n", + "\n", + " def __eq__(self, other):\n", + " if isinstance(other, Vector):\n", + " if len(self) != len(other):\n", + " raise ValueError(\"vectors must be of the same length\")\n", + " for x, y in zip(self, other):\n", + " if abs(x - y) > self.zero_threshold:\n", + " return False # exit early if two corresponding entries differ\n", + " return True\n", + " return NotImplemented" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])\n", + "w = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v == w" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Besides `.__eq__()`, there are other special methods to implement the `!=`, `<`, `>`, `<=`, and `>=` operators, the last four of which are not semantically meaningful in the context of linear algebra.\n", + "\n", + "Python implicitly creates the `!=` for us in that it just inverts the result of `.__eq__()`." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])\n", + "w = Vector([1, 2, 4])" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v == w" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v != w" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Number Emulation" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Our `Vector` and `Matrix` classes do not fully behave like a `numbers.Number` in the abstract sense. Besides the not yet talked about but useful unary `+` and `-` operators, numbers in Python usually support being passed to built-in functions like [abs() ](https://docs.python.org/3/library/functions.html#abs), [bin() ](https://docs.python.org/3/library/functions.html#bin), [bool() ](https://docs.python.org/3/library/functions.html#bool), [complex() ](https://docs.python.org/3/library/functions.html#complex), [float() ](https://docs.python.org/3/library/functions.html#float), [hex() ](https://docs.python.org/3/library/functions.html#hex), [int() ](https://docs.python.org/3/library/functions.html#int), and others (cf., the [reference ](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types) for an exhaustive list).\n", + "\n", + "To see that our classes lack simple but expected behaviors, let's try to invert the signs of all entries in the vector `v` ..." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "bad operand type for unary -: 'Vector'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[59], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;241;43m-\u001b[39;49m\u001b[43mv\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: bad operand type for unary -: 'Vector'" + ] + } + ], + "source": [ + "-v" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... or pass `v` to [abs() ](https://docs.python.org/3/library/functions.html#abs)." + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "bad operand type for abs(): 'Vector'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[60], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mabs\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: bad operand type for abs(): 'Vector'" + ] + } + ], + "source": [ + "abs(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For our example, we decide to implement the unary `+` and `-` operators, [abs() ](https://docs.python.org/3/library/functions.html#abs) as the Euclidean / Frobenius norm (i.e., the [magnitude ](https://en.wikipedia.org/wiki/Magnitude_%28mathematics%29#Euclidean_vector_space)), [bool() ](https://docs.python.org/3/library/functions.html#bool) to check if that norm is greater than `0` or not, and [float() ](https://docs.python.org/3/library/functions.html#float) to obtain a single scalar if the vector or matrix consists of only one entry. To achieve that, we implement the `.__pos__()`, `.__neg__()`, `.__abs__()`, `.__bool__()`, and `.__float__()` methods. For brevity, we do this only for the `Vector` class." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import math" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [], + "source": [ + "def norm(vec_or_mat):\n", + " \"\"\"Calculate the Frobenius or Euclidean norm of a matrix or vector.\"\"\"\n", + " return math.sqrt(sum(x ** 2 for x in vec_or_mat))" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "class Vector:\n", + "\n", + " def __init__(self, data):\n", + " self._entries = tuple(float(x) for x in data)\n", + " # ...\n", + "\n", + " def __repr__(self):\n", + " args = \", \".join(repr(x) for x in self)\n", + " return f\"Vector(({args}))\"\n", + "\n", + " def __iter__(self):\n", + " return iter(self._entries)\n", + "\n", + " def __len__(self):\n", + " return len(self._entries)\n", + "\n", + " def __pos__(self):\n", + " return self\n", + "\n", + " def __neg__(self):\n", + " return Vector(-x for x in self)\n", + "\n", + " def __abs__(self):\n", + " return norm(self)\n", + "\n", + " def __bool__(self):\n", + " return bool(abs(self))\n", + "\n", + " def __float__(self):\n", + " if len(self) != 1:\n", + " raise RuntimeError(\"vector must have exactly one entry to become a scalar\")\n", + " return self._entries[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])\n", + "w = Vector([3, 4])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The unary `+` operator is not only conceptually an identity operator but simply returns the instance itself." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "+v" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "+v is v" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((-3.0, -4.0))" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-w" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The magnitude of a `Vector` is only `0` if *all* entries are `0`." + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "5.0" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs(w)" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "z = Vector([0, 0])" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "abs(z)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Only an all `0`s `Vector` is `False` in a boolean context. As mentioned in [Chapter 3 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb#Truthy-vs.-Falsy), commonly we view an *empty* sequence as falsy; however, as we do not allow `Vector`s without any entries, we choose the all `0`s alternative. In that regard, the `Vector` class does not behave like the built-in sequence types." + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(v)" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bool(z)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Lastly, single entry `Vector`s can be casted as scalars." + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "s = Vector([42])" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "42.0" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "float(s)" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "RuntimeError", + "evalue": "vector must have exactly one entry to become a scalar", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[75], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;43mfloat\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[63], line 31\u001b[0m, in \u001b[0;36mVector.__float__\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__float__\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 30\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[0;32m---> 31\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mvector must have exactly one entry to become a scalar\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 32\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_entries[\u001b[38;5;241m0\u001b[39m]\n", + "\u001b[0;31mRuntimeError\u001b[0m: vector must have exactly one entry to become a scalar" + ] + } + ], + "source": [ + "float(v)" + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/11_classes/04_content.ipynb b/11_classes/04_content.ipynb new file mode 100644 index 0000000..684d227 --- /dev/null +++ b/11_classes/04_content.ipynb @@ -0,0 +1,2991 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "**Note**: Click on \"*Kernel*\" > \"*Restart Kernel and Clear All Outputs*\" in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) *before* reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it [in the cloud ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/04_content.ipynb)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 11: Classes & Instances (continued)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In this fourth part of the chapter, we finalize our `Vector` and `Matrix` classes. As both `class` definitions have become rather lengthy, we learn how we to organize them into a Python package and import them in this Jupyter notebook. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Packages vs. Modules" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In [Chapter 2 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/02_content.ipynb#Local-Modules-and-Packages), we introduce the concept of a Python module that is imported with the `import` statement. Essentially, a **module** is a single plain text \\*.py file on disk that contains Python code (e.g., [*sample_module.py* ](https://github.com/webartifex/intro-to-python/blob/main/02_functions/sample_module.py) in [Chapter 2's folder ](https://github.com/webartifex/intro-to-python/tree/main/02_functions)).\n", + "\n", + "Conceptually, a **package** is a generalization of a module whose code is split across several \\*.py to achieve a better organization of the individual parts. The \\*.py files are stored within a folder (e.g., [*sample_package* ](https://github.com/webartifex/intro-to-python/tree/main/11_classes/sample_package) in [Chapter 11's folder ](https://github.com/webartifex/intro-to-python/tree/main/11_classes)). In addition to that, a \"*\\_\\_init\\_\\_.py*\" file that may be empty must be put inside the folder. The latter is what the Python interpreter looks for to decide if a folder is a package or not.\n", + "\n", + "Let's look at an example with the final version of our `Vector` and `Matrix` classes.\n", + "\n", + "`!pwd` shows the location of this Jupyter notebook on the computer you are running [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) on: It is the local equivalent of [Chapter 11's folder ](https://github.com/webartifex/intro-to-python/tree/main/11_classes) in this book's [GitHub repository ](https://github.com/webartifex/intro-to-python)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/home/alexander/Repositories/intro-to-python/11_classes\n" + ] + } + ], + "source": [ + "!pwd" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "`!ls` lists all the files and folders in the current location: These are Chapter 11's Jupyter notebooks (i.e., the \\*.ipynb files) and the [*sample_package* ](https://github.com/webartifex/intro-to-python/tree/main/11_classes/sample_package) folder. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "00_content.ipynb 02_content.ipynb 04_content.ipynb\t06_review.ipynb\n", + "01_exercises.ipynb 03_content.ipynb 05_summary.ipynb\tsample_package\n" + ] + } + ], + "source": [ + "!ls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "If we run `!ls` with the `sample_package` folder as the argument, we see the folder's contents: Four \\*.py files. Alternatively, you can use [JupyterLab' File Browser](https://jupyterlab.readthedocs.io/en/stable/user/interface.html?highlight=file%20browser#left-sidebar) on the left to navigate into the package." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "__init__.py matrix.py\tutils.py vector.py\n" + ] + } + ], + "source": [ + "!ls sample_package" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The package is organized such that the [*matrix.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/matrix.py) and [*vector.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/vector.py) modules each define just one class, `Matrix` and `Vector`. That is intentional as both classes consist of several hundred lines of code and comments.\n", + "\n", + "The [*utils.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/utils.py) module contains code that is shared by both classes. Such code snippets are commonly called \"utilities\" or \"helpers,\" which explains the module's name.\n", + "\n", + "Finally, the [*\\_\\_init\\_\\_.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/__init__.py) file contains mostly meta information and defines what objects should be importable from the package's top level.\n", + "\n", + "With the `import` statement, we can import the entire package just as we would import a module from the [standard library ](https://docs.python.org/3/library/index.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import sample_package as pkg" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The above cell runs the code in the [*\\_\\_init\\_\\_.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/__init__.py) file from top to bottom, which in turn runs the [*matrix.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/matrix.py), [*utils.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/utils.py), and [*vector.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/vector.py) modules (cf., look at the `import` statements in the four \\*.py files to get the idea). As both [*matrix.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/matrix.py) and [*vector.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/vector.py) depend on each other (i.e., the `Matrix` class needs the `Vector` class to work and vice versa), understanding the order in that the modules are executed is not trivial. Without going into detail, we mention that Python guarantees that each \\*.py file is run only once and figures out the order on its own. If Python is unable to do that, for example, due to unresolvable cirular imports, it aborts with an `ImportError`.\n", + "\n", + "Below, `pkg` is an object of type `module` ..." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "module" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(pkg)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... and we use the built-in [dir() ](https://docs.python.org/3/library/functions.html#dir) function to check what attributes `pkg` comes with." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['Matrix',\n", + " 'Vector',\n", + " '__all__',\n", + " '__author__',\n", + " '__builtins__',\n", + " '__cached__',\n", + " '__doc__',\n", + " '__file__',\n", + " '__loader__',\n", + " '__name__',\n", + " '__package__',\n", + " '__path__',\n", + " '__spec__',\n", + " '__version__',\n", + " 'matrix',\n", + " 'utils',\n", + " 'vector']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dir(pkg)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The package's meta information and documentation are automatically parsed from the [*\\_\\_init\\_\\_.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/__init__.py) file." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on package linear_algebra_tools:\n", + "\n", + "NAME\n", + " linear_algebra_tools - This package provides linear algebra functionalities.\n", + "\n", + "DESCRIPTION\n", + " The package is split into three modules:\n", + " - matrix: defines the Matrix class\n", + " - vector: defines the Vector class\n", + " - utils: defines the norm() function that is shared by Matrix and Vector\n", + " and package-wide constants\n", + "\n", + " The classes implement arithmetic operations involving vectors and matrices.\n", + "\n", + " See the docstrings in the modules and classes for further info.\n", + "\n", + "PACKAGE CONTENTS\n", + " matrix\n", + " utils\n", + " vector\n", + "\n", + "CLASSES\n", + " builtins.object\n", + " sample_package.matrix.Matrix\n", + " sample_package.vector.Vector\n", + "\n", + " class Matrix(builtins.object)\n", + " | Matrix(data)\n", + " |\n", + " | An m-by-n-dimensional matrix from linear algebra.\n", + " |\n", + " | All entries are converted to floats, or whatever is set in the typing attribute.\n", + " |\n", + " | Attributes:\n", + " | storage (callable): data type used to store the entries internally;\n", + " | defaults to tuple\n", + " | typing (callable): type casting applied to all entries upon creation;\n", + " | defaults to float\n", + " | vector_cls (vector.Vector): a reference to the Vector class to work with\n", + " | zero_threshold (float): max. tolerance when comparing an entry to zero;\n", + " | defaults to 1e-12\n", + " |\n", + " | Methods defined here:\n", + " |\n", + " | __abs__(self)\n", + " | The Frobenius norm of a Matrix.\n", + " |\n", + " | __add__(self, other)\n", + " | Handle `self + other` and `other + self`.\n", + " |\n", + " | This may be either matrix addition or broadcasting addition.\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(1, 2), (3, 4)]) + Matrix([(2, 3), (4, 5)])\n", + " | Matrix(((3.0, 5.0,), (7.0, 9.0,)))\n", + " |\n", + " | >>> Matrix([(1, 2), (3, 4)]) + 5\n", + " | Matrix(((6.0, 7.0,), (8.0, 9.0,)))\n", + " |\n", + " | >>> 10 + Matrix([(1, 2), (3, 4)])\n", + " | Matrix(((11.0, 12.0,), (13.0, 14.0,)))\n", + " |\n", + " | __bool__(self)\n", + " | A Matrix is truthy if its Frobenius norm is strictly positive.\n", + " |\n", + " | __eq__(self, other)\n", + " | Handle `self == other`.\n", + " |\n", + " | Compare two Matrix instances for equality.\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(1, 2), (3, 4)]) == Matrix([(1, 2), (3, 4)])\n", + " | True\n", + " |\n", + " | >>> Matrix([(1, 2), (3, 4)]) == Matrix([(5, 6), (7, 8)])\n", + " | False\n", + " |\n", + " | __float__(self)\n", + " | Cast a Matrix as a scalar.\n", + " |\n", + " | Returns:\n", + " | scalar (float)\n", + " |\n", + " | Raises:\n", + " | RuntimeError: if the Matrix has more than one entry\n", + " |\n", + " | __getitem__(self, index)\n", + " | Obtain an individual entry of a Matrix.\n", + " |\n", + " | Args:\n", + " | index (int / tuple of int's): if index is an integer,\n", + " | the Matrix is viewed as a sequence in row-major order;\n", + " | if index is a tuple of integers, the first one refers to\n", + " | the row and the second one to the column of the entry\n", + " |\n", + " | Returns:\n", + " | entry (Matrix.typing)\n", + " |\n", + " | Example Usage:\n", + " | >>> m = Matrix([(1, 2), (3, 4)])\n", + " | >>> m[0]\n", + " | 1.0\n", + " | >>> m[-1]\n", + " | 4.0\n", + " | >>> m[0, 1]\n", + " | 2.0\n", + " |\n", + " | __init__(self, data)\n", + " | Create a new matrix.\n", + " |\n", + " | Args:\n", + " | data (sequence of sequences): the matrix's entries;\n", + " | viewed as a sequence of the matrix's rows (i.e., row-major order);\n", + " | use the .from_columns() class method if the data come as a sequence\n", + " | of the matrix's columns (i.e., column-major order)\n", + " |\n", + " | Raises:\n", + " | ValueError:\n", + " | - if no entries are provided\n", + " | - if the number of columns is inconsistent across the rows\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(1, 2), (3, 4)])\n", + " | Matrix(((1.0, 2.0,), (3.0, 4.0,)))\n", + " |\n", + " | __iter__(self)\n", + " | Loop over a Matrix's entries.\n", + " |\n", + " | See .entries() for more customization options.\n", + " |\n", + " | __len__(self)\n", + " | Number of entries in a Matrix.\n", + " |\n", + " | __mul__(self, other)\n", + " | Handle `self * other` and `other * self`.\n", + " |\n", + " | This may be either scalar multiplication, matrix-vector multiplication,\n", + " | vector-matrix multiplication, or matrix-matrix multiplication.\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(1, 2), (3, 4)]) * Matrix([(1, 2), (3, 4)])\n", + " | Matrix(((7.0, 10.0,), (15.0, 22.0,)))\n", + " |\n", + " | >>> 2 * Matrix([(1, 2), (3, 4)])\n", + " | Matrix(((2.0, 4.0,), (6.0, 8.0,)))\n", + " |\n", + " | >>> Matrix([(1, 2), (3, 4)]) * 3\n", + " | Matrix(((3.0, 6.0,), (9.0, 12.0,)))\n", + " |\n", + " | Matrix-vector and vector-matrix multiplication are not commutative.\n", + " |\n", + " | >>> from sample_package import Vector\n", + " |\n", + " | >>> Matrix([(1, 2), (3, 4)]) * Vector([5, 6])\n", + " | Vector((17.0, 39.0))\n", + " |\n", + " | >>> Vector([5, 6]) * Matrix([(1, 2), (3, 4)])\n", + " | Vector((23.0, 34.0))\n", + " |\n", + " | __neg__(self)\n", + " | Handle `-self`.\n", + " |\n", + " | Negate all entries of a Matrix.\n", + " |\n", + " | __pos__(self)\n", + " | Handle `+self`.\n", + " |\n", + " | This is simply an identity operator returning the Matrix itself.\n", + " |\n", + " | __radd__(self, other)\n", + " | See docstring for .__add__().\n", + " |\n", + " | __repr__(self)\n", + " | Text representation of a Matrix.\n", + " |\n", + " | __reversed__(self)\n", + " | Loop over a Matrix's entries in reverse order.\n", + " |\n", + " | See .entries() for more customization options.\n", + " |\n", + " | __rmul__(self, other)\n", + " | See docstring for .__mul__().\n", + " |\n", + " | __rsub__(self, other)\n", + " | See docstring for .__sub__().\n", + " |\n", + " | __str__(self)\n", + " | Human-readable text representation of a Matrix.\n", + " |\n", + " | __sub__(self, other)\n", + " | Handle `self - other` and `other - self`.\n", + " |\n", + " | This may be either matrix subtraction or broadcasting subtraction.\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(2, 3), (4, 5)]) - Matrix([(1, 2), (3, 4)])\n", + " | Matrix(((1.0, 1.0,), (1.0, 1.0,)))\n", + " |\n", + " | >>> Matrix([(1, 2), (3, 4)]) - 1\n", + " | Matrix(((0.0, 1.0,), (2.0, 3.0,)))\n", + " |\n", + " | >>> 10 - Matrix([(1, 2), (3, 4)])\n", + " | Matrix(((9.0, 8.0,), (7.0, 6.0,)))\n", + " |\n", + " | __truediv__(self, other)\n", + " | Handle `self / other`.\n", + " |\n", + " | Divide a Matrix by a scalar.\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(1, 2), (3, 4)]) / 4\n", + " | Matrix(((0.25, 0.5,), (0.75, 1.0,)))\n", + " |\n", + " | as_vector(self)\n", + " | Get a Vector representation of a Matrix.\n", + " |\n", + " | Returns:\n", + " | vector (vector.Vector)\n", + " |\n", + " | Raises:\n", + " | RuntimeError: if one of the two dimensions, .n_rows or .n_cols, is not 1\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix([(1, 2, 3)]).as_vector()\n", + " | Vector((1.0, 2.0, 3.0))\n", + " |\n", + " | cols(self)\n", + " | Loop over a Matrix's columns.\n", + " |\n", + " | Returns:\n", + " | columns (generator): produces a Matrix's columns as Vectors\n", + " |\n", + " | entries(self, *, reverse=False, row_major=True)\n", + " | Loop over a Matrix's entries.\n", + " |\n", + " | Args:\n", + " | reverse (bool): flag to loop backwards; defaults to False\n", + " | row_major (bool): flag to loop in row-major order; defaults to True\n", + " |\n", + " | Returns:\n", + " | entries (generator): produces a Matrix's entries\n", + " |\n", + " | rows(self)\n", + " | Loop over a Matrix's rows.\n", + " |\n", + " | Returns:\n", + " | rows (generator): produces a Matrix's rows as Vectors\n", + " |\n", + " | transpose(self)\n", + " | Switch the rows and columns of a Matrix.\n", + " |\n", + " | Returns:\n", + " | matrix (Matrix)\n", + " |\n", + " | Example Usage:\n", + " | >>> m = Matrix([(1, 2), (3, 4)])\n", + " | >>> m\n", + " | Matrix(((1.0, 2.0,), (3.0, 4.0,)))\n", + " | >>> m.transpose()\n", + " | Matrix(((1.0, 3.0,), (2.0, 4.0,)))\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Class methods defined here:\n", + " |\n", + " | from_columns(data) from builtins.type\n", + " | Create a new matrix.\n", + " |\n", + " | This is an alternative constructor for data provided in column-major order.\n", + " |\n", + " | Args:\n", + " | data (sequence of sequences): the matrix's entries;\n", + " | viewed as a sequence of the matrix's columns (i.e., column-major order);\n", + " | use the normal constructor method if the data come as a sequence\n", + " | of the matrix's rows (i.e., row-major order)\n", + " |\n", + " | Raises:\n", + " | ValueError:\n", + " | - if no entries are provided\n", + " | - if the number of rows is inconsistent across the columns\n", + " |\n", + " | Example Usage:\n", + " | >>> Matrix.from_columns([(1, 2), (3, 4)])\n", + " | Matrix(((1.0, 3.0,), (2.0, 4.0,)))\n", + " |\n", + " | from_rows(data) from builtins.type\n", + " | See docstring for .__init__().\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Readonly properties defined here:\n", + " |\n", + " | n_cols\n", + " | Number of columns in a Matrix.\n", + " |\n", + " | n_rows\n", + " | Number of rows in a Matrix.\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " |\n", + " | __dict__\n", + " | dictionary for instance variables\n", + " |\n", + " | __weakref__\n", + " | list of weak references to the object\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Data and other attributes defined here:\n", + " |\n", + " | __hash__ = None\n", + " |\n", + " | storage = \n", + " | Built-in immutable sequence.\n", + " |\n", + " | If no argument is given, the constructor returns an empty tuple.\n", + " | If iterable is specified the tuple is initialized from iterable's items.\n", + " |\n", + " | If the argument is a tuple, the return value is the same object.\n", + " |\n", + " |\n", + " | typing = \n", + " | Convert a string or number to a floating point number, if possible.\n", + " |\n", + " |\n", + " | vector_cls = \n", + " | A one-dimensional vector from linear algebra.\n", + " |\n", + " | All entries are converted to floats, or whatever is set in the typing attribute.\n", + " |\n", + " | Attributes:\n", + " | matrix_cls (matrix.Matrix): a reference to the Matrix class to work with\n", + " | storage (callable): data type used to store the entries internally;\n", + " | defaults to tuple\n", + " | typing (callable): type casting applied to all entries upon creation;\n", + " | defaults to float\n", + " | zero_threshold (float): max. tolerance when comparing an entry to zero;\n", + " | defaults to 1e-12\n", + " |\n", + " |\n", + " | zero_threshold = 1e-12\n", + "\n", + " class Vector(builtins.object)\n", + " | Vector(data)\n", + " |\n", + " | A one-dimensional vector from linear algebra.\n", + " |\n", + " | All entries are converted to floats, or whatever is set in the typing attribute.\n", + " |\n", + " | Attributes:\n", + " | matrix_cls (matrix.Matrix): a reference to the Matrix class to work with\n", + " | storage (callable): data type used to store the entries internally;\n", + " | defaults to tuple\n", + " | typing (callable): type casting applied to all entries upon creation;\n", + " | defaults to float\n", + " | zero_threshold (float): max. tolerance when comparing an entry to zero;\n", + " | defaults to 1e-12\n", + " |\n", + " | Methods defined here:\n", + " |\n", + " | __abs__(self)\n", + " | The Euclidean norm of a vector.\n", + " |\n", + " | __add__(self, other)\n", + " | Handle `self + other` and `other + self`.\n", + " |\n", + " | This may be either vector addition or broadcasting addition.\n", + " |\n", + " | Example Usage:\n", + " | >>> Vector([1, 2, 3]) + Vector([2, 3, 4])\n", + " | Vector((3.0, 5.0, 7.0))\n", + " |\n", + " | >>> Vector([1, 2, 3]) + 4\n", + " | Vector((5.0, 6.0, 7.0))\n", + " |\n", + " | >>> 10 + Vector([1, 2, 3])\n", + " | Vector((11.0, 12.0, 13.0))\n", + " |\n", + " | __bool__(self)\n", + " | A Vector is truthy if its Euclidean norm is strictly positive.\n", + " |\n", + " | __eq__(self, other)\n", + " | Handle `self == other`.\n", + " |\n", + " | Compare two Vectors for equality.\n", + " |\n", + " | Example Usage:\n", + " | >>> Vector([1, 2, 3]) == Vector([1, 2, 3])\n", + " | True\n", + " |\n", + " | >>> Vector([1, 2, 3]) == Vector([4, 5, 6])\n", + " | False\n", + " |\n", + " | __float__(self)\n", + " | Cast a Vector as a scalar.\n", + " |\n", + " | Returns:\n", + " | scalar (float)\n", + " |\n", + " | Raises:\n", + " | RuntimeError: if the Vector has more than one entry\n", + " |\n", + " | __getitem__(self, index)\n", + " | Obtain an individual entry of a Vector.\n", + " |\n", + " | __init__(self, data)\n", + " | Create a new vector.\n", + " |\n", + " | Args:\n", + " | data (sequence): the vector's entries\n", + " |\n", + " | Raises:\n", + " | ValueError: if no entries are provided\n", + " |\n", + " | Example Usage:\n", + " | >>> Vector([1, 2, 3])\n", + " | Vector((1.0, 2.0, 3.0))\n", + " |\n", + " | >>> Vector(range(3))\n", + " | Vector((0.0, 1.0, 2.0))\n", + " |\n", + " | __iter__(self)\n", + " | Loop over a Vector's entries.\n", + " |\n", + " | __len__(self)\n", + " | Number of entries in a Vector.\n", + " |\n", + " | __mul__(self, other)\n", + " | Handle `self * other` and `other * self`.\n", + " |\n", + " | This may be either the dot product of two vectors or scalar multiplication.\n", + " |\n", + " | Example Usage:\n", + " | >>> Vector([1, 2, 3]) * Vector([2, 3, 4])\n", + " | 20.0\n", + " |\n", + " | >>> 2 * Vector([1, 2, 3])\n", + " | Vector((2.0, 4.0, 6.0))\n", + " |\n", + " | >>> Vector([1, 2, 3]) * 3\n", + " | Vector((3.0, 6.0, 9.0))\n", + " |\n", + " | __neg__(self)\n", + " | Handle `-self`.\n", + " |\n", + " | Negate all entries of a Vector.\n", + " |\n", + " | __pos__(self)\n", + " | Handle `+self`.\n", + " |\n", + " | This is simply an identity operator returning the Vector itself.\n", + " |\n", + " | __radd__(self, other)\n", + " | See docstring for .__add__().\n", + " |\n", + " | __repr__(self)\n", + " | Text representation of a Vector.\n", + " |\n", + " | __reversed__(self)\n", + " | Loop over a Vector's entries in reverse order.\n", + " |\n", + " | __rmul__(self, other)\n", + " | See docstring for .__mul__().\n", + " |\n", + " | __rsub__(self, other)\n", + " | See docstring for .__sub__().\n", + " |\n", + " | __str__(self)\n", + " | Human-readable text representation of a Vector.\n", + " |\n", + " | __sub__(self, other)\n", + " | Handle `self - other` and `other - self`.\n", + " |\n", + " | This may be either vector subtraction or broadcasting subtraction.\n", + " |\n", + " | Example Usage:\n", + " | >>> Vector([7, 8, 9]) - Vector([1, 2, 3])\n", + " | Vector((6.0, 6.0, 6.0))\n", + " |\n", + " | >>> Vector([1, 2, 3]) - 1\n", + " | Vector((0.0, 1.0, 2.0))\n", + " |\n", + " | >>> 10 - Vector([1, 2, 3])\n", + " | Vector((9.0, 8.0, 7.0))\n", + " |\n", + " | __truediv__(self, other)\n", + " | Handle `self / other`.\n", + " |\n", + " | Divide a Vector by a scalar.\n", + " |\n", + " | Example Usage:\n", + " | >>> Vector([9, 6, 12]) / 3\n", + " | Vector((3.0, 2.0, 4.0))\n", + " |\n", + " | as_matrix(self, *, column=True)\n", + " | Get a Matrix representation of a Vector.\n", + " |\n", + " | Args:\n", + " | column (bool): if the vector is interpreted as a\n", + " | column vector or a row vector; defaults to True\n", + " |\n", + " | Returns:\n", + " | matrix (matrix.Matrix)\n", + " |\n", + " | Example Usage:\n", + " | >>> v = Vector([1, 2, 3])\n", + " | >>> v.as_matrix()\n", + " | Matrix(((1.0,), (2.0,), (3.0,)))\n", + " | >>> v.as_matrix(column=False)\n", + " | Matrix(((1.0, 2.0, 3.0,)))\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " |\n", + " | __dict__\n", + " | dictionary for instance variables\n", + " |\n", + " | __weakref__\n", + " | list of weak references to the object\n", + " |\n", + " | ----------------------------------------------------------------------\n", + " | Data and other attributes defined here:\n", + " |\n", + " | __hash__ = None\n", + " |\n", + " | matrix_cls = \n", + " | An m-by-n-dimensional matrix from linear algebra.\n", + " |\n", + " | All entries are converted to floats, or whatever is set in the typing attribute.\n", + " |\n", + " | Attributes:\n", + " | storage (callable): data type used to store the entries internally;\n", + " | defaults to tuple\n", + " | typing (callable): type casting applied to all entries upon creation;\n", + " | defaults to float\n", + " | vector_cls (vector.Vector): a reference to the Vector class to work with\n", + " | zero_threshold (float): max. tolerance when comparing an entry to zero;\n", + " | defaults to 1e-12\n", + " |\n", + " |\n", + " | storage = \n", + " | Built-in immutable sequence.\n", + " |\n", + " | If no argument is given, the constructor returns an empty tuple.\n", + " | If iterable is specified the tuple is initialized from iterable's items.\n", + " |\n", + " | If the argument is a tuple, the return value is the same object.\n", + " |\n", + " |\n", + " | typing = \n", + " | Convert a string or number to a floating point number, if possible.\n", + " |\n", + " |\n", + " | zero_threshold = 1e-12\n", + "\n", + "DATA\n", + " __all__ = ['Matrix', 'Vector']\n", + "\n", + "VERSION\n", + " 0.1.0\n", + "\n", + "AUTHOR\n", + " Alexander Hess\n", + "\n", + "FILE\n", + " /home/alexander/Repositories/intro-to-python/11_classes/sample_package/__init__.py\n", + "\n", + "\n" + ] + } + ], + "source": [ + "help(pkg)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The meta information could also be accessed separately and individually." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'linear_algebra_tools'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg.__name__" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.1.0'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg.__version__ # follows the semantic versioning format" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We create `Vector` and `Matrix` instances in the usual way by calling the `Vector` and `Matrix` classes from the package's top level." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg.Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg.Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "A common practice by package authors is to put all the objects on the package's top level that they want the package users to work with directly. That is achieved via the `import` statements in the [*\\_\\_init\\_\\_.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/__init__.py) file.\n", + "\n", + "However, users can always reach into a package and work with its internals.\n", + "\n", + "For example, the `Vector` and `Matrix` classes are also available via their **qualified name** (cf., [PEP 3155 ](https://www.python.org/dev/peps/pep-3155/)): First, we access the `vector` and `matrix` modules on `pkg`, and then the `Vector` and `Matrix` classes on the modules." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.vector.Vector" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg.vector.Vector" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.matrix.Matrix" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pkg.matrix.Matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Also, let's import the [*utils.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/utils.py) module with the `norm()` function into the global scope. As this function is integrated into the `Vector.__abs__()` and `Matrix.__abs__()` methods, there is actually no need to work with it explicitly." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from sample_package import utils" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function norm in module sample_package.utils:\n", + "\n", + "norm(vec_or_mat)\n", + " Calculate the Frobenius or Euclidean norm of a matrix or vector.\n", + "\n", + " Find more infos here: https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm\n", + "\n", + " Args:\n", + " vec_or_mat (Vector / Matrix): object whose entries are squared and summed up\n", + "\n", + " Returns:\n", + " norm (float)\n", + "\n", + " Example Usage:\n", + " As Vector and Matrix objects are by design non-empty sequences,\n", + " norm() may be called, for example, with `[3, 4]` as the argument:\n", + " >>> norm([3, 4])\n", + " 5.0\n", + "\n" + ] + } + ], + "source": [ + "help(utils.norm)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Many tutorials on the internet begin by importing \"everything\" from a package into the global scope with `from ... import *`.\n", + "\n", + "That is commonly considered a *bad* practice as it may overwrite already existing variables. However, if the package's [*\\_\\_init\\_\\_.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/__init__.py) file defines an `__all__` attribute, a `list` with all the names to be \"exported,\" the **star import** is safe to be used, in particular, in *interactive* sessions like Jupyter notebooks. We emphasize that the star import should *not* be used *within* packages and modules as then it is not directly evident from a name where the corresponding object is defined.\n", + "\n", + "For more best practices regarding importing we refer to, among others, [Google's Python Style Guide](https://google.github.io/styleguide/pyguide.html#22-imports).\n", + "\n", + "The following `import` statement makes the `Vector` and `Matrix` classes available in the global scope." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "from sample_package import *" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.vector.Vector" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.matrix.Matrix" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For further information on modules and packages, we refer to the [official tutorial ](https://docs.python.org/3/tutorial/modules.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## The final `Vector` and `Matrix` Classes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The final implementations of the `Vector` and `Matrix` classes are in the [*matrix.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/matrix.py) and [*vector.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/vector.py) files: They integrate all of the functionalities introduced in this chapter. In addition, the code is cleaned up and fully documented, including examples of common usages.\n", + "\n", + "We strongly suggest the eager student go over the files in the [*sample_package* ](https://github.com/webartifex/intro-to-python/tree/main/11_classes/sample_package) in detail at some point to understand what well-written and (re-)usable code looks like." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector([1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.vector.Vector" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(v)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "m = Matrix([(1, 2, 3), (4, 5, 6), (7, 8, 9)])" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.matrix.Matrix" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Furthermore, the classes are designed for easier maintenence in the long-run.\n", + "\n", + "For example, the `Matrix/Vector.storage` and `Matrix/Vector.typing` class attributes replace the \"hard coded\" [tuple() ](https://docs.python.org/3/library/functions.html#func-tuple) and [float() ](https://docs.python.org/3/library/functions.html#float) built-ins in the `.__init__()` methods: As `self.storage` and `self.typing` are not defined on the *instances*, Python automatically looks them up on the *classes*." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "tuple" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector.storage" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "float" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector.typing" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mSignature:\u001b[0m \u001b[0mVector\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mSource:\u001b[0m \n", + " \u001b[0;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Create a new vector.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Args:\u001b[0m\n", + "\u001b[0;34m data (sequence): the vector's entries\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Raises:\u001b[0m\n", + "\u001b[0;34m ValueError: if no entries are provided\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example Usage:\u001b[0m\n", + "\u001b[0;34m >>> Vector([1, 2, 3])\u001b[0m\n", + "\u001b[0;34m Vector((1.0, 2.0, 3.0))\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m >>> Vector(range(3))\u001b[0m\n", + "\u001b[0;34m Vector((0.0, 1.0, 2.0))\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_entries\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstorage\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtyping\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mdata\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"a vector must have at least one entry\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mFile:\u001b[0m ~/Repositories/intro-to-python/11_classes/sample_package/vector.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Vector.__init__??" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Both `Matrix/Vector.storage` and `Matrix/Vector.typing` themselves reference the `DEFAULT_ENTRIES_STORAGE` and `DEFAULT_ENTRY_TYPE` constants in the [*utils.py* ](https://github.com/webartifex/intro-to-python/blob/main/11_classes/sample_package/utils.py) module. This way, we could, for example, change only the constants and thereby also change how the `._entries` are stored internally in both classes. Also, this single **[single source of truth ](https://en.wikipedia.org/wiki/Single_source_of_truth)** ensures that both classes are consistent with each other at all times." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "tuple" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "utils.DEFAULT_ENTRIES_STORAGE" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "slideshow": { + "slide_type": "-" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "float" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "utils.DEFAULT_ENTRY_TYPE" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For the same reasons, we also replace the \"hard coded\" references to the `Vector` and `Matrix` classes within the various methods.\n", + "\n", + "Every instance object has an automatically set `.__class__` attribute referencing its class." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.matrix.Matrix" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.__class__" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Of course, we could also use the [type() ](https://docs.python.org/3/library/functions.html#type) built-in instead." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.matrix.Matrix" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "So, for example, the `Matrix.transpose()` method makes a `self.__class__(...)` instead of a `Matrix(...)` call." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mSignature:\u001b[0m \u001b[0mMatrix\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtranspose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mSource:\u001b[0m \n", + " \u001b[0;32mdef\u001b[0m \u001b[0mtranspose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Switch the rows and columns of a Matrix.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Returns:\u001b[0m\n", + "\u001b[0;34m matrix (Matrix)\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example Usage:\u001b[0m\n", + "\u001b[0;34m >>> m = Matrix([(1, 2), (3, 4)])\u001b[0m\n", + "\u001b[0;34m >>> m\u001b[0m\n", + "\u001b[0;34m Matrix(((1.0, 2.0,), (3.0, 4.0,)))\u001b[0m\n", + "\u001b[0;34m >>> m.transpose()\u001b[0m\n", + "\u001b[0;34m Matrix(((1.0, 3.0,), (2.0, 4.0,)))\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__class__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mzip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_entries\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;31mFile:\u001b[0m ~/Repositories/intro-to-python/11_classes/sample_package/matrix.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Matrix.transpose??" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Whenever we need a `str` representation of a class's name, we use the `.__name__` attribute on the class, ..." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Matrix'" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Matrix.__name__" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... or access it via the `.__class__` attribute on an instance." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'Matrix'" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m.__class__.__name__" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For example, the `.__repr__()` and `.__str__()` methods make use of that." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mSignature:\u001b[0m \u001b[0mMatrix\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__repr__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mSource:\u001b[0m \n", + " \u001b[0;32mdef\u001b[0m \u001b[0m__repr__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Text representation of a Matrix.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mname\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__class__\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__name__\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0margs\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\", \"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\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[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrepr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mc\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mc\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mr\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m\",)\"\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mr\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_entries\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;34mf\"\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m}\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[0;34m\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mFile:\u001b[0m ~/Repositories/intro-to-python/11_classes/sample_package/matrix.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Matrix.__repr__??" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "In order to not have to \"hard code\" the name of *another* class (e.g., the `Vector.as_matrix()` method references the `Matrix` class), we apply the following \"hack:\" First, we store a reference to the other class as a class attribute (e.g., `Matrix.vector_cls` and `Vector.matrix_cls`), and then reference that attribute within the methods, just like `.storage` and `.typing` above." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.vector.Vector" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Matrix.vector_cls" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "sample_package.matrix.Matrix" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Vector.matrix_cls" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "As an example, the `Vector.as_matrix()` method makes a `self.matrix_cls(...)` instead of a `Matrix(...)` call." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[0;31mSignature:\u001b[0m \u001b[0mVector\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mas_matrix\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcolumn\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mSource:\u001b[0m \n", + " \u001b[0;32mdef\u001b[0m \u001b[0mas_matrix\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcolumn\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;34m\"\"\"Get a Matrix representation of a Vector.\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Args:\u001b[0m\n", + "\u001b[0;34m column (bool): if the vector is interpreted as a\u001b[0m\n", + "\u001b[0;34m column vector or a row vector; defaults to True\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Returns:\u001b[0m\n", + "\u001b[0;34m matrix (matrix.Matrix)\u001b[0m\n", + "\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m Example Usage:\u001b[0m\n", + "\u001b[0;34m >>> v = Vector([1, 2, 3])\u001b[0m\n", + "\u001b[0;34m >>> v.as_matrix()\u001b[0m\n", + "\u001b[0;34m Matrix(((1.0,), (2.0,), (3.0,)))\u001b[0m\n", + "\u001b[0;34m >>> v.as_matrix(column=False)\u001b[0m\n", + "\u001b[0;34m Matrix(((1.0, 2.0, 3.0,)))\u001b[0m\n", + "\u001b[0;34m \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcolumn\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatrix_cls\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmatrix_cls\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\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;31mFile:\u001b[0m ~/Repositories/intro-to-python/11_classes/sample_package/vector.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Vector.as_matrix??" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "For completeness sake, we mention that in the final `Vector` and `Matrix` classes, the `.__sub__()` and `.__rsub__()` methods use the negation operator implemented in `.__neg__()` and then dispatch to `.__add__()` instead of implementing the subtraction logic themselves." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## \"Real-life\" Experiment" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Let's do some math with bigger `Matrix` and `Vector` instances." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We initialize `m` as a $100x50$ dimensional `Matrix` with random numbers in the range between `0` and `1_000`." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "m = Matrix((1_000 * random.random() for _ in range(50)) for _ in range(100))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We quickly lose track with all the numbers in the `Matrix`, which is why we implemented the `__str__()` method as a summary representation." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((639.4267984578837, 25.010755222666937, 275.02931836911927, 223.21073814882274, 736.4712141640124, 676.6994874229113, 892.1795677048455, 86.93883262941615, 421.9218196852704, 29.797219438070343, 218.63797480360336, 505.3552881033624, 26.535969683863627, 198.8376506866485, 649.8844377795232, 544.9414806032166, 220.4406220406967, 589.2656838759087, 809.4304566778267, 6.498759678061017, 805.8192518328079, 698.1393949882269, 340.25051651799185, 155.47949981178155, 957.2130722067812, 336.59454511262675, 92.7458433801479, 96.71637683346401, 847.4943663474597, 603.7260313668911, 807.1282732743801, 729.7317866938179, 536.2280914547007, 973.1157639793706, 378.5343772083535, 552.040631273227, 829.4046642529948, 618.5197523642461, 861.7069003107772, 577.352145256762, 704.5718362149235, 45.82438365566222, 227.89827565154687, 289.38796360210716, 79.79197692362749, 232.79088636103018, 101.00142940972911, 277.97360311009214, 635.6844442644002, 364.8321789700842,), (370.18096711688264, 209.50703077148768, 266.97782204911334, 936.6545877124939, 648.0353852465936, 609.1310056669882, 171.13864819809697, 729.1267979503492, 163.40249376192838, 379.4554417576478, 989.5233506365953, 639.9997598540929, 556.9497437746462, 684.6142509898746, 842.8519201898096, 775.9999115462448, 229.04807196410437, 32.10024390403776, 315.45304805908194, 267.74087597570275, 210.98284358632645, 942.9097143350544, 876.3676264726688, 314.67788079847793, 655.43866529488, 395.63190106066423, 914.5475897405435, 458.8518525873988, 264.88016649805246, 246.62750769398346, 561.3681341631508, 262.7416085229353, 584.5859902235405, 897.822883602477, 399.4005051403973, 219.32075915728333, 997.5376064951103, 509.5262936764645, 90.9094121737939, 47.11637542473457, 109.64913035065915, 627.44604170309, 792.0793643629642, 422.159966799684, 63.52770615195713, 381.61928650653675, 996.1213802400968, 529.114345099137, 971.0783776136182, 860.779702234498,), (11.481021942819636, 720.7218193601947, 681.7103690265748, 536.9703304087951, 266.82518995254276, 640.961798579808, 111.55217359587644, 434.765250669105, 453.72370632920644, 953.8159275210801, 875.852940378194, 263.38905075109074, 500.5861130502983, 178.65188053013136, 912.6278393448205, 870.5185698367669, 298.4447914486329, 638.9494948660051, 608.9702114381723, 152.83926854963482, 762.5108000751512, 539.3790301196258, 778.6264786305583, 530.3536721951775, 0.5718961279435053, 324.1560570046731, 19.476742385832303, 929.0986162646171, 878.7218778231842, 831.6655293611794, 307.5141254026614, 57.92516649418755, 878.0095992040406, 946.9494452979941, 85.65345206787877, 485.9904633166138, 69.2125184683836, 760.6021652572316, 765.8344293069878, 128.3914644997628, 475.28237809873133, 549.8035934949439, 265.0566289400591, 872.4330410852574, 423.13794020088693, 211.79820544208206, 539.2960887794584, 729.9310690899762, 201.1510633896959, 311.71629130089497,), (995.1493566608947, 649.8780576394535, 438.10008391450407, 517.5758410355907, 121.00419586826573, 224.69733703155737, 338.08556214745533, 588.3087184572333, 230.114732596577, 220.21738445155947, 70.99308600903254, 631.102957270099, 228.94178381115438, 905.420013006128, 859.6354002537465, 70.85734988865345, 238.00463436899523, 668.9777782962806, 214.2368073704386, 132.311848725025, 935.514240580671, 571.0430933252844, 472.67102631179415, 784.6194242907534, 807.4969977666434, 190.4099143618777, 96.93081422882332, 431.0511824063775, 423.57862301992077, 467.024668036675, 729.0758494598506, 673.3645472933015, 984.1652113659661, 98.41787115195888, 402.62128210226876, 339.30260539496317, 861.6725363527911, 248.65633392028565, 190.2089084408115, 448.6135478331319, 421.8816398344042, 278.5451446669405, 249.80644788210049, 923.2655992760128, 443.13074505345696, 861.3491047618306, 550.3253124498481, 50.58832952488124, 999.2824684127266, 836.0275850799519,), (968.9962572847512, 926.3669830081276, 848.6957344143054, 166.31111060391402, 485.64112545071845, 213.74729919918167, 401.0402925494526, 58.635399972178924, 378.9731189769161, 985.308843779726, 265.20305817215194, 784.0706019485693, 455.0083673391433, 423.00748599016293, 957.3176408596732, 995.4226894927139, 555.7683234056182, 718.408275296326, 154.79682527406413, 296.7078254945642, 968.7093649691589, 579.1802908162562, 542.1952013742742, 747.9755603790641, 57.16527290748308, 584.1775944589713, 502.85038291951355, 852.7198920482854, 157.43272793948327, 960.7789032744504, 80.11146524058688, 185.8249609807232, 595.0351064500277, 675.2125536040902, 235.2038950009312, 119.88661394712419, 890.2873141294375, 246.21534778862485, 594.5191535334412, 619.3815103321031, 419.2249153358725, 583.6722892912247, 522.7827155319588, 934.7062577364272, 204.25919942353644, 716.1918007894149, 238.68595261584602, 395.7858467912545, 671.6902229599713, 299.99707979876223,), (316.17719627185403, 751.8644924144021, 72.54311449315732, 458.2855226185861, 998.4544408544424, 996.0964478550944, 73.260721099633, 213.1543122670404, 265.20041475040136, 933.2593779937091, 880.8641736864396, 879.2702424845428, 369.52708873888395, 157.74683235723197, 833.744954639807, 703.539925087371, 611.6777657259502, 987.2330636315044, 653.9763177107326, 7.823107152157949, 817.1041351154616, 299.3787521999779, 663.3887149660774, 938.9300039271039, 134.2911143933677, 115.4286704191022, 107.03597770941764, 553.223640884816, 272.3482123148163, 604.8298270302239, 717.6121871387979, 203.59731232745293, 634.2379588850797, 263.98390163040943, 488.53185214937656, 905.3364910793232, 846.1037132948554, 92.29846771273343, 423.57577256372633, 276.68022397225167, 3.5456890877823, 771.119223019627, 637.1133773013796, 261.9552624343482, 741.2309083479308, 551.6804211263913, 427.68691898067937, 9.669699608339965, 75.24386007376704, 883.1063933001429,), (903.9285715598932, 545.5902892055224, 834.5950198860166, 582.509566489794, 148.09378556748266, 127.44551928213876, 308.2583499301337, 898.9814887425899, 796.1223048880418, 860.7025820009028, 898.9246365264746, 210.07653833975405, 249.52973922292443, 102.79362167178563, 780.1162418714426, 884.1347014510089, 406.3773898321168, 620.6615101507128, 154.55333833220465, 929.8810156936744, 864.605696219964, 976.2060329309629, 810.7717199403969, 881.4162046633244, 24.786361898188723, 736.5644717550821, 332.1854679464287, 930.8158860483255, 802.2351389371389, 864.0640283752793, 810.749316574389, 266.80570959447203, 787.3745091354712, 108.09562640295711, 872.1667829060897, 858.5932513377817, 222.43371754566442, 816.586605596929, 460.3032346789421, 305.1908673386006, 795.3454991528617, 227.59548740777035, 23.66443470145152, 193.12978832770867, 328.26195119770654, 864.3529420302864, 966.8891040483611, 279.1249927218714, 641.4817386076277, 399.6783843600609,), (981.1496871982602, 536.2157324787219, 939.2371403247157, 115.34175185142759, 970.400611022228, 178.56781617246364, 962.5343157615555, 265.4663625229686, 108.40254721471109, 434.56375856464433, 728.5450606527043, 313.67731419499125, 606.2088533061433, 511.4230596694781, 385.1954333447272, 576.5880434965995, 254.72250613858193, 708.7852838341706, 1.6912782186294661, 925.5751654990827, 538.4519970927919, 719.4299991448455, 741.9500778394765, 670.6285044329995, 364.2214717812642, 69.97381112631018, 664.2376849112724, 330.2000360425964, 313.91564505835964, 848.0152795063354, 719.7542630139502, 300.3222682112642, 309.28466220865323, 408.3929086192168, 402.40038705772463, 295.655202525947, 127.28779905915322, 420.4463337729083, 940.363670730183, 677.3179452727329, 902.8055457325827, 615.5149159513805, 300.9498745655653, 547.9372131356982, 0.4059396972875273, 286.91371686892717, 429.8881499898346, 579.984781195682, 654.7056237030716, 464.9881902470142,), (442.1597993048074, 213.70140098910028, 473.18618590932624, 901.1808258282542, 796.0247601267803, 169.69139619805475, 84.79553672512175, 515.4520099152164, 632.9408557657957, 335.1882554098009, 818.4234645366643, 751.1381375407323, 672.795670557167, 224.64066599728827, 199.12993272657664, 24.425387726826344, 244.84254407835016, 475.1363442188051, 849.737694624732, 72.82822918456911, 414.44101099771933, 629.7653807377137, 194.4352367397093, 696.3542504905049, 494.37716901043694, 243.98443957843884, 656.058011111784, 5.54481813803176, 750.9644766184729, 770.0461885740251, 106.58729656353894, 425.1461939427341, 175.88668170653165, 957.9660422795397, 517.9577504437408, 50.21838514064092, 249.19827965997166, 848.3363473516597, 456.4618254701725, 801.4166017222644, 667.5777325863531, 987.8924530664481, 595.4523184694197, 950.0396084431559, 891.425925810437, 612.6523227617629, 719.273961275967, 504.778164824402, 830.569169721415, 547.8719506108284,), (897.2081032332621, 743.6554421595849, 474.6744368230533, 259.19154846501937, 247.23973750965956, 637.6614367761563, 765.8136842971654, 521.2998128279821, 626.7484369817813, 274.59744691753826, 77.48335386473582, 285.7281508631525, 271.7151070821846, 319.7095684187623, 540.1522225184564, 138.37406151615573, 231.26147972818677, 693.9498122990523, 706.4191416945522, 64.22885071387807, 407.5993696665867, 542.6111405039153, 415.77423410315595, 206.83438951441025, 420.14351777342563, 904.838478340177, 584.0794142042251, 695.5229864979681, 856.7320323039343, 765.5945761180694, 380.38102892771167, 5.8960835839930725, 351.7588026718247, 753.4751250593858, 853.4479505691046, 953.4303384701062, 419.0212826298219, 747.5156689780508, 546.1323097338391, 603.2525889412414, 220.5386943238189, 219.42163462143617, 435.8359760466366, 29.02481994671524, 336.12954369838246, 679.1418850283497, 404.31666913763706, 165.04473120350883, 467.39014923231014, 127.6277972811607,), (622.2569609740647, 26.96645190513769, 394.0202563397047, 564.3919830247742, 27.102046340312434, 642.7496480093357, 135.69948723056424, 461.69844405151173, 50.28463348862755, 379.1038641881396, 211.66028421148152, 326.84580488130854, 761.2297078940271, 379.12621556413626, 752.0098235547848, 831.9242851552726, 252.2715317823806, 81.90623276164256, 19.38328705001069, 539.4190479225337, 999.9078285092093, 349.9603437201839, 650.144093249875, 781.2330496108949, 651.7546552438894, 754.2332040595262, 949.6117327159889, 199.3606823625329, 20.380017320332342, 152.38234578479037, 126.22097487420625, 669.4588446199107, 563.9695819300191, 217.96454090650363, 699.4649712461509, 766.8980983562408, 167.78914336780227, 607.2474938909317, 747.9256519552858, 114.53287137889767, 819.3011743110851, 964.7207730340876, 108.09874965760658, 25.678425497466016, 311.9572443946952, 677.3472868504088, 958.1728382058959, 396.65444151662734, 715.0147050494684, 75.99647784305996,), (690.6144159329804, 627.2423956010444, 101.90130544597653, 772.480884951224, 850.2932390887963, 600.4116148168441, 121.05506506731511, 983.8443515146713, 782.6353463610196, 347.20376530844896, 428.37801323474446, 370.5708762180562, 505.9607896770779, 341.2311748612832, 849.5756269995774, 822.330918090058, 105.53887064399858, 960.7875672145785, 635.585106101446, 828.7073110024583, 707.308643706077, 435.48714500767704, 733.7953040133918, 965.4737312380777, 270.0823963874008, 808.1992188067559, 538.1729064482557, 483.49750388319615, 435.5744930038943, 731.0262143051223, 268.3955380492253, 851.7131600193319, 830.7310188906034, 86.6628980556744, 881.631184002772, 243.863439190956, 464.7084666032317, 610.3317042305206, 378.98930412826405, 28.69999777008958, 850.9528363124591, 181.8398571576918, 212.119850179739, 797.8323568280965, 340.3388431642836, 880.3199797582254, 701.1837503322016, 276.26857575618493, 10.151114438677112, 948.0625777770312,), (85.61296195802126, 720.0746641041943, 488.5778468487874, 758.1646534824664, 690.609339446523, 645.9028997409523, 490.8213350785862, 792.9328681323175, 93.05335055467168, 221.59640047253015, 691.7871552952018, 306.2060301300884, 581.5555853323672, 473.2604887595248, 530.9219311457678, 425.5038127052148, 745.9354367136096, 330.79129719593016, 702.8549421857689, 270.91642697619636, 251.4036762009415, 120.65588475230582, 192.5842934515174, 119.55474125505283, 535.8639653837498, 762.189609484103, 185.14984243766196, 216.38463987860567, 484.1985872127087, 724.5850010930516, 976.6070228830567, 524.6368691086078, 282.998703274955, 100.52610809079077, 194.11757809107343, 227.48316348255472, 179.44154368540333, 14.148367256063832, 534.1350892676633, 274.3113267832673, 974.2949311027379, 553.3589667986083, 697.4173929101944, 126.2794929584089, 868.461197262944, 490.87869452377885, 872.7197349985346, 574.0642196263323, 469.3969449305102, 440.46879813601026,), (184.36367039008172, 51.37671718357784, 941.0635967681033, 477.7291872656877, 822.1156452994304, 400.70744225193977, 74.0821702556398, 629.4457069519983, 53.609074292533144, 149.19758447365516, 562.8395970845642, 303.8355118422277, 993.9181227102201, 118.45156221146158, 764.4434453261943, 606.3176512429628, 790.7408298434194, 225.68713706503462, 522.5725351302178, 450.5144648887542, 442.72100401932835, 860.1666658261801, 990.0312604667979, 305.3802443193604, 621.0273210721485, 609.6309122451303, 740.089305484545, 947.590200325378, 207.78790582355134, 211.025195305393, 660.4281371920366, 157.05709338087715, 173.81354832643447, 75.06486890116793, 2.675722602885733, 450.5037046177024, 593.8111951195633, 291.2592890310276, 231.47623455602584, 706.9558298790332, 702.9875580937172, 454.03132640704325, 687.3849200712427, 923.9110444825487, 787.8280266467314, 625.0580071642219, 661.1830428533679, 933.6684584455132, 425.13896610421165, 544.5623787105786,), (647.6347234024807, 908.411445212606, 826.6311596500597, 71.40983685581415, 165.92278922072467, 307.6118126142325, 748.9577220696233, 569.2070493190923, 288.61058830584176, 124.35365817470823, 688.6779912120937, 699.7336849757173, 942.6762407440408, 500.4721771179256, 493.7952193450843, 80.44185189930097, 39.86078418361305, 432.02866416077256, 322.32158335829996, 250.3679024120039, 91.3268866309852, 961.9111021928367, 835.9586139061813, 575.1991092199305, 950.7862778063524, 999.5724168774514, 672.2815843032392, 269.5110259670539, 40.231673144616174, 756.2688304125571, 470.50082325163356, 651.5094894336863, 916.0727879267675, 181.4891472231259, 585.3296252778543, 634.7847194540868, 491.7258021910134, 91.24240629382719, 347.96105629465046, 333.308393665092, 670.1335095211855, 857.7330944003526, 329.8036635789654, 693.6736739834325, 288.2177953614557, 945.1935395632106, 813.5660347379726, 550.0966089717829, 454.82590860172945, 314.51715717016793,), (323.27378627599467, 970.1847268085771, 404.17505700074565, 514.5962523291051, 988.1192147817542, 657.660386483129, 542.593594357195, 413.2475707993215, 187.5825413945271, 361.77935915167524, 756.4431540555737, 625.408742145009, 759.990536061017, 203.55823835027388, 549.2196390853317, 927.6727608444477, 438.11609507237824, 698.2500291221722, 121.42608336261983, 973.1468158216944, 608.8716670819649, 239.29746232765038, 158.3781638052324, 550.83900700267, 552.251409053733, 93.20920128152255, 992.2571393952196, 912.929878484168, 461.44789417878343, 117.46614908955755, 832.1431737689012, 498.37550470719526, 716.603325991791, 508.87201506951175, 273.42489671327417, 834.7239455766944, 980.2446326033755, 243.73090607513836, 551.2650768804697, 383.58601324714994, 921.8681499315619, 508.240891592894, 879.3262551464761, 864.0269344285881, 276.24740282204095, 790.0061820031135, 414.94242355134514, 934.2483936825201, 507.7376758096565, 820.5494731855783,), (282.83898328282663, 298.5558497717672, 586.937722414161, 998.9023332293388, 489.640346655701, 148.59541838278912, 538.5805777038737, 345.1239416930075, 551.917417070811, 543.430062958673, 455.3446168665011, 321.77735022903744, 188.65237370710543, 697.4984276205713, 571.7976419849011, 233.5624460578931, 775.5444750992589, 43.64729909730192, 744.7051515651959, 705.2278810250026, 811.4089025648768, 386.0787524909823, 663.6888294845347, 820.7475517095551, 980.8181386597851, 495.32864961642355, 37.01961134557619, 502.29115013296587, 590.180429309567, 869.7003133622154, 874.1903740543083, 440.3062097706771, 525.9510868075614, 456.92807447424224, 722.4438275706426, 409.9786197463644, 654.7813264277338, 154.36121877154883, 469.49060098473194, 969.2036305742172, 338.56123405143165, 692.7045985868821, 649.8366525792726, 851.7652923506914, 852.341336565893, 859.3421841682666, 380.00939752265504, 316.66115393339965, 718.717425222983, 759.4018093343636,), (872.3830173985363, 35.89909983768658, 68.42074718155145, 631.1610168546406, 920.9290987802967, 997.4259229562153, 746.766366737927, 433.9714719231257, 98.44312638319796, 633.7478287807912, 872.5792326070907, 443.6785516617058, 694.0011627932346, 903.424062050486, 45.99096867721275, 796.1434651622118, 293.3677759652088, 374.84108977531486, 145.56979569939844, 531.1663181487601, 565.9280619159368, 792.5194738809532, 169.98364906809738, 78.96835065999464, 870.8395986448548, 619.7103685374153, 240.82979185631302, 912.8290160235548, 143.11772012850966, 461.1499133549976, 253.97733941354227, 255.32670815915404, 9.397431454871041, 804.6330769751463, 901.2094235988831, 677.6108856990794, 157.97562207178694, 441.72978360381444, 345.5656244430817, 587.5717051264214, 638.9387023600121, 424.30893846089765, 250.09822440770125, 845.3039251425987, 199.21699910889234, 384.6932489673455, 483.2080610592161, 237.2057019275746, 571.9226923507389, 574.8119301786451,), (992.6920436268975, 295.23075388326936, 977.9444845768627, 658.2298159286806, 274.48038017822773, 565.929016955699, 685.7994927340121, 744.6688411653251, 49.04425077599994, 606.4064930764746, 496.7272865238703, 904.1552908937254, 286.1941514591945, 798.8601195075987, 607.064998164266, 352.3209558353165, 636.6178780058918, 620.8911631303041, 677.7644586233595, 720.928376670709, 659.1815403167537, 838.3371169625166, 628.2481036868983, 903.4037040729679, 646.3406088905228, 308.9328839526361, 440.82319016281156, 579.5738053684028, 732.3597679392383, 90.13337574621893, 295.1104516291553, 747.4808649383715, 175.64007044430662, 132.15979774678354, 539.4077589844211, 971.4895812113399, 530.852373702785, 913.486974481697, 830.4726195673974, 256.97008456326233, 824.6898125424073, 481.8478298737412, 806.4884937937666, 746.559350717045, 338.71525380188626, 115.1697074497594, 962.8932928688776, 140.75701500588266, 966.5002094627521, 860.1405968988219,), (724.2167120755182, 979.9422427819035, 967.2697473000323, 804.5876440205619, 365.7750494055664, 790.6819685889375, 13.918655100901178, 536.5723082690591, 454.7860277338747, 672.8283818737536, 672.3407973510132, 584.5600916521661, 822.4173012267743, 940.2918917795541, 108.34610219923069, 233.82190221158316, 25.024649646482324, 884.2348452148522, 561.4073822498789, 915.2559087431595, 221.36720007399003, 63.21704116019544, 823.8553513904476, 909.387638427892, 302.1901745292502, 408.2958557954127, 139.7770125072211, 946.2615328819462, 304.3645843560052, 492.6246189782059, 97.1919986218216, 887.2593085285023, 135.66404870633653, 453.6437568888926, 670.4862188501712, 743.1401215231715, 945.9740857794321, 419.1267534147228, 742.2690147653158, 154.522902409835, 414.88452743680693, 99.02163471052849, 489.34703778961455, 408.1158856977005, 951.5215253810595, 32.7162868550469, 370.5299587344354, 443.38308606070166, 950.555169851427, 855.4501933059546,), (99.35462460613032, 685.6802654812853, 544.4658614821449, 977.8425294520467, 358.67384121231794, 398.1396427443731, 189.80856216107955, 122.15971908726375, 848.0331884636811, 454.7173685705171, 662.768738061978, 641.7044672332177, 597.145959519545, 21.357454736370627, 786.7945904546167, 243.56889716402364, 125.92388530804288, 564.5779759079634, 68.6101528243559, 765.1573758885845, 207.1573703466585, 215.95135191867476, 869.6954267695447, 328.5595534322304, 147.55417994144372, 900.5310356317582, 2.8355514800163517, 858.4061263801764, 144.687980320508, 129.9921314434368, 250.65419672812382, 174.49712090139346, 661.0576425973168, 25.780149786198358, 14.860327230708847, 789.9846642347538, 237.93160609046305, 323.77146196202244, 174.2462014061772, 52.39901786117651, 741.7180569541495, 526.0855265978671, 745.6652750339957, 476.24596542325025, 778.0170393142027, 513.2379576091917, 109.05401000384552, 503.8386897858214, 945.4156429701063, 43.365036899171706,), (783.2269959803796, 866.9809077598383, 521.4512147130841, 458.042522097682, 964.0261831220287, 60.825407494505825, 478.9819109983633, 401.61725451256046, 686.0974960622327, 490.26885414422526, 909.7008291152293, 73.49071576645049, 80.79047741079192, 608.29742363344, 65.68223332011391, 275.0159995579532, 633.0767243010155, 548.3564340483606, 325.1854433186679, 994.6277558609236, 530.5568374313245, 453.71541757547163, 605.4267915353129, 99.17846167904199, 701.7794185460662, 852.7927372955751, 650.9166648813055, 768.9627301047386, 720.8399166575991, 215.0230663274969, 451.5549159652815, 228.4935743645844, 338.9316188351752, 453.4989029074473, 415.9896502614953, 95.08583927563086, 426.764006260958, 665.1078630603089, 374.3010234355368, 152.63892476872377, 922.9850357343844, 67.13330813664776, 831.7718884748544, 93.2301017036774, 96.56443256578562, 738.7959984887157, 811.7692852773923, 556.3707356153501, 586.465082739477, 561.5864139920724,), (329.6459814162051, 122.23128535455929, 353.59807963376767, 665.3405200029155, 750.2842502514783, 868.0921488690649, 721.0606787461494, 968.3986253114745, 600.410091224677, 351.646185693149, 577.9185183898549, 212.7388056720061, 656.7363029881521, 224.2448691075656, 108.21838192726662, 845.3734186013451, 367.56105061535015, 762.6056319368497, 574.1000043314627, 807.2213711523444, 845.1551613283581, 974.5466021257082, 818.4268595406696, 613.5732805354647, 642.6991638298314, 26.2538314535834, 929.0842909949364, 829.460789959663, 267.4477251541106, 180.4160719608544, 702.6987728656047, 308.98468882991017, 339.8246567772584, 6.10578940365869, 869.8627065364382, 566.3210947613762, 400.78434399951556, 141.87465415126866, 633.1720126555139, 30.65709838090591, 746.1117620057067, 215.13288003351093, 419.83249376450317, 340.89598176933276, 370.05309247700393, 721.5959677426732, 776.835619966741, 567.5935566143972, 84.95703997717929, 52.608826425521784,), (157.40989710715314, 617.8381819260305, 673.968710613113, 272.10284354621893, 661.9386928082612, 485.66170489099625, 442.04418669777914, 273.1668443647695, 754.9431436683707, 113.81750811020174, 429.91363339789035, 283.24647008044934, 678.4862547601331, 486.63275336425085, 667.1325587363496, 45.417362604427524, 395.26339608762896, 599.3249569444504, 7.687085899882873, 301.4193620184368, 211.23397922031228, 137.234805257327, 255.51950402145928, 328.1223559378411, 7.7299065693119395, 747.0141234297678, 175.6948018758423, 380.20744571523625, 703.6712633826636, 500.2623465562132, 833.3542024198782, 806.2001865667638, 72.07549659215783, 861.7643620225886, 42.30226156279138, 18.7415365856457, 921.1624345024125, 862.1100136120664, 575.7591607368311, 573.399680885843, 709.4989615689342, 417.69395984348057, 115.17337266379823, 20.85655902474881, 324.76817944551385, 801.3221543104522, 618.1252633042402, 832.0259130717071, 919.7697517413847, 88.12988129788468,), (844.484359814697, 243.31647482273365, 588.8712883029119, 523.9625430006316, 395.76669685933297, 310.27456183586133, 339.51328114846393, 333.06862249313195, 168.1327080456866, 510.4832845421483, 114.02663983855254, 509.952062322972, 905.9227315800505, 349.3752654723803, 727.3791056739043, 818.9486015248519, 815.0370057500141, 236.2688489467243, 146.44421827769438, 197.27180282398328, 602.3989852731659, 760.2152955468921, 655.5090105187392, 177.14612894722913, 772.8480892475602, 494.11702501738864, 754.4458252469858, 759.8771496077485, 448.905256995063, 924.1542583856593, 564.4917834027992, 635.2983190605934, 624.5217794418948, 864.2468748314161, 627.2174068997718, 150.9574013930769, 68.28625849575609, 442.20806383628485, 302.8204351390129, 274.67366748615916, 56.172120213078045, 507.33688528977814, 310.40785060631094, 451.9138637006822, 56.89005083395238, 831.6966316631215, 76.731001173455, 864.2500339252391, 855.2933714560903, 615.0083884155736,), (507.06781733395513, 462.7116589272723, 554.316371338304, 791.8177972653585, 895.8767655568026, 449.73370365090625, 809.8159176979711, 651.8374546486099, 321.52676288042835, 475.6290279430695, 150.86107669563853, 61.87370010110072, 103.50187727039162, 899.1268339557736, 343.4377758382676, 714.3155491674627, 504.5490015009574, 172.55891143636492, 247.74372359828422, 437.758274309233, 439.4217917626243, 522.7480352655456, 158.74620798154137, 372.8519821013271, 282.8935786144179, 408.7693972473203, 338.3671468414195, 597.8858623829894, 789.2269315636647, 647.3053569693069, 65.91185427971746, 94.50594751509433, 678.379344840081, 284.1469738781137, 723.7336550024618, 656.5640864104779, 906.3426971636773, 873.2796620605388, 333.3620360605566, 582.7395145864195, 141.42838058431784, 349.8207875358442, 967.6965076927446, 698.4799628118809, 391.9579843353603, 595.0412281515748, 938.0021995657609, 309.5818874159609, 376.67930605630016, 791.6619578635435,), (813.184783814801, 670.1163999947225, 828.9589728944645, 738.7746721294827, 685.4144402775762, 526.393339734174, 646.0248207334879, 423.40636632210374, 361.8280963467846, 362.59766898204435, 180.26292287684464, 214.19266120890035, 947.6682675343468, 486.2709208727273, 226.54304653388724, 137.5653531770793, 77.16508430087632, 844.4283886858833, 101.14076366789415, 770.874720363101, 835.1198266345559, 883.6821654925616, 37.74749235756125, 336.76437196428157, 766.3076044472416, 131.049041429508, 376.7198708225247, 162.2472120884505, 831.3450566891255, 771.0978137309884, 809.0437196393011, 165.5391657440587, 437.67340513834677, 410.85861149653346, 676.3629221885329, 237.53020144692505, 444.19870980527566, 284.92793256335824, 748.5365180954363, 448.92796303334035, 534.0111496982611, 309.46789656278963, 808.6238710907863, 469.0156050322527, 835.1133927257074, 367.8409582250328, 947.130170244123, 984.4397935315773, 461.6799784409892, 281.7717327037754,), (381.87243419071046, 527.4597884614826, 966.2681532059473, 816.8912395812401, 801.2592241515476, 138.39853460343122, 250.00321158900707, 641.1790362044471, 874.116945052237, 554.5407454244312, 102.58973174840935, 845.8922767334385, 851.1660480847788, 285.06301405932845, 763.1168302916909, 272.7912995913566, 905.3062089782412, 147.3486559916043, 437.4725601949808, 946.4132630117607, 222.0380069069806, 451.1279902207089, 349.5850781386489, 26.67019080293853, 53.25688717107446, 502.00711469323545, 235.77807386343264, 994.5253512382913, 374.91267341776756, 28.18754552815006, 930.8259047499531, 839.1762876116056, 649.9606842941816, 791.380637482176, 137.59958772587166, 286.87939731326816, 829.7615831528226, 696.0719885759836, 138.7926918181862, 705.5361752890805, 448.6014739822466, 5.251198305617932, 79.22577127072128, 255.9239284370447, 834.963099282381, 548.8042454438354, 727.2347853249317, 527.7715058867243, 111.18686032423841, 288.10157803923056,), (301.15119458621996, 47.74944661885128, 419.82554375344307, 793.8991086394382, 457.1136166364487, 110.857895290015, 905.1468856619986, 596.7390428189469, 16.435352205894425, 515.3757302094061, 241.93813442099332, 143.57684024626005, 429.23889310333374, 614.8095827759506, 240.56423880654353, 416.5675952085398, 664.3713017420091, 85.61395498726176, 974.6544909522216, 67.67932290884605, 526.0594453221705, 507.3276965797866, 988.3314855964675, 554.1519524181955, 390.4537325600641, 470.135078158486, 635.67079146863, 981.0394225515603, 253.65026106921275, 16.242231108947625, 788.5200162795153, 344.80249316339126, 732.9410214506416, 628.2569624756565, 771.5013741098591, 735.1869848123113, 332.5186083719849, 44.33568829520817, 546.0137452076915, 813.5088655560882, 175.08912705170843, 779.1425934782815, 464.62289974703776, 695.389251996064, 631.7358477583379, 811.4976818476064, 63.10053703222462, 776.1903997034217, 457.6795774473532, 293.4425711750313,), (43.806275659123095, 199.46983371518834, 41.905941930382, 933.3709799503973, 515.3835892544988, 989.1227022961234, 543.0306976541859, 253.3137652026174, 753.290918818865, 191.1034307339109, 356.9741760353634, 780.841566978425, 865.7982770780576, 331.92468638134454, 124.4750082443834, 368.019174314673, 889.4865170122814, 743.3077055196212, 894.6374949550533, 386.64476826069216, 973.723584315309, 496.20322653702266, 497.52339249362734, 924.3104666269636, 519.275853534942, 801.1480874017738, 727.0813243426358, 78.92700605546787, 602.4532988302273, 822.3412795398867, 545.4743973446369, 321.21142821459404, 80.06891107499526, 660.9192214581367, 306.49585609075245, 602.6216277305998, 426.11607288304543, 689.7648084454862, 351.54698377199213, 42.355162850129524, 870.0371750563921, 352.5593103084823, 998.1505977730491, 274.5553600748576, 980.0272791942791, 947.9043786030863, 75.04116498815927, 637.5125378832984, 363.3111306509823, 801.0959755621699,), (679.4106078146899, 952.7893962796078, 142.77946836254972, 607.5729033208553, 781.3119697434665, 34.79896579854203, 67.23336306210881, 778.5153735068654, 366.32847310714857, 382.8544016887777, 567.2446417241194, 605.0948288547855, 679.0620569133949, 948.8235292655564, 372.01337847068373, 763.0844717985796, 573.9217783338956, 529.4598815362897, 398.03404595244996, 649.5607367060315, 249.61165309339793, 113.44861258501804, 735.6748594794277, 499.0439602564668, 386.9873800392635, 561.6727107596471, 261.77667658749584, 260.2897711399034, 446.2731124056162, 996.3651121547607, 285.576877698158, 916.4789095418814, 491.20019525422407, 122.63742190397086, 852.8262903843226, 452.04268524539737, 898.6790307862303, 445.11119274245505, 87.79074110473107, 681.9292602506372, 845.5212189746994, 319.58777209997015, 347.42529473856456, 64.93907831607115, 542.1713612255624, 891.3316823538889, 851.3620507531259, 711.8091040072127, 927.3244567231777, 637.7000225640163,), (793.6963838450027, 508.7557451743008, 121.36245507845689, 200.98037117768575, 138.87687203836575, 790.3730608077492, 26.284026808265693, 554.021437259595, 368.9111655012207, 803.6617262885865, 551.6469339264276, 611.9483626205209, 86.21548165193272, 309.29071752846215, 999.5950439343869, 718.8696598907195, 525.6956548303001, 769.164550374698, 823.3393995083907, 73.75071291123902, 972.3797315137659, 642.3385893863814, 449.9744956629961, 680.108990649581, 344.5147807026997, 877.9601516504117, 780.2629288383084, 639.7939293666631, 181.96313655213737, 966.2646139341068, 432.61828648197485, 910.7122709468873, 55.412850017777075, 124.1611206390122, 153.01546681020062, 164.65707989012412, 322.6607511545556, 709.3321325292459, 346.0230823418731, 940.9040549315347, 894.9259168211245, 845.9337061558656, 250.60530898199906, 635.0570913877459, 550.8414154831397, 125.1702951269027, 302.8246061088761, 533.4780297683806, 502.5731447412627, 168.6359017477068,), (941.6069878685959, 154.19426687956317, 658.7328733837543, 720.632768437273, 605.1389078618554, 842.5300041133889, 563.6180344312958, 825.2362673266321, 28.373487898877613, 45.46180329134197, 641.4537338243601, 576.771231477379, 651.1299774855999, 766.9590009163111, 416.5867836632462, 638.9911922088704, 498.0380601361161, 627.1640102980843, 289.67165680160576, 956.650169954405, 482.94481817541947, 804.688154215733, 684.9908411956767, 297.43387142974234, 72.97302550534579, 59.91303293466898, 439.605499920411, 484.2511301073592, 204.0230135633987, 606.6602628206364, 312.582499246933, 718.3628851124375, 734.1997548045468, 860.7773657543253, 975.3741279148802, 130.76615155238648, 370.5401980502913, 561.6512157483692, 319.1158864031572, 466.4725670406364, 267.4716744541824, 247.91888252554563, 96.81165256057145, 290.2120049699015, 384.14983350160605, 615.3774441639687, 248.27028038118658, 865.3075017207177, 159.69966213016096, 327.43582080119495,), (577.6870378935964, 312.71492086227306, 763.1213751386285, 498.2655420599088, 514.7248392314845, 498.75970839765415, 308.5404820544293, 23.176298216351032, 945.2328040692373, 505.44446302377156, 966.6866305524754, 215.1444225262411, 352.89508885635536, 50.54040462789189, 494.89420352517635, 882.3394763682011, 654.2600368896171, 470.5868533681957, 536.690749288927, 847.1723653934679, 430.9277693475405, 882.4557309185928, 727.5080633593919, 763.8567641619895, 365.937352517563, 400.5816210147566, 570.2816438810293, 194.65530188771908, 553.2229266211517, 73.53174974284182, 504.2555291232047, 764.4041147072396, 279.720677623982, 989.0907006207226, 680.3986401188607, 118.81101972358954, 975.082815413784, 393.90371272860716, 794.8972283809458, 339.0852999525653, 938.9485669553754, 754.9651722927939, 199.05788155244997, 509.1225162251821, 500.07790357064385, 45.30335246707473, 137.0363735675204, 333.0407053527672, 473.74415039519005, 456.98855928287395,), (606.2605224038041, 515.505732147254, 327.96584763652623, 613.068121064678, 162.5020458769394, 990.6157375611401, 739.3193605736042, 299.2343425283183, 336.37345215167835, 828.2893859573759, 532.3398298758764, 708.7398064354379, 299.7905647374137, 815.7488332438242, 368.3578098657696, 673.8063924730166, 979.8980311791619, 583.7021418487864, 796.7548139301472, 725.3242125518553, 688.0436512027717, 26.647154963833188, 474.59021408506055, 967.0706961720509, 782.9039914314214, 776.1620251720722, 577.6343958641828, 721.4001122963067, 583.5232770476399, 170.51206174665933, 629.0252411453637, 619.7358055010894, 841.1671249582643, 147.77570830033181, 680.7268950506789, 31.57051334209937, 948.2051707843013, 109.89552133369685, 18.937367506612567, 313.6924834458552, 151.43125811934544, 690.5002609185254, 410.37740932270503, 774.972301807091, 920.5209498972107, 872.8177089123203, 735.8372712686813, 62.28128601443195, 138.0824857852102, 207.34170497124472,), (325.0495344260548, 662.2267997143491, 525.4771514000354, 313.75259873781715, 173.18242385732773, 912.1241609240104, 342.32701768184614, 354.28698864481277, 771.9897841487408, 720.9245613272926, 643.3090999994615, 693.3133100298209, 610.0765800787516, 192.2641691367134, 246.5191355273604, 558.0866508074041, 224.8670379928167, 972.910627590569, 297.6145652769079, 289.0041374035249, 207.27779485464026, 704.9882597401968, 317.04074501844804, 348.8031742013066, 933.7003747708006, 795.4053560023335, 273.45753675542306, 121.87410573271507, 676.6222457825457, 379.69418537438247, 980.1605373213622, 818.3774601305623, 954.6088633914613, 804.6158339565703, 290.45265199293556, 287.6303416141399, 714.1412874983953, 346.3635140956165, 442.37611186483537, 256.44397547348206, 479.0792630164832, 202.06800720059647, 538.5779237086527, 933.0239337833077, 696.1713006468226, 137.27295480092295, 615.6770344144923, 586.8304985708152, 242.4580384102868, 669.8339648931039,), (531.0414897327992, 637.9445734086377, 52.49110683946279, 413.3013660482586, 717.3580562502436, 100.5449047773479, 770.7660580982791, 5.18144640713003, 550.3525657796166, 929.0996801699778, 406.90745154688346, 935.0320985961515, 878.3996215142282, 477.44852044089646, 199.45597466760168, 963.9140388909358, 321.1677021191667, 645.8979178902698, 907.9369587356224, 89.46072051151633, 574.1333531753733, 535.1522768936047, 723.1176782424234, 936.669379750626, 913.2297256698525, 175.0647754809631, 882.2449731648331, 175.78870753886255, 919.6348118509339, 997.1718030885099, 396.99457427829964, 495.3838973217578, 936.6087447777628, 962.1313800833424, 926.039697896443, 876.7431679179234, 9.267168480099786, 567.9618686644953, 107.300690773538, 982.9938883710064, 284.56165235004624, 989.0994700887234, 543.3004835569749, 493.91242034181914, 938.5605017408724, 851.0597385779967, 468.0207690062901, 192.81139827380977, 112.64676102015447, 162.49426253973866,), (458.9144710323612, 257.2648802295191, 186.19906912806772, 736.6178954922569, 790.7676644252557, 567.7812224209393, 757.2827502575612, 175.49491383846473, 856.1465042734577, 897.0427617839752, 826.9898252763096, 515.2806589597959, 86.73776984835591, 669.2558534099542, 184.78120104733998, 140.61187563429846, 323.6016700850828, 248.04708366714502, 260.78527706544384, 235.5212528015339, 753.756651781457, 954.0348226549125, 301.9458396916147, 722.8825284009974, 11.43573419416799, 653.6833670701986, 692.7685904227388, 62.12433180053611, 118.22484780397258, 306.80634200361976, 405.41661296839726, 502.52047123838963, 895.1183698162541, 703.557035102256, 310.9779483846603, 117.41574584477632, 916.1303858506353, 295.0376001562468, 614.625448143849, 219.1286009353439, 133.56878219680414, 153.18556468917922, 747.7348371508565, 605.7389480890694, 415.84561543122464, 549.2345088183648, 470.8280768793115, 537.5176876932009, 664.0944393396638, 218.41162265732595,), (247.46542930857774, 754.7395497248654, 873.1350584877333, 81.87030647327487, 446.7479774964833, 703.7661251558726, 78.10272214744019, 564.1687222734748, 61.75804755668379, 547.6492487109315, 505.4870560925958, 572.7016742278647, 149.8523813824143, 328.11763774220645, 520.341541184787, 116.24002218466423, 205.40148553936154, 583.1476784410175, 90.94164445168784, 510.37535403283454, 808.6920831772371, 453.432300179782, 513.2478432016416, 456.79847571187605, 57.736780932524766, 462.3783057389237, 806.9153525543925, 723.2800798250023, 395.9487099532575, 816.4532259331305, 745.8044828318157, 578.3112650590353, 45.289802727828786, 344.52886656213457, 63.75991211208321, 994.1236604769201, 934.5827988849464, 69.0191461603138, 933.7755625849306, 31.734871023079037, 408.8669358192483, 768.9720625834827, 765.8276829237117, 978.333284924101, 645.8808180971635, 420.3619388823279, 992.8565985808789, 382.47961885137204, 869.6202853107085, 906.7673115245726,), (375.6455338019876, 682.7303541015414, 661.7925381254681, 539.3002639188602, 653.534098418702, 347.7698871391502, 178.47362900411167, 537.2584863980012, 528.8425395440092, 727.8581409061281, 222.6902159662665, 3.473294944907779, 22.735327321966594, 298.36298870298185, 673.4998577765671, 544.4453390250208, 531.9336084967588, 823.3604373757844, 247.51203850377556, 346.15973498762673, 275.6497277951273, 937.4103611350669, 725.0239459089183, 112.84463876647732, 809.4781835391944, 419.2405984917521, 766.0534675139033, 883.7566218453745, 15.645796363549568, 206.08162185021416, 100.89671309250703, 33.57627575428079, 597.7848967315526, 703.2862668649286, 48.6763212437491, 740.5410784798617, 402.2653508109063, 234.33927848756164, 217.26921017755342, 863.7302426700563, 56.44403502446094, 503.8958489409345, 289.26345135391887, 815.7862567633168, 731.5174831160183, 318.903696369192, 597.9176742772623, 672.5319014369137, 320.6651153929831, 301.7644351442033,), (143.26043416332868, 660.2124238107432, 221.04274044603255, 300.5009537574196, 60.957637106762164, 948.5202550267213, 879.7138909638829, 911.5776656205444, 625.9931376483299, 427.2005822945435, 495.62078749302094, 972.2902353436858, 941.5864098319117, 671.3425247457224, 785.804595980518, 318.7344573163122, 416.3246334214447, 149.2176078251637, 376.46018850713205, 754.4160972381253, 473.51882041221995, 849.3409322600405, 300.7364187951478, 707.5767974879722, 805.7761599348869, 914.7411738159054, 562.3859495869091, 967.7861885466267, 557.2867581992731, 134.09275105907804, 242.85851608958575, 203.3367330600544, 646.7058515479447, 922.2261045112224, 847.1333859196119, 92.46399652686365, 724.5847123072019, 190.4816184307383, 268.4615878549812, 673.6719206345784, 602.9220449890759, 873.6204584895396, 188.16329393275532, 761.69641753721, 724.3052398521492, 558.8504762725273, 479.3942064709148, 869.473851524538, 332.9643108188213, 957.0197605266732,), (15.333706228492838, 937.1597632022477, 962.077556114459, 117.31619944333448, 999.5720070178116, 478.9208763423658, 242.59318184574153, 604.4015340787812, 204.5131429778937, 915.1264595935564, 552.0792925478145, 775.5138820637702, 380.66174437292256, 533.6501274162631, 359.2595555515757, 261.5616273853526, 512.8165400237735, 497.27729310098334, 98.60823156548004, 981.3184568465767, 469.4904220225584, 839.7311815636849, 914.3304988974891, 370.7049214312584, 413.9301705786246, 562.5247274643543, 221.27409831622523, 145.92271310254856, 260.77410990976955, 934.7582502963535, 579.1429260374005, 417.5780735551736, 152.41141018414328, 329.8652859778599, 379.83977474115414, 833.3627152869851, 499.30148236932246, 654.607966121022, 684.8466123459061, 257.32675763488277, 821.5919396923563, 966.5082672503437, 641.6944543987072, 490.5955825807824, 168.233645951541, 794.9755143536341, 169.2657108601817, 720.3135307620937, 488.3163212541324, 916.8993896943668,), (542.1368553513552, 641.8094631823666, 58.732051580730136, 33.82375716469055, 846.6973831827223, 945.1881112008982, 668.2155433931528, 764.3388435720192, 412.3922224155927, 842.5447168253485, 231.43339207744552, 707.1695637034599, 9.141461690661767, 505.7329196930651, 373.20069268681186, 617.835235876803, 666.7547295533396, 616.5193610843379, 483.2041536218251, 487.85438400307123, 6.6123569921588965, 551.6435609112568, 11.850968127892436, 529.4176468416506, 274.740737197832, 977.4793482325117, 17.1425976024272, 813.1572209139099, 674.0329521192103, 806.1676989474289, 909.7733659987662, 107.0164286944929, 96.31389025140847, 148.89748574025052, 191.9320569916132, 526.4559854002163, 815.2143907132826, 267.32473667663584, 396.89642249807144, 373.05158346368216, 406.027409873886, 565.0021824479691, 990.2330316397273, 225.85725262742218, 684.0416254703855, 847.8671020315358, 653.7357195591532, 858.2191590211871, 759.5858501768448, 93.50050542710942,), (379.2640222398159, 552.7014395296953, 56.1149391289657, 9.450172654130618, 171.38357522104764, 499.858393112792, 433.9096519716623, 784.3763107901111, 565.856627951035, 857.9603133636695, 95.36183547073007, 528.159185641648, 42.551757617045105, 211.41705588454718, 868.1168905816056, 887.5543070344936, 475.5002876452733, 46.56197074174329, 74.34805992565107, 925.5848100809231, 899.3116508650087, 563.5098640479836, 32.90178015347189, 928.7663612546593, 314.48469322665096, 961.4691898760058, 587.0361040844883, 752.254469846865, 712.7113999493597, 398.296020395122, 76.93749144134587, 162.45025071470587, 240.4721943042868, 834.650560091752, 389.1566073673588, 896.5257670027198, 331.72983962182855, 755.6092645208922, 139.9505942351973, 988.4779579141031, 724.1635700943149, 500.7928516377251, 974.3233274359964, 53.6964319473936, 437.08825284349916, 838.674657613175, 340.59274647510597, 769.0056533654423, 954.8583969146658, 396.7030493089595,), (773.5549161313224, 29.625658945165114, 273.32702862879734, 992.5858784507974, 490.6034561107933, 355.8111977058479, 941.1428449707254, 431.8479462395447, 679.6948580881589, 660.6719075148754, 85.69411763673574, 618.6158902598878, 798.0551738027466, 713.1085326783857, 82.03800967314857, 154.22096386551843, 711.6771547786167, 633.9008819630509, 739.6552889765946, 316.67822868153615, 106.55091771126112, 5.195222370030117, 308.26745513629805, 359.9174973468966, 269.7664432393865, 132.50700449419938, 187.3917835278772, 448.84367702145147, 554.7400006088677, 408.04417508313753, 26.261904013297197, 353.91428734525886, 93.06426045987415, 598.0437977942729, 324.4303305724127, 385.2379173959837, 291.847351296321, 387.7995593783937, 84.69951692852251, 901.136044856433, 905.2075743133588, 978.1730640138873, 571.9604307495395, 169.5829233923255, 380.73202348874315, 138.84005895941675, 301.1312653750687, 493.1239422107847, 63.267150276954396, 434.67626926274494,), (421.1023397725243, 484.2313139339285, 76.92136139515715, 251.69973778890287, 246.59006834700293, 625.0336877688947, 593.8063980390787, 195.54822488026048, 106.972367580902, 304.657995751257, 948.8234623945651, 332.21721531162694, 620.1921878746894, 804.0764619573824, 329.5417162792602, 334.736223746957, 815.4754700030031, 859.5084671008352, 974.2253765046069, 136.1244715782648, 320.66515537508764, 947.2789220161162, 200.8514887020717, 314.18328119544134, 964.5746230947939, 968.7252217466955, 291.4481515800532, 694.9577676721101, 491.00731289210233, 575.8792816249606, 242.4242967805731, 376.0553023241471, 816.4945154329131, 392.93512973945985, 113.88782361199812, 563.850508628786, 592.2270342503855, 545.6290854508221, 681.7126331300876, 550.0991569974728, 953.004611486296, 461.62222283314844, 708.3670512560635, 438.45495430890855, 291.33120798529535, 692.8352793243428, 818.9655680044585, 795.6568359959543, 409.14159024490147, 499.30321528896025,), (633.3360396536635, 242.02116767079883, 658.6629685323181, 715.2363912994261, 789.076763220063, 73.96513558228845, 990.700614436968, 479.23469977262624, 400.80509732937367, 506.61264302950906, 920.3921844782957, 691.7088658981077, 543.6452033874776, 790.7209170492591, 359.5294930162599, 895.501515360899, 536.9059860245878, 638.1803670108411, 84.98193405098775, 768.9540479200405, 657.6016445807657, 355.0088194484896, 646.9998479834043, 44.2966935278496, 983.6082059519828, 677.4718958641856, 399.61774622508227, 752.6827777296058, 965.7167777138283, 430.45554389018224, 10.547772784579967, 258.73837040365544, 510.6762405250447, 518.7977668493495, 580.5182955240041, 575.2353982704261, 445.7785515049068, 391.1341686775028, 772.3422324624321, 588.5899565401472, 500.4657816197345, 344.9673875995183, 24.562653025769322, 104.54935800606168, 415.9754285257126, 961.7278656799704, 116.06933795897467, 940.676158146193, 141.6751768315483, 311.89034389754147,), (455.3326352258771, 206.8673262083145, 482.92599877938846, 476.16253120916355, 438.16593827302705, 696.7632655236661, 318.90947421770045, 300.2640831819403, 810.1859369186981, 115.08526669878594, 849.1800080469097, 647.9699172777136, 677.139332890884, 164.35409285070324, 983.9004705882779, 243.9129465519927, 174.45323282413395, 160.1357112153593, 559.8489524898631, 958.4626217339497, 231.85554741141036, 405.04743628025284, 184.45177515139366, 640.4788766600781, 432.1344524292825, 29.19227434239058, 614.1069373719198, 197.32443578224635, 592.2031583603683, 388.8357803557071, 704.7356159597344, 205.78447936732192, 752.3254953604917, 808.7297886312608, 62.56375146916437, 101.75204872714238, 871.9793300098852, 186.9598356320934, 325.9849115988185, 457.5504222061855, 262.3533954523609, 862.6365474573073, 527.7150196277827, 639.1085856661506, 596.9708292829935, 611.3084211390019, 587.0047145652179, 347.9246374367544, 845.5178026695593, 617.362679336425,), (813.7382542609338, 705.98836094598, 297.4448346993519, 614.4845157129195, 84.7519686230278, 133.94776965071065, 117.8616526616567, 305.38000253549893, 183.0445183314835, 693.4365417164455, 510.82486948716087, 418.2391062112586, 137.86729853455748, 383.709962837687, 185.75369929276565, 635.5016420586353, 693.4329265293553, 645.2600950799504, 999.899552563092, 554.9125758324974, 489.64202619220885, 140.29653509779706, 314.5800146608443, 451.00097074665115, 53.61126263083682, 359.0391721713688, 9.583439563281226, 136.5347146624959, 815.2159406538618, 963.8290894149399, 505.43801973067036, 494.96984786640365, 684.6966705599865, 415.6304352741119, 839.8918021012228, 488.69951193968564, 82.67062578646689, 30.860705643004806, 761.0566185454242, 292.0899095587066, 274.852918273534, 537.6086182048194, 168.2089774365737, 457.3213872734491, 742.5182519297871, 765.9195549436906, 549.7261845380514, 113.21099529202317, 114.2066513589688, 775.1130278639955,), (823.2828079975666, 366.8617721054209, 822.6109277962613, 41.61052227023332, 718.9802411300433, 546.3532747219646, 989.7757278766833, 102.4164388774983, 830.0707165425397, 751.3454947436721, 297.70893510289363, 999.3126692789077, 449.73234283041376, 348.57697682231384, 816.7285851164386, 439.069903383333, 993.9576843186171, 775.6316498807736, 236.94605536668124, 810.7027168394102, 587.9238969768106, 350.6308411139897, 710.7539594937995, 632.7706309271384, 165.9816176902592, 139.23496592677608, 206.61965618677337, 206.94272075176602, 59.35783363934111, 350.8154789528309, 281.085018790198, 538.768546047638, 323.6536158546817, 704.0537617551885, 289.3332434649436, 267.34306627808013, 858.0168449462576, 985.4883022617942, 679.29931592331, 95.22516381434276, 962.771994993792, 785.6910482912973, 918.7687118298253, 992.4862256446744, 867.0475904337784, 126.88816861381025, 866.0787949911568, 249.6772419381398, 711.3948488391691, 828.4818026986326,), (761.473587479857, 676.234553699946, 489.4587259156378, 577.4255293041615, 268.71715208748594, 414.22508936503766, 451.99172255036433, 633.6277633502976, 880.1250813073235, 93.09478404261739, 515.613472087699, 278.2256878517837, 936.3361140885752, 369.071174075422, 950.2540788653826, 327.2892801609303, 2.4730851419847433, 774.1352904376932, 732.724026539487, 730.931937487053, 458.4492566797177, 664.1438208318425, 358.2227293409872, 63.33068606017467, 534.4244643875649, 217.82993501520588, 429.64310068523616, 211.85146640773823, 268.53683831442254, 828.343617048808, 337.75515517078736, 577.9336402609515, 566.1421109171403, 485.33790400850506, 343.7396205526192, 682.5519260932059, 48.40926115172295, 99.57474191620585, 783.8897618405682, 459.58176267356686, 124.23717923039845, 857.6515999286138, 441.2859488764266, 0.6759315121042109, 958.0317693039723, 202.31820639739973, 688.5918819115103, 131.91308738401352, 649.9971993406527, 158.97746290581938,), (932.7255627259242, 274.0194580952822, 654.5879644187941, 250.38927854910887, 371.84376764676574, 903.8002688356579, 165.5250791593438, 396.3415669322332, 305.5092448442307, 699.4413715245736, 234.14384148441945, 655.485228383535, 703.6980397640442, 1.086303691723689, 476.8067082609141, 132.69979203998662, 226.19086145940602, 679.982725121579, 9.28694760846016, 695.5971072880487, 817.1090269132984, 988.154909464272, 422.31393377505987, 132.17515109256084, 70.82830540051211, 383.0699256757727, 730.7633817639711, 102.42717044950666, 313.3514774409062, 880.9889949802706, 137.1292947435456, 773.4604836506242, 753.157800991068, 133.14623118621216, 992.940155246385, 142.85306683489384, 530.508276546681, 8.474741953009568, 650.0202131578069, 440.09942077985187, 722.4320263643224, 628.0800383409025, 151.37413084428098, 411.70989435967806, 686.5661698757399, 859.96252460215, 86.68803346598852, 100.46511247763878, 752.4456465479524, 589.5739177615131,), (384.03190934312073, 963.2487105532641, 314.50366818216935, 139.8301888092337, 276.9676569448454, 84.24871599798666, 553.3966317847301, 600.0078672258529, 607.5930989155232, 778.9696525132599, 690.4760750146538, 847.892104439959, 658.4053700851439, 301.6493335771986, 517.7491277972375, 509.52256611471256, 747.8436409812496, 295.5420512420667, 54.56913101525296, 897.9125603569896, 954.6715113471528, 494.88771994326686, 112.74366260380853, 499.5825412726576, 593.9297681377232, 528.2865009361842, 977.6969478433356, 986.882532843189, 933.9244016742159, 131.98288073270203, 860.8140039367474, 568.3803887729609, 365.4124628480987, 682.9420490024262, 762.7259378955857, 954.4529839408864, 770.3670492246139, 16.689401770856204, 67.53256927276719, 262.18527716206705, 39.82686193718976, 60.46885958820569, 789.2899989930913, 506.61060427492697, 628.570696060576, 501.04896550707514, 415.4319943754019, 701.8106386235163, 82.42781192464588, 536.5648160700154,), (616.047055666748, 277.46773573456005, 309.90688561664194, 511.30469829533143, 203.19749395782782, 808.0600809706533, 536.3901728894583, 390.7315029683497, 634.2942473532567, 834.5264691847527, 681.0556847092104, 66.11508362121499, 698.6758816054913, 729.9583774315414, 846.4681151407763, 57.90594402313043, 86.21279839148932, 434.4835886060191, 453.3718422971427, 608.8324044274435, 309.2900638735262, 741.6935090575543, 740.6581802341782, 119.44300690788745, 707.9006071180984, 701.499611223866, 163.83263093540756, 953.0162017546976, 523.0053627284083, 782.9871015437459, 720.765518093459, 166.96295419238083, 126.93060748112484, 781.1239799369876, 268.76448640924565, 886.414759740656, 771.4302904896247, 29.356251172803493, 807.1078466573698, 271.98059502135374, 63.87198912314807, 712.3232482004954, 576.651674714184, 77.07003128638534, 455.1977776794275, 360.1238955217533, 499.61000576138326, 566.8861282384498, 367.68777785681175, 255.11608103907813,), (102.9049150218696, 573.9123051920565, 722.7783733258665, 228.40442315325106, 508.8248849106468, 44.03790726280954, 862.9284949026113, 244.55145035670344, 471.76458957773815, 382.9794743673991, 150.08466824115007, 931.1600771571855, 857.4851977479724, 552.8647884958646, 913.947669521129, 740.6658281065769, 419.3693043627985, 321.8020144720613, 416.25659850131535, 720.2879214281971, 271.25795800981666, 77.88706996691197, 372.8081667378613, 502.04094255958273, 901.9410092582714, 179.34435027230566, 804.3380194587567, 981.413863941495, 954.0794074604277, 68.9264377727451, 465.0941779168861, 282.30687062090243, 844.846504261328, 327.30092172611893, 553.0913953240675, 7.969173557455522, 200.67115345295582, 563.8067408241869, 303.90930726808296, 622.7175238921045, 463.92669058049927, 591.6909349848162, 493.36067585488485, 772.6132629505244, 195.4224238728235, 900.4432972264233, 760.4822203216966, 245.12687437658286, 6.377882751804065, 410.0361095660199,), (232.99774185974297, 346.4236977394062, 839.5740068961738, 877.198614251575, 950.9902621434433, 1.4620405355777466, 657.3038501759978, 849.0059877789217, 727.2150390409726, 103.94901935155964, 529.8144890600125, 238.16759452086566, 492.0288768498976, 59.895075653189636, 996.9592701075485, 711.6524747001027, 93.02663835808822, 921.2744029721064, 897.2874719314592, 519.759218492037, 700.8469096986796, 372.49197407408076, 974.5547901682511, 84.90241751648952, 95.57700533738556, 133.51385025095698, 819.962891894216, 74.83048432987749, 567.8207282892083, 434.9828784349853, 964.2182747327408, 236.7717347418965, 260.99093892361003, 315.0089154097583, 800.8168219994952, 700.7256063215422, 735.3528687823681, 318.0577192661006, 271.9558718324544, 74.68743580247971, 202.7126262792337, 779.9369824688629, 584.7080358041289, 155.4105513219367, 164.3749806997682, 466.0551124366618, 406.51255190921376, 535.9245490900237, 964.6347222502287, 207.63622561035356,), (308.3077182125046, 265.0079269147082, 119.91355635539358, 157.6153770655162, 686.0548429505152, 826.3866879396497, 696.8779832370764, 40.32980810866338, 835.9247509879863, 327.79365888479197, 91.21290379134861, 248.21878946125508, 355.7427555042837, 513.4596753787295, 677.1833769890718, 260.1667927212239, 990.6766204079072, 31.082093358759554, 404.39222644717466, 452.2011694010627, 748.0784472087092, 249.85262822155107, 462.0432488762389, 803.8967534438501, 139.79303927312236, 11.956923169412992, 830.372331894636, 982.5672374836216, 130.71523754397796, 823.6734151883558, 372.23974381599766, 630.2975486038245, 644.6850157085017, 582.3238014140632, 258.82065234652583, 812.7470916136143, 21.800182738612726, 64.47203604690976, 902.4960819332814, 443.43132383598885, 128.79154899700453, 905.0779703801529, 829.3561772174179, 331.5498912522767, 42.69631909043081, 460.9949220019407, 167.98920872905643, 573.8841998067228, 821.6854015322838, 394.9986882219194,), (29.48229300029148, 683.2129167516829, 172.80721183078919, 214.71610597002666, 187.15111371116166, 279.8545542807238, 883.4237988936261, 34.64944373150591, 619.1765488458958, 245.78477924524543, 295.10516299971647, 411.9922062961651, 550.6886652039806, 60.97906893284277, 279.77598743897136, 137.2360171320648, 199.45780666658663, 884.652516224965, 525.8140582527797, 630.7543888247554, 802.1680827711275, 794.8462443018702, 989.403615761534, 781.915766845425, 359.112109837565, 544.5178060340517, 484.6794854405083, 912.677016598367, 502.3932555437417, 388.3812745693004, 179.81587369786922, 318.87756732937953, 219.02018807309454, 895.7647816143137, 778.5382813284745, 58.59123027811852, 991.5313234305709, 529.4322919654317, 766.8421684502729, 999.6057468113461, 973.9798617335022, 100.13433730495758, 656.8644213762342, 266.52700106568307, 816.2851616087347, 917.2594825704406, 55.908655852449755, 996.3920485064572, 219.41158604392484, 846.5050904651331,), (797.3906873794087, 354.80463456515986, 839.2222523059093, 845.2209023662808, 176.10036757999003, 592.5195091814127, 806.2102253593883, 697.62656359958, 913.9800041285343, 28.20656972715907, 700.5608033514183, 947.5587350700152, 563.6064867831936, 563.1088908640183, 188.2336649052563, 988.0062104046431, 881.6263598977603, 492.2267238954052, 309.05307215121957, 490.4364201274792, 90.25737685090284, 232.62335808323908, 218.80894169157727, 526.4485141485109, 0.6834963838308061, 917.8961859206465, 201.4643833521411, 130.4895456191826, 716.9376326908161, 918.7807873072061, 844.2840864376508, 323.5888113048372, 21.912896243572288, 586.6091750912095, 917.2241474811186, 774.366498819892, 846.4808875791347, 860.6694665915508, 960.5587502757579, 373.5907800833732, 941.9232134680417, 395.5955623538543, 101.03217018068634, 301.7651537254691, 136.45166885749006, 157.50388601684585, 948.6935320263234, 791.8425249998718, 960.6621003687383, 649.180265644814,), (174.20267095553066, 968.7428963331677, 693.560398292543, 928.8452089854253, 786.9917925955768, 223.23721398064245, 588.9501557831036, 175.3516299902208, 306.82322929795805, 688.4984395989571, 127.3483647235355, 728.830955935999, 948.7881042212877, 948.6981598499029, 391.60142727760194, 994.2831305476307, 965.183940287551, 32.38274292708743, 602.3886674509712, 921.0382164180794, 967.5316856159355, 220.89525043701207, 565.5502461203205, 936.6877935542542, 140.64336814856836, 745.3433281019825, 237.99592514939692, 982.3796549403569, 167.88741675158414, 885.3202533951373, 88.72601257012636, 708.9658131452244, 639.102687904879, 886.6535152178218, 446.6299340977938, 265.21244056287617, 249.54086313527878, 67.79478006423378, 256.6579415427148, 108.03460121462926, 1.281157494689933, 385.93709795340993, 732.5844944987687, 969.1018251056852, 884.5519975872274, 493.0819934738476, 378.712574774259, 546.0239676784543, 101.42855963712161, 479.4892503971605,), (864.0507204685769, 650.9695631134773, 687.1604042171144, 162.65651174848838, 73.70938956137418, 845.7624840245603, 294.90861311613014, 318.7169079566575, 951.6621314341206, 74.20040176665933, 170.10520315119072, 375.45788407999635, 731.9850852713168, 547.0462586122314, 898.1241731199735, 93.10468538068662, 594.0305440520949, 613.6447257788939, 482.7367864884782, 30.99320405217498, 942.4422581019638, 164.9971947619141, 889.7532704040436, 157.10377733078406, 101.25781097333564, 205.55047671793713, 189.98425758838545, 697.1967038073572, 722.0381045756757, 729.9740056702899, 265.2647693499766, 281.52307344526525, 237.54828494724566, 49.63544303850986, 571.7698078577415, 839.854504845651, 153.5899345167342, 360.82892122727304, 427.6059574457135, 294.5494489662148, 662.0202260098677, 600.2155280137688, 199.67480076094634, 25.735692731685745, 170.8321916922231, 291.8114660461075, 81.93609471299234, 843.7690743469366, 308.3534001177294, 397.46734850812294,), (489.09681212065726, 661.0400547460049, 91.13687755067923, 544.1260292196804, 184.88122054204726, 885.492692749539, 369.40166493194505, 445.7752707944581, 263.2959666828842, 465.0579561546505, 226.2422802826669, 268.5619932729496, 61.639369660099305, 752.2351421152871, 667.4684591345684, 85.70731183991708, 343.7913865460467, 541.449006006022, 970.7055371735757, 589.7264423036034, 553.6019061579088, 840.7971664302505, 818.4052148653735, 418.63207493445054, 535.5144119789297, 866.8940137055623, 474.82438812192, 881.6538694176228, 476.26228108146864, 78.9550497740832, 902.7772013391541, 714.3056991303599, 502.10147031560484, 900.4550614715919, 800.4503367206856, 677.5348855760302, 620.273114349303, 120.28187475543328, 757.1887556391935, 172.87897280649446, 984.367990249133, 972.1648783377569, 808.4607213193631, 126.18599863853974, 423.37489931559946, 988.2799363369157, 435.39324553250725, 997.2692267787756, 627.2262122323979, 834.1081186760042,), (257.88635998777653, 910.8044857315942, 914.1739368985866, 67.01195304861241, 388.09915301475075, 397.35499435703593, 326.126805369736, 276.2008199072493, 458.15718112261516, 873.466253372757, 786.5506388277718, 623.0579285888082, 522.5620104498092, 419.59383422522865, 414.4423987689787, 148.1997412934446, 587.934133281388, 758.3773878576729, 939.6499849871099, 924.9281667592735, 562.6837594773225, 100.99044282544867, 286.0928540488724, 535.6334811459167, 343.8691785774645, 410.89003729922047, 383.04347287705286, 485.5847922863307, 608.9621519524352, 37.46567049771343, 275.4141420347538, 143.85267062667373, 608.655264085627, 693.661253167092, 38.78253248179509, 889.5742139142575, 331.4940757342707, 237.57929391630717, 745.7498198391361, 920.8349829061652, 897.0313621494225, 20.23288859327188, 817.3859253052775, 303.0549534480966, 280.3466531846416, 491.61921335931703, 696.1828446749748, 98.21739027274123, 868.9000250991986, 134.3855209684405,), (974.2194246033105, 443.11061333142885, 825.8233317797506, 269.4199810087636, 416.77272770538275, 645.5548114276443, 187.9356908926114, 211.3905021058976, 823.9694342551406, 740.944798920008, 759.4932952262567, 867.1889705502257, 821.0173923984603, 515.2697026095568, 158.97244263900046, 311.1230932889089, 506.7952752365554, 135.6498210170598, 851.2953409871922, 879.332591359856, 28.948159320185685, 192.7634468315582, 832.9299330765513, 836.976677608695, 249.49026132427932, 456.4486671280806, 918.0310693411162, 704.6339452200365, 273.9774984743589, 823.5062916891162, 505.12637493716386, 635.409809075844, 123.87285889069999, 30.563192381993566, 372.4667615353072, 594.0435934521513, 177.58369416386165, 870.4807719184629, 586.8802484224268, 349.7599016446545, 163.51361451785084, 894.4919203121954, 748.9611344849333, 688.8505376159543, 285.0693764347619, 386.5656707409847, 162.9074055986205, 572.2580610203007, 964.9176092022551, 857.1111874383023,), (647.379891761482, 677.6952460909707, 269.0835792689571, 409.50719649554213, 20.052357499524387, 780.3036642606434, 767.5727284118514, 8.897986070045771, 911.5154380523718, 647.3715541750241, 601.1419504945338, 8.463727114871311, 252.39044136065147, 805.0855691954863, 305.45969838455676, 967.0244433683766, 642.740311290164, 423.80680065961906, 376.4754522710008, 348.7091992720813, 251.99870927787603, 466.6980154972157, 677.1961563873133, 824.3108214706635, 397.15253997212017, 102.31210296860016, 511.51371746830176, 662.3554525262523, 843.2839500857772, 374.2297631610715, 647.3421200645904, 609.0501046380168, 298.4759262532974, 108.10453058584346, 63.83705389710992, 988.3609658217671, 640.5947456146558, 861.4916002518306, 261.14690792649685, 711.0937755717918, 892.3737976809327, 298.7911119189864, 149.92929635415652, 765.4743883036232, 899.6870447454949, 805.4298226335299, 802.3637444528116, 600.0333011455716, 660.530323681129, 680.7557503873818,), (721.2831975926106, 655.4005704624416, 997.4690995782959, 259.42627271198893, 418.5656860813416, 388.27492100545834, 35.31900482642425, 708.0689593758711, 572.0424340368412, 189.9151604113507, 726.5498765809035, 222.36319397626892, 534.6351015442132, 784.897055296117, 906.5265055050547, 671.8684759624089, 507.31485560681534, 845.4192442445952, 840.6386945964, 876.4947843421105, 181.13585780400078, 97.60306381270934, 127.94413969605334, 258.6518505602079, 808.3438691424701, 762.9182183193025, 183.06835701259504, 679.7123692899282, 335.63246320511877, 89.2998220194392, 355.2833961198324, 744.2099293428081, 307.08527133220855, 788.0904368075993, 331.31713697077646, 260.5592354057036, 294.05112165034274, 851.2138882114102, 470.5365676706802, 866.3933696925396, 583.5746197425805, 944.300984000077, 71.21573740407582, 889.4260614703878, 500.47737449270437, 867.49775469823, 381.6692198055871, 298.35575381746327, 54.06197064257512, 854.2397704033438,), (137.36525917312392, 200.29659079514352, 409.1918467521041, 569.4035879734539, 906.6209345061276, 457.57143306154126, 316.3834576487089, 715.668456110036, 778.9411774338146, 487.58084574810744, 631.0327722626952, 176.82426656346072, 634.4989691526711, 4.713501423993849, 273.5278622014996, 761.1932812149992, 168.60575658290767, 764.4837361457579, 489.5770906300164, 763.5691671054992, 88.0413437590446, 614.4797387250618, 633.4875407025942, 403.39348039795044, 965.3278401300331, 383.31642005346066, 37.725318474937986, 199.41425122811697, 373.10140542737105, 14.077118724632133, 322.21313384806905, 833.228666566564, 190.57670994040456, 676.7481416771101, 626.6869286716226, 248.82312287588192, 693.5247471703972, 344.3497740633209, 128.93080042830275, 383.55047630769366, 588.6706881680719, 167.02018726830937, 823.8438296744104, 298.20225349413874, 290.8277918094866, 727.8319114468128, 596.3699186155023, 337.83515120793294, 887.9740438370417, 995.4724098279283,), (342.73283925755993, 901.3837761445245, 359.25088636663236, 188.42603389086943, 948.0843537069427, 918.2054088414334, 403.3916445011253, 228.4182848358255, 727.1688281363213, 131.20577641718256, 734.0766193115209, 589.6928347992416, 168.98474189014712, 366.5914513998485, 650.5403801212248, 37.363427692595266, 876.5441375994762, 255.78917392529289, 534.7334341938405, 48.586961581737256, 994.6802336570648, 661.9699158378946, 653.5676184007526, 19.741766514145166, 689.749748377426, 416.77556517954383, 380.2537629276146, 547.2823847289255, 474.3963383277372, 153.12709812443637, 695.192029220909, 630.3113401629369, 301.1164547567923, 661.664576090722, 662.4829658888967, 269.9771495371891, 605.6343369240042, 137.16352297980893, 830.6527204708898, 104.91423690009694, 718.7662245161679, 117.73456843723929, 114.01255268424792, 106.26370830639497, 198.6466617062822, 199.74938614635852, 262.9939156137319, 523.1461075925806, 201.67318998218943, 703.4367502462583,), (295.35120100061295, 39.405707628568656, 496.3547133680927, 207.693984271615, 933.1243446518774, 330.6036161967014, 2.7379990573926927, 671.6331324652049, 906.880044269792, 835.2321652632412, 669.0219305016401, 149.1444926667661, 90.08267357592447, 511.7053469646589, 723.5636168984789, 101.29047859295737, 255.89210275066486, 231.16028649174592, 988.6659307576041, 296.0221787104385, 464.27735723614836, 99.80891505761747, 174.7018089453909, 39.444519968693584, 290.56717723767946, 801.5961026904473, 312.70710696276427, 738.5401957921762, 94.99847033058029, 758.2040048412383, 45.88908709341521, 851.9987250744881, 663.355027490844, 170.51227635637534, 357.5274042377443, 437.7145167819347, 621.8070333325072, 878.4755373974737, 92.9513154374223, 814.9642895364964, 182.86942030364273, 400.77751428075294, 962.309930149754, 271.83309075019014, 385.71548975931427, 850.671578036584, 799.8990969489502, 648.8464838722638, 796.9095570354069, 113.05655637764612,), (696.1698735237949, 58.64639566517737, 942.4669879999001, 159.39539748216492, 416.0277166308965, 590.7503029903385, 802.2647840450959, 678.3931362492056, 181.2605605373021, 379.7507791158876, 358.5989148270966, 28.81622957122154, 684.4641333814728, 838.5364765471118, 973.4445802253692, 130.65532954841586, 920.3979856381027, 112.93747937376742, 411.28426373517425, 45.972325379669996, 261.61286722848985, 314.2379448738709, 704.568176480623, 677.9289210201644, 767.5287182899103, 576.6490081233795, 565.0122256873914, 977.8955575507985, 669.8443582143154, 338.30064176842325, 523.103175224674, 700.5801842975543, 95.24213845169471, 661.713262845963, 248.57699485070762, 345.7494874575037, 676.2955683574412, 384.87661740671433, 839.0330212297873, 558.3442367021879, 987.7916247614772, 54.56611522767585, 643.3987165568129, 156.92737330174123, 848.8455747760007, 851.8711514460662, 869.4156346127679, 74.86524247698479, 491.6479611444012, 240.89181669723115,), (970.1450487834002, 50.35071452654394, 222.706523168799, 643.3173148232519, 403.27699346172284, 234.99999999128718, 459.09459474823524, 801.2612877267221, 448.0985856653739, 856.5892241780172, 447.068119913798, 118.70685205363284, 497.373348066501, 653.3730031383718, 102.64332148015242, 412.3385476755494, 557.1310114048331, 0.16971547833644074, 90.89554691153224, 604.2032760617975, 619.0661537463823, 304.5979581036485, 508.3377403194649, 206.85124884989835, 671.4743707791561, 950.3551299933657, 363.34247756981097, 54.27521330556784, 222.9181143963146, 454.4599826035843, 560.1508451060681, 619.7579696770842, 473.1336810935889, 657.1672064311396, 715.9135002831553, 114.0233583557383, 759.3199510791171, 221.74710782019002, 341.3571145768387, 829.8838856344012, 964.43368345434, 291.9835638082815, 521.9850866041359, 702.0960532599854, 45.77810985503427, 164.15536770539086, 140.44918732224886, 716.8561126478827, 721.5590954513159, 106.961505519519,), (610.8517661386837, 187.54340053160334, 930.2162591531453, 392.89648991517345, 457.0460082929889, 781.4204036487547, 716.7879144693429, 107.76574728749199, 414.4703451283303, 926.6265305373107, 837.4662374864649, 588.8111078979014, 772.1451736372288, 450.50459695896404, 658.4566950002027, 956.190321625751, 134.63601443063277, 498.82864210261124, 530.3643475190843, 48.571605631905214, 935.3082808316526, 838.8161106789165, 482.9319635359263, 508.3328990718833, 921.1623140438011, 177.20243850214555, 578.5543857041766, 730.4946617427778, 128.3817820515778, 387.40557922066597, 600.5460218054113, 882.6958635586781, 504.1101193531644, 384.6626387437041, 979.500760205043, 915.9102416970992, 762.3746562617927, 273.5958799561112, 963.5935376797527, 970.4916499121006, 452.84570608136676, 133.37183101609352, 412.7454728076193, 700.0142848971251, 748.426865877916, 298.91313495542425, 701.494310407385, 860.707546129158, 711.874356395145, 935.5120774625358,), (632.5832845072639, 200.88908232534442, 624.1675892053881, 290.5794954792507, 345.29358089858107, 672.3737130513169, 981.3354962348673, 650.0863878069779, 958.3264528051965, 503.89343904324966, 694.3119737273263, 322.3813974138552, 115.40525228362542, 352.23780246287737, 480.3699223906268, 570.6381942530993, 667.1572380522164, 417.4779331255218, 747.8687233765869, 841.3893024169117, 285.92798022536357, 847.3010914205489, 808.304779326696, 522.7299862092023, 25.272141254472082, 145.32671163279554, 670.2541787669418, 199.9024172647168, 750.1850186700538, 160.93539434323, 286.61554815910995, 250.56100745818298, 839.3818144651713, 690.59485036401, 295.140565961606, 753.593948045165, 32.30638324232715, 814.0262175544686, 102.4800063379766, 868.0353736544103, 739.4924801728499, 864.8414852046026, 742.4127358728605, 561.2705527908106, 237.5954725662045, 784.3071728708866, 798.8217524895705, 287.6386545072203, 664.4935685916236, 926.4848111687795,), (387.5546486419903, 956.7019700118268, 975.8227220561263, 312.6539808946341, 552.1245975330635, 12.965237674638308, 251.34112837230572, 620.561695611849, 780.9248928758853, 870.1180923213351, 829.9834819551445, 911.4863770737386, 704.6290688927878, 647.6323855500005, 755.1077052085499, 546.6151367012413, 603.3869772886416, 776.1467451223322, 964.2898231260307, 294.1777218044915, 177.4258984882632, 682.6588517015351, 186.97658153232365, 173.79513185530516, 513.8468527745739, 377.1982270526456, 428.5078144007343, 556.5986205953329, 130.06653011537227, 583.9860576839312, 255.01614813242202, 330.56278874226905, 709.6892963163141, 154.80569779828625, 153.70338356962975, 322.63002989352, 50.85544119522678, 932.3818655711908, 615.60867609933, 661.890210964939, 490.5509411536766, 572.6769905549301, 358.0855881637394, 784.0314485705014, 317.8590053721895, 219.96201542755745, 183.86909753535895, 68.17646910442588, 505.1403504563159, 415.9312437704217,), (537.0359525339087, 91.94555393906644, 221.19808110145277, 213.35381018619591, 331.8835149039815, 360.7978246627327, 218.4452857845147, 752.6556895545169, 530.4988956068246, 996.6310826043388, 823.6884009252022, 981.1331976152965, 8.803696295037412, 668.9384679943867, 445.66724628469245, 904.4964584876922, 613.7010970607944, 621.131697623634, 958.8968160739336, 682.551360768528, 320.9370105070815, 915.9324302986754, 944.9876704318376, 385.8759347840439, 540.2373129425615, 282.8289942793975, 911.335926628618, 822.1090642399662, 374.95804216197917, 802.8167375082938, 445.556897571718, 43.85166091050174, 898.2816089058533, 192.51238793367708, 513.8947001274336, 948.2118196484181, 167.3948092134949, 956.276206473399, 537.974830964251, 7.330586216845125, 65.47182210371993, 670.3351261430566, 773.5822663692767, 864.9508557126312, 424.20945194114057, 103.92680917367014, 537.7542140241612, 702.7428662675412, 976.2239213390278, 775.2067404630612,), (646.5611593183453, 939.7511115083134, 746.8908256742621, 153.7545772176455, 459.22523526004164, 331.7153754672878, 87.52089793419859, 54.268070891441035, 794.2147869758903, 558.0842053178108, 574.9622942627916, 227.64563494165668, 258.64795899117564, 388.03260364477075, 630.2172646257878, 433.02503463927087, 16.890968304141275, 673.2987323277127, 534.8343700610021, 641.4227464816397, 618.3493589106022, 755.9605966951774, 585.6080133133892, 700.8820313830021, 72.58944479897711, 928.0477501663338, 106.13554186997254, 786.8918976625699, 301.3507098448711, 86.50522143779271, 765.2742410204625, 437.676149468225, 395.11325822861767, 660.7231649072405, 473.98767736795145, 533.4677638103494, 136.3814603295468, 390.9925654289903, 797.4846128334658, 545.7244686408226, 960.1342650804345, 144.07553745848313, 677.2890455050625, 914.1689163846179, 795.0171222324592, 729.3245766158448, 373.130514900324, 949.4651035305127, 553.524187764279, 555.0697456402619,), (123.23515893349901, 5.029839074005116, 596.6548874533303, 537.152048554461, 946.9793892288718, 304.6911523495084, 748.2965867327935, 903.5463759745459, 343.8516498486469, 412.65449602624795, 645.9315035600874, 512.552694547587, 160.95435756439502, 220.78796935740797, 834.7483668680621, 194.32621534632543, 181.35798477365006, 799.6600536055314, 852.2511277674757, 851.3877224254742, 932.3698654031485, 994.3130504254334, 461.2652036895939, 549.6646096791668, 290.65266752008534, 67.36275740480136, 98.20240138037195, 725.5892573341403, 487.1904101337485, 330.8509316827234, 128.21514659595158, 655.4729806969527, 100.0450572754229, 619.5157771543329, 900.1185908429218, 317.7859372337254, 450.64597821493504, 616.3424302594531, 305.6754894067207, 584.1699328451972, 565.5520079051098, 364.4934356786292, 316.33315949632913, 428.3073929595911, 4.831882664671383, 246.17513854548412, 221.52570420651975, 739.8162159386168, 436.1240660896885, 840.1100587806552,), (134.30583238288884, 732.9727957514148, 877.8858295079616, 462.84837607561025, 358.74160211359674, 305.47179644533526, 551.6718261601972, 175.77016089522536, 606.6276660708613, 841.7929953382605, 858.7136553707992, 140.0016687161999, 538.6180370780223, 263.2346505935833, 886.3358320788956, 76.46237855721539, 75.39995679337663, 18.630294871235353, 507.1783112855739, 31.194231008309092, 581.8904904939293, 405.134467551114, 590.036311840718, 906.3035773981833, 551.5939985261583, 543.4693239181186, 998.7281750118517, 472.1039507829259, 775.1972388374282, 365.8186301030095, 223.30664762435504, 772.1448435604627, 733.226827212901, 290.9991602143739, 464.7011421704528, 510.4119933437855, 396.78515764970746, 502.16726966643085, 662.6784893786967, 847.9818401038666, 806.5400569278984, 614.0359765601281, 165.84355203215827, 514.1366805163822, 446.22044383900175, 179.14518390862077, 948.7118316129784, 659.4133069529768, 967.735559837513, 735.9198088348245,), (483.50341572743093, 358.79553884580594, 218.81911725902836, 487.64214837364784, 62.861161413469205, 369.43518544747434, 42.808440838646874, 206.77407471750098, 907.7260925729606, 361.2513645596288, 469.67422901020893, 454.64344979488834, 46.437457501178116, 980.5897820631023, 324.0711100639776, 703.7362528476223, 521.3729041335506, 829.6471556536503, 834.8722662173934, 263.3160693841534, 544.3500620883715, 173.84981644645003, 653.6753131338843, 365.29677799104763, 653.3365465568498, 835.4690676766445, 516.6628975539387, 376.3287810435452, 907.2424264468196, 516.3717821675702, 352.89800775972566, 867.7194218712473, 486.86156267417556, 482.11008971312594, 604.2109802856869, 501.83698447074175, 138.96830707712994, 165.60582242899213, 77.08822574124684, 643.4770023883098, 211.51710413575475, 186.9056758087676, 362.22294421509105, 717.3027084540954, 118.33022282354267, 230.34633259932457, 811.1726293729599, 720.2556588749491, 481.51664938948613, 478.81464272008213,), (210.70395105176343, 161.24625986262575, 833.3649963952541, 22.446038154291358, 43.144331335165376, 573.4865113522683, 161.129140367538, 629.540424888155, 39.190865239483564, 572.2692992304645, 55.91200416044417, 258.2596460167085, 180.0375395762971, 958.2383364845537, 599.3575227949298, 563.6047023357652, 18.62003563723691, 719.5472995040886, 661.7450626291582, 283.45462640564045, 85.74850273574452, 449.20336161945676, 992.7758237079788, 867.3023989920944, 170.56678398969626, 830.3635747042808, 600.8387144112, 794.6160702604512, 820.4531801304973, 181.88415187009966, 659.648750057853, 264.5206985452897, 724.1867980777558, 342.6712745603319, 453.47014514009174, 590.5959909323074, 229.81378717877976, 385.46188274184755, 108.5701139015155, 202.34854908817513, 859.5916920106055, 504.35952607071266, 419.7950126716061, 149.56119362645603, 96.2985767232044, 475.86918548947733, 614.7481694803669, 39.03656361812824, 783.8682348387684, 503.3292081384595,), (117.51640657477047, 482.2409351124809, 130.32884077716434, 603.4746508247106, 827.5217786520734, 896.9689528475317, 774.8704230678032, 649.2317351065338, 522.0442296615541, 370.3774502487801, 43.438283336481476, 529.8165189397056, 229.4862563790502, 802.3001960078137, 789.5555851509546, 381.23649619905507, 589.0480861412066, 741.394042282245, 761.5392270474919, 696.7076031574667, 97.71690834678259, 133.88310562460137, 477.14123700454, 232.51289062265568, 859.0532473014114, 283.26666800895583, 876.7663131932245, 408.6467046872152, 189.02101371787072, 709.1488372061078, 789.422482305125, 577.9871458004462, 117.82860398115102, 7.194052450581467, 654.5461208031974, 687.7667367777761, 315.90171462927066, 362.2697897745908, 154.77858056181358, 651.5759347779317, 253.63272623198395, 854.9769262679285, 423.56154743336316, 365.64409443647173, 275.56054287998876, 680.6057050634485, 754.9719006098335, 413.2281582959364, 783.7884169635375, 482.4677570068806,), (370.212968879147, 554.9113562983285, 253.77764544269633, 306.6530866745945, 344.35630421485365, 705.4946079710852, 735.7824306046107, 854.9989228235969, 659.282849126203, 747.8141950798877, 446.5948774152193, 699.3114422950533, 161.79511770813627, 214.45814153808007, 400.56090642572286, 338.4378463093469, 550.5762145968181, 697.0574087244603, 706.7899335328698, 160.73800913321722, 964.5702513745175, 5.290633472571971, 91.08858423870403, 145.3284358357537, 925.8617877978492, 435.377434552764, 64.0804563693106, 221.770244619512, 80.03973669580266, 37.755338285078935, 384.326010426022, 984.4481006312732, 613.9655907167139, 521.0055432205589, 711.6058317089812, 597.4725638140962, 945.7158578456077, 820.2287344282204, 640.291314530137, 438.97204854123584, 201.7072428060146, 656.8842227068429, 803.9099091153836, 286.00651170869537, 33.609351396258425, 599.5149128924895, 515.8136315528284, 231.67097281806159, 169.48289181310216, 36.06113314434878,), (239.25662528737723, 0.28727185462340543, 157.44476085039082, 996.8171438272063, 779.6728789764172, 353.127663356528, 395.1431170512062, 586.0801652096985, 517.7427009892949, 736.925540146395, 68.61717193705897, 90.1658575951505, 284.4937870363794, 829.7648024492999, 915.008973948154, 348.09524824824246, 963.2876650654888, 276.5078486971222, 606.3634079922522, 194.52519308596106, 929.7138408121202, 574.9067217458147, 261.10750567193907, 407.13462610720165, 106.46495639257658, 72.38597052670393, 294.25182446812005, 947.0154651776626, 802.9422425723574, 956.7093495852752, 876.113046243067, 813.1473496063743, 574.8938787739089, 694.1918614713001, 966.0554556331059, 563.1686038049302, 769.9671927338335, 757.4732850466725, 961.2387253016144, 458.848429334124, 460.7204414645122, 586.8265171499795, 27.350697532073866, 116.2683014409226, 67.55426066125358, 633.6617975581885, 994.1980333654756, 676.9129760650999, 229.93013858915413, 315.7306940289286,), (955.4456871233602, 516.5042172048383, 9.723191714424928, 832.1770449232287, 248.25760936207752, 93.00943016209173, 673.6979167818138, 821.0746894925302, 76.02882053368909, 931.3962398356626, 476.5554540892897, 353.53792621617106, 894.3162528702067, 269.07578246879905, 947.1181090634782, 683.1073085237686, 909.9274589878878, 498.9993403636831, 199.6665390943092, 735.4290652428307, 872.7424984082979, 206.77863232200988, 202.76655878827333, 249.69543394695748, 618.5968991818082, 155.99556847615892, 106.6892901053912, 920.1881295329896, 675.8121248765266, 663.42407832045, 613.8269752757525, 764.244212635527, 541.4739546030245, 42.26312673099775, 482.2920812010506, 620.6674748317666, 492.340377704212, 985.3613293244484, 896.999773193295, 868.7688356245534, 490.8053516065467, 984.3946826775544, 916.0068296725573, 280.26662318318705, 222.09278307981327, 576.6651863474098, 54.14817128878413, 799.269917779981, 478.32007661204347, 540.767945591827,), (502.47521212167254, 393.7169228523979, 686.3386465303895, 174.80036211904903, 976.5071952370417, 698.2445680961396, 460.07059004643, 689.2105490596489, 11.819978610448922, 210.7523857014477, 581.0082189794467, 325.43619991542636, 612.7780963617714, 259.700769090298, 548.5634003791886, 237.13915250754525, 471.34536473845543, 613.0840431384478, 365.9241511219268, 498.7700166101968, 210.53248211065633, 700.7126268163007, 372.30962786881605, 854.3260872408196, 279.62953188068406, 179.88922872699587, 131.13916649872394, 575.8927292079621, 228.5667494996686, 99.86974090945311, 269.9491069025366, 235.80517288550763, 432.2187517308349, 380.883382231468, 145.84373533753637, 959.1479207466692, 149.54805726548292, 805.6683615575902, 176.60727665961284, 499.5978261118507, 995.5617702996016, 849.3899153966604, 517.0069749205006, 720.5603295246306, 785.3329366938784, 300.0341821534487, 562.1384520717112, 567.8345192053117, 398.40190186332757, 690.5431759606441,), (60.036377724016596, 813.8180692068463, 476.539424352137, 629.8622123621544, 449.85481906379175, 334.6565385145628, 360.96202105512157, 560.3500348142645, 931.8445757777681, 257.7148254524192, 19.83533361376, 121.23748921670374, 867.3685839741368, 963.1165609179141, 198.9581106368732, 575.8944574733487, 649.130951899575, 174.53883572945838, 780.3090980992371, 355.1090383908918, 673.1928401473501, 487.49374430436166, 736.5261647621551, 889.6324491382205, 381.07099841284577, 286.5235765557891, 631.7121904834689, 144.84833827806742, 167.57553548053383, 807.7171439570685, 337.3883099989331, 631.273408325837, 571.6375738737935, 848.9007719388086, 71.34407817655742, 161.99948894698534, 228.21793938787326, 316.8782416955612, 291.3563527944199, 267.4723908706379, 644.6442931551883, 271.248101351349, 444.89838764797173, 862.8064836470026, 363.1648402504287, 586.9197735260105, 965.5260328224946, 413.98906087367317, 183.886199103384, 23.10109231650759,), (727.7724164324044, 662.0225478086397, 940.4373373379956, 700.6969284246842, 80.06473787554714, 166.91046503965245, 103.00931405688607, 63.79239959116023, 878.6909764497414, 548.4337438182938, 26.364913366241026, 397.0125207601695, 764.9963721335957, 83.25120139068665, 262.45226426498715, 155.27436200016186, 654.8326755433498, 885.4713170963988, 309.3706535499986, 247.39651931519225, 284.175280058089, 626.4806846261897, 131.27711062686387, 838.5177788928746, 28.5712930199129, 663.5491353038045, 860.3688174133925, 325.20131665647875, 476.15474065537353, 974.9815454400946, 540.5841613922831, 272.41442761231417, 444.84493237074383, 969.5422645545461, 697.6228769070474, 177.03663997971208, 597.5841988365606, 631.0475226410597, 635.8511364258356, 563.9844867212875, 523.4598043545075, 639.528248710812, 309.6613281139966, 347.1683001641309, 539.6681221360597, 804.707766246453, 441.8603055765344, 365.7833219483778, 259.80704393772436, 303.57291557308895,), (0.09234639492805563, 815.3797891608882, 847.9464883826706, 343.88491724999426, 469.19795524402616, 8.3171105963733, 922.0883957440528, 946.9575118681244, 477.06707045145714, 9.132599576333167, 430.43531326923346, 292.9226130023878, 231.21539351915854, 7.217985005767535, 373.64387652703357, 411.73586906613457, 560.5543586396154, 394.75207962018334, 163.26949533028878, 737.1178541033773, 389.7232698531959, 378.35806027496176, 262.98027544624904, 422.23938275369267, 239.56181493856056, 764.5382940766145, 909.9544034392849, 807.5749404275756, 684.6402903785192, 284.67685467345297, 742.9268921311517, 808.8086490990372, 412.9038765132473, 853.4820233916041, 182.4071790026428, 289.59529929128547, 636.8927095483467, 617.7294139025154, 272.13413045162406, 622.7537802755535, 187.7894425440112, 19.39539724699635, 49.632072223098625, 534.9707530597663, 185.93759814152565, 102.27348865653974, 269.19513288914555, 715.195942211275, 727.107070447535, 232.81049795595544,), (150.68369369748856, 493.5795531957453, 341.91285386862626, 311.5766852030555, 799.4623519360701, 997.9634461655995, 463.6844672034293, 791.4247142652953, 330.24864703583034, 843.5462992680785, 951.644041732608, 56.038344401892815, 775.6529107125235, 71.38469474905685, 470.1467765987197, 192.19229292645278, 841.5392879402597, 817.2507239995704, 828.2520473943315, 121.96155618577076, 768.1456708511898, 248.96236050723408, 771.2253330273345, 442.96094214292003, 737.6190125535086, 33.51029157027507, 460.8346353122799, 770.9959677901886, 521.1391200572839, 982.1158514056141, 472.8255449915152, 681.4423200578532, 312.1093655840698, 323.0631884829062, 629.1642541306993, 42.18591559473106, 937.4209236322339, 520.9262529175096, 253.31274033508888, 638.5200120420238, 198.72363180239984, 887.5195760435424, 863.6576657496004, 217.78824905510285, 112.92845931623752, 632.9089659017448, 324.5134876300968, 167.36035640828607, 276.1000438384478, 119.64959201073866,), (789.1437801948886, 8.929855889342143, 41.993369021095404, 783.4752402510599, 475.27067876165006, 596.2122839316125, 371.39866330323366, 89.47990274967444, 157.6984561973751, 91.40155230421298, 618.6448584379411, 930.8487845186271, 996.9731029835355, 625.1323302094067, 59.775131609422495, 644.5748966479184, 701.3281400230029, 791.4662443652128, 125.8274576652264, 232.67463085606354, 981.6757109638336, 788.5618257868736, 756.697523201389, 805.5253765865858, 438.71889809659734, 192.96529905015848, 689.2499802840422, 358.1674972504314, 134.73217376095226, 898.6630442700426, 485.30213316251314, 437.5000041650592, 294.7145964408969, 697.0377749956548, 189.77583621965144, 186.6267289491038, 347.6646790182655, 732.4024101227128, 275.32446285830315, 831.3495933911199, 914.4740019780461, 554.5203101660566, 68.87658336115943, 155.02547807532696, 295.67266121073476, 263.349268895432, 367.11270700453946, 0.7198287242494716, 638.0260624435374, 379.04356966657735,), (185.53429009714995, 18.719856588055194, 856.8292883342145, 776.0165677979388, 238.78655821602112, 721.0035626839232, 658.28941737982, 538.9565215934048, 387.49084018073563, 524.0697688104523, 497.54915956954727, 551.4678072492145, 613.0392423505267, 322.60003991279393, 640.9039297850732, 110.46413403940414, 523.2005391932355, 66.67984309626563, 835.1933578982914, 129.7742663222461, 873.4927775805724, 202.07902868878213, 485.3702872366953, 98.75358664853229, 571.012181354699, 836.2995266064913, 657.9212582849684, 525.8213626144117, 701.3140215747126, 255.93853033397806, 366.3540200942962, 605.95362423245, 70.873960950122, 930.211036809051, 208.73809349350148, 491.4055014282741, 889.7673251634665, 79.95680323287202, 801.0199490108841, 60.70018828605151, 558.0198894700217, 938.2438576998607, 415.9532027097531, 369.0686508584735, 708.4337849788369, 823.6429662774005, 265.8156344966652, 40.65315227434285, 70.38812961024409, 303.90454886805543,), (944.6034820072357, 960.5867454371248, 97.11925015766253, 725.1719328091889, 531.6610514024726, 189.46041310667573, 526.1920217936971, 248.76302905401337, 625.325053539152, 178.8978916289171, 749.1943579773724, 415.18460024974024, 105.51418189915573, 638.6242998880421, 357.96336587140377, 458.7205924283744, 665.2423547735436, 882.8240193732261, 166.80931890354745, 180.39653860006823, 401.5571736148952, 337.512870561569, 158.1764558993315, 998.4335720532814, 443.01216999828785, 352.3240752703322, 315.1549961423973, 991.4629906181269, 324.65410862009236, 371.71951243537126, 764.1843679043711, 430.5880727332455, 725.8676836151698, 608.413370257039, 563.1447028042841, 213.9281360437204, 765.7082196342848, 926.1469225926575, 254.08912718302412, 961.6517756313344, 447.49978899824936, 397.09056168940384, 726.3132783372915, 982.9564711785478, 595.7628266237857, 517.6093588403293, 993.4120095545393, 301.8416539323987, 300.3339617281736, 221.8867034824401,), (855.7204255082062, 21.70137145759954, 821.9399224471144, 689.719183909427, 275.95713674360564, 553.6631532853207, 556.3230855067685, 925.9224502117322, 154.7147862187559, 37.73630008376983, 355.65575328549073, 138.40819347641, 367.0828930260639, 582.15570635987, 232.99414855423439, 811.0742367826583, 91.90512128537264, 399.79824847776393, 917.8818033444678, 734.2659538775798, 723.580526744515, 792.90419431428, 172.9024908563226, 825.7052443848854, 689.5951010498992, 576.2324617766981, 907.6556421470943, 595.2312025595801, 300.39325384327464, 730.7893619562772, 576.2836961563387, 78.47739592285386, 55.9227088233194, 770.9015036701385, 347.9302069529332, 817.1417000465422, 416.5217126179194, 867.8310969512203, 869.7865141212199, 226.7176713621516, 652.7791831603508, 602.3016343165425, 11.43438096260352, 777.4186181440473, 382.4869621852632, 304.78339287924285, 41.18225216356319, 539.9297294386691, 149.58001916730123, 502.40161044723277,), (220.79717482028516, 50.51981414582751, 731.5785735649414, 392.8827045190352, 445.61623597771074, 595.1321912948403, 504.7293461120348, 222.0856281514143, 289.78302915668706, 394.3224038780659, 132.18904537087806, 82.54511263858267, 571.4405905106294, 49.31109807008804, 399.1912995469419, 85.0789874682354, 501.8231156057946, 773.8251365562567, 130.37517489517413, 134.87076372595496, 559.2961773135403, 487.8610998043328, 652.2484389791689, 196.09930039322077, 615.9968375942019, 735.6677541453226, 246.24584050711306, 71.64398034108488, 776.7718987916836, 323.41164115621126, 924.1380808950829, 89.59464812673524, 671.7475534497354, 423.54060236564004, 348.3078568465243, 320.7383628526695, 593.8771490410704, 24.206830335269046, 304.8188475463931, 987.6519169637293, 616.2209944545034, 990.1591822713317, 442.2101052342515, 145.81847519027758, 44.87755093137324, 818.1719201318144, 199.68521360575576, 373.82079032056936, 757.7337825930621, 852.7641451792005,), (112.37174078510425, 54.53801336175423, 948.9409150257889, 926.7296951091071, 868.7523516449686, 820.1339564299961, 13.733292693431377, 693.7952250561176, 111.27799231646985, 450.0615706383459, 22.748115151202008, 209.00954112420212, 538.0054579069861, 203.80135287660516, 523.2658814665313, 258.6580034255985, 483.02633034469176, 729.9235835097888, 141.34742092394183, 698.7552320082528, 18.38851255630447, 583.0049433264157, 663.5283419996432, 43.482007499458966, 170.31964704476331, 284.0135410479907, 789.1884945074221, 617.9647763913978, 53.08524865627895, 654.757871815387, 8.334175248427611, 388.6451472369792, 271.3103790803375, 852.0829447572587, 660.1003733293647, 864.2822735599534, 19.078323934194174, 867.4104428544812, 649.4111447522284, 231.16862013781702, 380.7012029628479, 976.6119398898542, 99.60371184253735, 315.45596930877974, 866.7727455281198, 531.5606451632132, 186.41738741751902, 500.65082286887883, 457.98626214778074, 926.3499730264335,), (21.488458214345528, 247.39213974975127, 529.4153638551198, 333.56963463077574, 393.3998178272747, 157.0052505908327, 346.7799029075455, 351.913082267181, 625.2309702510104, 236.20476570721272, 978.2442672445462, 501.25146117715735, 811.8928600997455, 625.792363715763, 878.6772741911603, 890.4020860787615, 814.858637678337, 29.46589845520775, 554.9393515968055, 280.19384462820506, 151.70818076868466, 897.1753149691715, 656.9317589496127, 87.79501713730143, 382.2071218544083, 960.6056934445716, 612.4483933501054, 625.5246004948266, 227.42797745267552, 240.36080817057604, 152.7491541688516, 970.537173095217, 909.7353935641712, 329.2847383705465, 542.2337512058923, 206.75727466100935, 138.5765267985366, 541.353826851015, 800.1050591640936, 862.5876615800681, 308.9983961621827, 705.1363877851007, 523.8054835018718, 135.25249384391523, 995.6858002320087, 975.7769090685993, 145.15239489251186, 933.0299464229068, 917.1121206734174, 317.4962108419391,), (557.3402980935688, 948.6015641647615, 118.38729071717869, 317.59758919121697, 879.6381216162653, 727.0795332601915, 765.4350125691849, 880.131900108576, 414.04015541967885, 411.2517632793493, 443.004487049652, 933.768524391193, 894.1300931338027, 933.2500304158002, 273.79616463152354, 779.1078849727928, 106.77143284340107, 184.74649657456732, 762.4474837329655, 611.981597669024, 266.86261300447956, 567.0430929389746, 230.911348733013, 232.18253426933566, 687.377885178361, 359.26133453455134, 688.141284732716, 476.6022200441854, 502.32458745046125, 604.7117596027515, 712.01912797666, 373.95940113050096, 852.1294324655852, 491.4455970663347, 137.5127223416147, 193.25982947851017, 32.22955322704579, 764.5388541282347, 15.014260451757021, 269.85870264618126, 413.04402219171976, 742.3666616628996, 988.2434174629566, 757.7765897256293, 66.13632423488936, 927.1032061777938, 985.6276395863681, 867.1290026386034, 489.9428985108636, 324.8956268040456,), (457.5449294417121, 246.78384086158633, 404.86676824252845, 41.826275050118156, 735.3317798996605, 380.3741687551209, 312.89749141149747, 611.504874670052, 742.4678547079448, 593.805801392789, 525.2305317028896, 875.7829718598168, 786.9198623188731, 520.6490912237487, 451.6095925636621, 827.0101016593499, 42.41591600909778, 995.8363715240481, 518.7026525875117, 395.6111966358917, 735.1700815502675, 557.7009498134095, 516.1266830947985, 630.6485530927204, 49.29544918127604, 291.17936223412954, 398.0398914545593, 304.5541478222386, 827.6210076057507, 461.347163963963, 422.46218245044145, 613.1342792438925, 54.54666681552644, 516.969650266138, 142.23961877509163, 829.8935044266395, 451.6980878296554, 722.646791492833, 113.17158624359503, 778.7311418760397, 937.8424576992394, 696.1141418397956, 135.30667419036257, 413.55930509761663, 450.8858548470743, 178.8785789436156, 590.315475289654, 712.6343913063026, 201.9320047091576, 454.6832076473508,), (250.08229407723826, 691.7174283309014, 907.1624630054067, 796.4862224757226, 718.374719718563, 123.54421683543349, 114.16493882407775, 449.3984180247218, 362.94338915725456, 523.8173060661488, 384.0873742615717, 791.0665942370252, 511.6664514845053, 949.7538891427195, 378.67863141257976, 380.60700926114674, 768.2603237843883, 912.2527788221324, 565.4915785141691, 659.5303406020439, 150.08973451051176, 868.8185374782038, 178.86676467326868, 712.0474878796226, 419.58169123576727, 309.5183054369818, 768.6304223609426, 446.2582341490725, 626.4528496615293, 117.55397840247983, 131.57103181208575, 202.8517052794463, 622.5603279873416, 253.11097511803925, 458.64265782853664, 855.4110564190316, 543.5011596043003, 5.750548899053909, 882.7029205371946, 237.87835401947677, 588.8623271843092, 475.71198980315387, 410.15136571135633, 79.40491736841615, 600.1021042955892, 244.71317143863337, 547.3416095831464, 619.7660388695853, 557.5486482688888, 825.2525047036826,), (48.918799943029725, 148.25616817969035, 653.2768782155202, 36.72105182241203, 853.8292872341877, 666.996542946195, 838.014435792606, 298.44154664211777, 920.3846982695362, 49.03136274879905, 416.9569238553582, 178.2483917574228, 672.090495261405, 611.089756048654, 691.5136180806667, 594.9689863047196, 787.3075795335859, 177.4192673436795, 455.3378280202657, 578.9662924422967, 931.4798193860056, 93.12628782176957, 299.2944815965134, 368.56942547507407, 378.9393515904601, 67.66667782099533, 427.3238393910946, 550.4710828134854, 292.83271335673953, 134.54578907805103, 694.619438494801, 274.4185427517334, 527.0527334046208, 524.6462092537088, 695.8294973451716, 612.053744928424, 109.30223443632624, 729.7285197626795, 628.9785990785555, 987.1892637585828, 483.662983209447, 688.6542782543451, 933.8916747881832, 986.2784423441115, 287.1873038042605, 608.8446069344726, 316.48855849847223, 525.3912253315947, 995.0243355575512, 353.85334437419004,)))" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Matrix((639.4267984578837, ...), ..., (..., 353.85334437419004))[100x50]\n" + ] + } + ], + "source": [ + "print(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Similarily, `v` is now a `Vector` with $50$ entries." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "v = Vector(1_000 * random.random() for _ in range(50))" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((129.7131375543026, 562.6343376666077, 519.7058001844938, 631.8576074968361, 492.50445371851595, 179.90723165619127, 609.4057577306154, 708.5870675033931, 979.2576595660704, 1.580917225831202, 23.986797518176004, 625.4607423896532, 117.92572392127187, 848.0697885701867, 799.5643512555062, 998.9870072501308, 414.04113952437183, 333.7922569338235, 560.4155049178265, 637.5035435880566, 11.297267978029769, 201.1871389300902, 281.62670325053864, 790.1955028462116, 307.77254924389706, 506.6896270960092, 323.9238393830701, 6.131262435292828, 685.8357886469579, 341.36158523000273, 724.3966428110118, 615.9933429964842, 29.117381811460618, 175.62908823799773, 330.51483209301, 337.936855273543, 672.4729573660214, 916.1630531735751, 797.2543838289249, 645.6522206645749, 481.4955231203607, 627.2004877076889, 892.0583267899182, 536.9675449723645, 335.10965453438513, 783.9890332085388, 413.95306533418574, 742.5846461655369, 835.1057359656187, 299.3437466393607))" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "v" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vector(129.7131375543026, ..., 299.3437466393607)[50]\n" + ] + } + ], + "source": [ + "print(v)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The arithmetic works as before." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "w = m * v" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vector(11378937.310589302, ..., 13593029.305789862)[100]\n" + ] + } + ], + "source": [ + "print(w)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We can multiply `m` with its transpose or the other way round." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "n = m * m.transpose()" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Matrix((14370711.26526542, ...), ..., (..., 16545418.239505697))[100x100]\n" + ] + } + ], + "source": [ + "print(n)" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "o = m.transpose() * m" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Matrix((32618511.507031415, ...), ..., (..., 32339164.778032355))[50x50]\n" + ] + } + ], + "source": [ + "print(o)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Comparison with [numpy](https://www.numpy.org/)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We started out in this chapter by realizing that Python provides us no good data type to model a vector $\\vec{x}$ or a matrix $\\bf{A}$. Then, we built up two custom data types, `Vector` and `Matrix`, that wrap a simple `tuple` object for $\\vec{x}$ and a `tuple` of `tuple`s for $\\bf{A}$ so that we can interact with their `._entries` in a \"natural\" way, which is similar to how we write linear algebra tasks by hand. By doing this, we extend Python with our own little \"dialect\" or **[domain-specific language ](https://en.wikipedia.org/wiki/Domain-specific_language)** (DSL).\n", + "\n", + "If we feel like sharing our linear algebra library with the world, we could easily do so on either [GitHub ](https://github.com) or [PyPI](https://pypi.org). However, for the domain of linear algebra this would be rather pointless as there is already a widely adopted library with [numpy](https://www.numpy.org/) that not only has a lot more features than ours but also is implemented in C, which makes it a lot faster with big data.\n", + "\n", + "Let's model the example in the [first part ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb#Example:-Vectors-&-Matrices) with both [numpy](https://www.numpy.org/) and our own DSL and compare them." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "x = (1, 2, 3)\n", + "A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "TypeError", + "evalue": "can't multiply sequence by non-int of type 'tuple'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[55], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mA\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mx\u001b[49m\n", + "\u001b[0;31mTypeError\u001b[0m: can't multiply sequence by non-int of type 'tuple'" + ] + } + ], + "source": [ + "A * x" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The creation of vectors and matrices is similar to our DSL. However, numpy uses the more general concept of an **n-dimensional array** (i.e., the `ndarray` type) where a vector is only a special case of a matrix and a matrix is yet another special case of an even higher dimensional structure." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "x_arr = np.array(x)\n", + "A_arr = np.array(A)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [], + "source": [ + "x_vec = Vector(x)\n", + "A_mat = Matrix(A)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The text representations are very similar. However, [numpy](https://www.numpy.org/)'s `ndarray`s keep the entries as `int`s while our `Vector` and `Matrix` objects contain `float`s." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1, 2, 3])" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_arr" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((1.0, 2.0, 3.0))" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_vec" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1, 2, 3],\n", + " [4, 5, 6],\n", + " [7, 8, 9]])" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_arr" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 2.0, 3.0,), (4.0, 5.0, 6.0,), (7.0, 8.0, 9.0,)))" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_mat" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "[numpy](https://www.numpy.org/)'s `ndarray`s come with a `.shape` instance attribute that returns a `tuple` with the dimensions ..." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(3,)" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_arr.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(3, 3)" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_arr.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "... while `Matrix` objects come with `.n_rows` and `.n_cols` properties." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(3, 3)" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_mat.n_rows, A_mat.n_cols" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The built-in [len() ](https://docs.python.org/3/library/functions.html#len) function does not return the number of entries in an `ndarray` but the number of the rows instead. This is equivalent to the first element in the `.shape` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(x_arr)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(x_vec)" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(A_arr)" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "9" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(A_mat)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "The `.transpose()` method also exists for `ndarray`s." + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1, 4, 7],\n", + " [2, 5, 8],\n", + " [3, 6, 9]])" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_arr.transpose()" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Matrix(((1.0, 4.0, 7.0,), (2.0, 5.0, 8.0,), (3.0, 6.0, 9.0,)))" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_mat.transpose()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "To perform matrix-matrix, matrix-vector, or vector-matrix multiplication in [numpy](https://www.numpy.org/), we use the `.dot()` method. If we use the `*` operator with `ndarray`s, an *entry-wise* multiplication is performed." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([14, 32, 50])" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_arr.dot(x_arr)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 1, 4, 9],\n", + " [ 4, 10, 18],\n", + " [ 7, 16, 27]])" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_arr * x_arr" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((14.0, 32.0, 50.0))" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "A_mat * x_vec" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Scalar multiplication, however, works as expected." + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([10, 20, 30])" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10 * x_arr" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Vector((10.0, 20.0, 30.0))" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "10 * x_vec" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "Because we implemented our classes to support the sequence protocol, [numpy](https://www.numpy.org/)'s *one*-dimensional `ndarray`s are actually able to work with them: The `*` operator is applied on a per-entry basis." + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([2., 4., 6.])" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_arr + x_vec" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1., 4., 9.])" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x_arr * x_vec" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "outputs": [ + { + "ename": "ValueError", + "evalue": "operands could not be broadcast together with shapes (3,3) (9,) ", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[79], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mA_arr\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mA_mat\u001b[49m\n", + "\u001b[0;31mValueError\u001b[0m: operands could not be broadcast together with shapes (3,3) (9,) " + ] + } + ], + "source": [ + "A_arr + A_mat" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "We conclude that it is rather easy to extend Python in a way that makes the resulting application code read like core Python again. As there are many well established third-party packages out there, it is unlikely that we have to implement a fundamental library ourselves. Yet, we can apply the concepts introduced in this chapter to organize the code in the applications we write." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/11_classes/05_summary.ipynb b/11_classes/05_summary.ipynb new file mode 100644 index 0000000..163f711 --- /dev/null +++ b/11_classes/05_summary.ipynb @@ -0,0 +1,100 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Chapter 11: Classes & Instances (TL;DR)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "With the `class` statement, we can create a *user-defined* data type that we also call a **class**.\n", + "\n", + "Then, to create new **instances** of the data type, we simply call the class, just as we do with the built-in constructors.\n", + "\n", + "Conceptually, a class is the **blueprint** defining the **behavior** each instance exhibits.\n", + "\n", + "In the example used throughout the chapter, the `Vector` and `Matrix` classes implement the linear algebra rules all `Vector` and `Matrix` instances follow *in general*. On the contrary, the instances **encapsulate** the **state** of *concrete* vectors and matrices.\n", + "\n", + "The `class` statement acts as a *namespace* that consists of simple *variable assignments* and *function definitions*:\n", + "1. Variables become the **class attributes** that are shared among all instances.\n", + "2. Functions may take a different role:\n", + " - By default, they become **instance methods** by going through a **binding process** where a reference to the instance on which the method is *invoked* is passed in as the first argument. By convention, the corresponding parameter is called `self`; it embodies an instance's state: That means that instance methods set and get **instance attributes** on and from `self`.\n", + " - They may be declared as **class methods**. Then, the binding process is adjusted such that the first argument passed in is a reference to the class itself and, by convention, named `cls`. A common use case is to design **alternative constructors**.\n", + " - They may also be declared as **properties**. A use case for that are *derived* attributes that follow semantically from an instance's state.\n", + " \n", + "The **Python Data Model** concerns what special methods (i.e., the ones with the dunder names) exists and how they work together.\n", + "\n", + "The instantiation process is controlled by the `.__init__()` method.\n", + "\n", + "The `__repr__()` and `__str__()` methods implement the **text representation** of an instance, which can be regarded as a Unicode encoded representation of all the state encapsulated in an instance.\n", + "\n", + "**Sequence emulation** means that a user-defined data type exhibits the same four properties as the built-in sequences, which are regarded as finite and iterable containers with a predictable order. The `.__len__()`, `.__iter__()`, `__reversed__()`, `__getitem__()`, and some others are used to implement the corresponding behaviors.\n", + "\n", + "Similarly, **number emulation** means that an instance of a user-defined data type behaves like a built-in number. For example, by implementing the `.__abs__()` method, an instance may be passed to the built-in [abs() ](https://docs.python.org/3/library/functions.html#abs) function.\n", + "\n", + "If different data types, built-in or user-defined, share a well-defined set of behaviors, a single function may be written to work with objects of all the data types. We describe such functions as **polymorphic**.\n", + "\n", + "Classes may specify how *operators* are **overloaded**. Examples for that are the `.__add__()`, `.__sub__()`, or `.__eq__()` methods.\n", + "\n", + "**Packages** are folders containing **modules** (i.e., \\*.py files) and a \"*\\_\\_init\\_\\_.py*\" file. We use them to design coherent libraries with reusable code." + ] + } + ], + "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.12.2" + }, + "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 +} diff --git a/11_classes/06_review.ipynb b/11_classes/06_review.ipynb new file mode 100644 index 0000000..53cb6bb --- /dev/null +++ b/11_classes/06_review.ipynb @@ -0,0 +1,271 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Chapter 11: Classes & Instances (Review Questions)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The questions below assume that you have read the [first ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb), [second ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/02_content.ipynb), [third ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/03_content.ipynb), and [fourth ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/04_content.ipynb) part of Chapter 11.\n", + "\n", + "Be concise in your answers! Most questions can be answered in *one* sentence." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Essay Questions " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q1**: How are **classes** a way to manage the **state** in a big program? How should we think of classes conceptually?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q2**: What do we mean with **instantiation**? How do **instances** relate to **classes**?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q3:** What is an **implementation detail**? Name two different examples of implementation details regarding the `Vector` and `Matrix` classes!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q4**: How are **instance methods** different from **class methods**? How do **special methods** fit into the picture?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q5**: How do **mutability** and **immutability** come into play when designing a user-defined data type?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q6**: Explain the concept of **method chaining**!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q7**: How can we implement **operator overloading** for a user-defined data type? When do we need to user *reverse* special methods?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## True / False Questions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Motivate your answer with *one short* sentence!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q8**: An instance's **text representation** is a `bytes` object with a special encoding." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q9**: Computed **properties** are special kinds of **instance methods**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q10**: **Sequence emulation** means designing a user-defined data type around the built-in `list` or `tuple` types." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q11**: The **Python Data Model** can be regarded as the \"Theory\" or \"Mental Model\" behind the Python language." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q12**: **Polymorphism** means that two instances have the same data type, be it a built-in or user-defined one." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q13**: **Number emulation** means that two instances of the same user-defined data type can be added together." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Q14**: **Packages** are a good place to collect all the code to be reused in a data science project, for example, across different Jupyter notebooks." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " < your answer >" + ] + } + ], + "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.12.2" + }, + "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": {}, + "toc_section_display": false, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/11_classes/sample_package/__init__.py b/11_classes/sample_package/__init__.py new file mode 100644 index 0000000..8e87577 --- /dev/null +++ b/11_classes/sample_package/__init__.py @@ -0,0 +1,34 @@ +"""This package provides linear algebra functionalities. + +The package is split into three modules: +- matrix: defines the Matrix class +- vector: defines the Vector class +- utils: defines the norm() function that is shared by Matrix and Vector + and package-wide constants + +The classes implement arithmetic operations involving vectors and matrices. + +See the docstrings in the modules and classes for further info. +""" + +# Import the classes here so that they are available +# from the package's top level. That means that a user +# who imports this package with `import sample_package` +# may then refer to, for example, the Matrix class with +# simply `sample_package.Matrix` instead of the longer +# `sample_package.matrix.Matrix`. +from sample_package.matrix import Matrix +from sample_package.vector import Vector + + +# Define meta information for the package. +# There are other (and more modern) ways of +# doing this, but specifying the following +# dunder variables here is the traditional way. +__name__ = "linear_algebra_tools" +__version__ = "0.1.0" # see https://semver.org/ for how the format works +__author__ = "Alexander Hess" + +# Define what is imported with the "star import" +# (i.e., with `from sample_package import *`). +__all__ = ["Matrix", "Vector"] diff --git a/11_classes/sample_package/matrix.py b/11_classes/sample_package/matrix.py new file mode 100644 index 0000000..50a683f --- /dev/null +++ b/11_classes/sample_package/matrix.py @@ -0,0 +1,443 @@ +"""This module defines a Matrix class.""" + +import numbers + +# Note the import at the bottom of this file, and +# see the comments about imports in the matrix module. +from sample_package import utils + + +class Matrix: + """An m-by-n-dimensional matrix from linear algebra. + + All entries are converted to floats, or whatever is set in the typing attribute. + + Attributes: + storage (callable): data type used to store the entries internally; + defaults to tuple + typing (callable): type casting applied to all entries upon creation; + defaults to float + vector_cls (vector.Vector): a reference to the Vector class to work with + zero_threshold (float): max. tolerance when comparing an entry to zero; + defaults to 1e-12 + """ + + storage = utils.DEFAULT_ENTRIES_STORAGE + typing = utils.DEFAULT_ENTRY_TYPE + # the `vector_cls` attribute is set at the bottom of this file + zero_threshold = utils.ZERO_THRESHOLD + + def __init__(self, data): + """Create a new matrix. + + Args: + data (sequence of sequences): the matrix's entries; + viewed as a sequence of the matrix's rows (i.e., row-major order); + use the .from_columns() class method if the data come as a sequence + of the matrix's columns (i.e., column-major order) + + Raises: + ValueError: + - if no entries are provided + - if the number of columns is inconsistent across the rows + + Example Usage: + >>> Matrix([(1, 2), (3, 4)]) + Matrix(((1.0, 2.0,), (3.0, 4.0,))) + """ + self._entries = self.storage( + self.storage(self.typing(x) for x in r) for r in data + ) + for row in self._entries[1:]: + if len(row) != self.n_cols: + raise ValueError("rows must have the same number of entries") + if len(self) == 0: + raise ValueError("a matrix must have at least one entry") + + @classmethod + def from_columns(cls, data): + """Create a new matrix. + + This is an alternative constructor for data provided in column-major order. + + Args: + data (sequence of sequences): the matrix's entries; + viewed as a sequence of the matrix's columns (i.e., column-major order); + use the normal constructor method if the data come as a sequence + of the matrix's rows (i.e., row-major order) + + Raises: + ValueError: + - if no entries are provided + - if the number of rows is inconsistent across the columns + + Example Usage: + >>> Matrix.from_columns([(1, 2), (3, 4)]) + Matrix(((1.0, 3.0,), (2.0, 4.0,))) + """ + return cls(data).transpose() + + @classmethod + def from_rows(cls, data): + """See docstring for .__init__().""" + # Some users may want to use this .from_rows() constructor + # to explicitly communicate that the data are in row-major order. + # Otherwise, this method is redundant. + return cls(data) + + def __repr__(self): + """Text representation of a Matrix.""" + name = self.__class__.__name__ + args = ", ".join( + "(" + ", ".join(repr(c) for c in r) + ",)" for r in self._entries + ) + return f"{name}(({args}))" + + def __str__(self): + """Human-readable text representation of a Matrix.""" + name = self.__class__.__name__ + first, last, m, n = self[0], self[-1], self.n_rows, self.n_cols + return f"{name}(({first!r}, ...), ..., (..., {last!r}))[{m:d}x{n:d}]" + + @property + def n_rows(self): + """Number of rows in a Matrix.""" + return len(self._entries) + + @property + def n_cols(self): + """Number of columns in a Matrix.""" + return len(self._entries[0]) + + def __len__(self): + """Number of entries in a Matrix.""" + return self.n_rows * self.n_cols + + def __getitem__(self, index): + """Obtain an individual entry of a Matrix. + + Args: + index (int / tuple of int's): if index is an integer, + the Matrix is viewed as a sequence in row-major order; + if index is a tuple of integers, the first one refers to + the row and the second one to the column of the entry + + Returns: + entry (Matrix.typing) + + Example Usage: + >>> m = Matrix([(1, 2), (3, 4)]) + >>> m[0] + 1.0 + >>> m[-1] + 4.0 + >>> m[0, 1] + 2.0 + """ + # Sequence-like indexing (one-dimensional) + if isinstance(index, int): + if index < 0: + index += len(self) + if not (0 <= index < len(self)): + raise IndexError("integer index out of range") + row, col = divmod(index, self.n_cols) + return self._entries[row][col] + # Mathematical-like indexing (two-dimensional) + elif ( + isinstance(index, tuple) + and len(index) == 2 + and isinstance(index[0], int) + and isinstance(index[1], int) + ): + return self._entries[index[0]][index[1]] + raise TypeError("index must be either an int or a tuple of two int's") + + def rows(self): + """Loop over a Matrix's rows. + + Returns: + rows (generator): produces a Matrix's rows as Vectors + """ + return (self.vector_cls(r) for r in self._entries) + + def cols(self): + """Loop over a Matrix's columns. + + Returns: + columns (generator): produces a Matrix's columns as Vectors + """ + return ( + self.vector_cls(self._entries[r][c] for r in range(self.n_rows)) + for c in range(self.n_cols) + ) + + def entries(self, *, reverse=False, row_major=True): + """Loop over a Matrix's entries. + + Args: + reverse (bool): flag to loop backwards; defaults to False + row_major (bool): flag to loop in row-major order; defaults to True + + Returns: + entries (generator): produces a Matrix's entries + """ + if reverse: + rows = range(self.n_rows - 1, -1, -1) + cols = range(self.n_cols - 1, -1, -1) + else: + rows, cols = range(self.n_rows), range(self.n_cols) + if row_major: + return (self._entries[r][c] for r in rows for c in cols) + return (self._entries[r][c] for c in cols for r in rows) + + def __iter__(self): + """Loop over a Matrix's entries. + + See .entries() for more customization options. + """ + return self.entries() + + def __reversed__(self): + """Loop over a Matrix's entries in reverse order. + + See .entries() for more customization options. + """ + return self.entries(reverse=True) + + def __add__(self, other): + """Handle `self + other` and `other + self`. + + This may be either matrix addition or broadcasting addition. + + Example Usage: + >>> Matrix([(1, 2), (3, 4)]) + Matrix([(2, 3), (4, 5)]) + Matrix(((3.0, 5.0,), (7.0, 9.0,))) + + >>> Matrix([(1, 2), (3, 4)]) + 5 + Matrix(((6.0, 7.0,), (8.0, 9.0,))) + + >>> 10 + Matrix([(1, 2), (3, 4)]) + Matrix(((11.0, 12.0,), (13.0, 14.0,))) + """ + # Matrix addition + if isinstance(other, self.__class__): + if (self.n_rows != other.n_rows) or (self.n_cols != other.n_cols): + raise ValueError("matrices must have the same dimensions") + return self.__class__( + (s_col + o_col for (s_col, o_col) in zip(s_row, o_row)) + for (s_row, o_row) in zip(self._entries, other._entries) + ) + # Broadcasting addition + elif isinstance(other, numbers.Number): + return self.__class__((c + other for c in r) for r in self._entries) + return NotImplemented + + def __radd__(self, other): + """See docstring for .__add__().""" + if isinstance(other, self.vector_cls): + raise TypeError("vectors and matrices cannot be added") + # As both matrix and broadcasting addition are commutative, + # we dispatch to .__add__(). + return self + other + + def __sub__(self, other): + """Handle `self - other` and `other - self`. + + This may be either matrix subtraction or broadcasting subtraction. + + Example Usage: + >>> Matrix([(2, 3), (4, 5)]) - Matrix([(1, 2), (3, 4)]) + Matrix(((1.0, 1.0,), (1.0, 1.0,))) + + >>> Matrix([(1, 2), (3, 4)]) - 1 + Matrix(((0.0, 1.0,), (2.0, 3.0,))) + + >>> 10 - Matrix([(1, 2), (3, 4)]) + Matrix(((9.0, 8.0,), (7.0, 6.0,))) + """ + # As subtraction is the inverse of addition, + # we first dispatch to .__neg__() to invert the signs of + # all entries in other and then dispatch to .__add__(). + return self + (-other) + + def __rsub__(self, other): + """See docstring for .__sub__().""" + if isinstance(other, self.vector_cls): + raise TypeError("vectors and matrices cannot be subtracted") + # Same comments as in .__sub__() apply + # with the roles of self and other swapped. + return (-self) + other + + def _matrix_multiply(self, other): + """Internal utility method to multiply to Matrix instances.""" + if self.n_cols != other.n_rows: + raise ValueError("matrices must have compatible dimensions") + # Matrix-matrix multiplication means that each entry of the resulting + # Matrix is the dot product of the respective row of the "left" Matrix + # and column of the "right" Matrix. So, the rows/columns are represented + # by the Vector instances provided by the .cols() and .rows() methods. + return self.__class__((rv * cv for cv in other.cols()) for rv in self.rows()) + + def __mul__(self, other): + """Handle `self * other` and `other * self`. + + This may be either scalar multiplication, matrix-vector multiplication, + vector-matrix multiplication, or matrix-matrix multiplication. + + Example Usage: + >>> Matrix([(1, 2), (3, 4)]) * Matrix([(1, 2), (3, 4)]) + Matrix(((7.0, 10.0,), (15.0, 22.0,))) + + >>> 2 * Matrix([(1, 2), (3, 4)]) + Matrix(((2.0, 4.0,), (6.0, 8.0,))) + + >>> Matrix([(1, 2), (3, 4)]) * 3 + Matrix(((3.0, 6.0,), (9.0, 12.0,))) + + Matrix-vector and vector-matrix multiplication are not commutative. + + >>> from sample_package import Vector + + >>> Matrix([(1, 2), (3, 4)]) * Vector([5, 6]) + Vector((17.0, 39.0)) + + >>> Vector([5, 6]) * Matrix([(1, 2), (3, 4)]) + Vector((23.0, 34.0)) + """ + # Scalar multiplication + if isinstance(other, numbers.Number): + return self.__class__((x * other for x in r) for r in self._entries) + # Matrix-vector multiplication: Vector is a column Vector + elif isinstance(other, self.vector_cls): + # First, cast the other Vector as a Matrix, then do matrix-matrix + # multiplication, and lastly return the result as a Vector again. + return self._matrix_multiply(other.as_matrix()).as_vector() + # Matrix-matrix multiplication + elif isinstance(other, self.__class__): + return self._matrix_multiply(other) + return NotImplemented + + def __rmul__(self, other): + """See docstring for .__mul__().""" + # As scalar multiplication is commutative, we dispatch to .__mul__(). + if isinstance(other, numbers.Number): + return self * other + # Vector-matrix multiplication: Vector is a row Vector + elif isinstance(other, self.vector_cls): + return other.as_matrix(column=False)._matrix_multiply(self).as_vector() + return NotImplemented + + def __truediv__(self, other): + """Handle `self / other`. + + Divide a Matrix by a scalar. + + Example Usage: + >>> Matrix([(1, 2), (3, 4)]) / 4 + Matrix(((0.25, 0.5,), (0.75, 1.0,))) + """ + # As scalar division division is the same as multiplication + # with the inverse, we dispatch to .__mul__(). + if isinstance(other, numbers.Number): + return self * (1 / other) + return NotImplemented + + def __eq__(self, other): + """Handle `self == other`. + + Compare two Matrix instances for equality. + + Example Usage: + >>> Matrix([(1, 2), (3, 4)]) == Matrix([(1, 2), (3, 4)]) + True + + >>> Matrix([(1, 2), (3, 4)]) == Matrix([(5, 6), (7, 8)]) + False + """ + if isinstance(other, self.__class__): + if (self.n_rows != other.n_rows) or (self.n_cols != other.n_cols): + raise ValueError("matrices must have the same dimensions") + for x, y in zip(self, other): + if abs(x - y) > self.zero_threshold: + return False # exit early if two corresponding entries differ + return True + return NotImplemented + + def __pos__(self): + """Handle `+self`. + + This is simply an identity operator returning the Matrix itself. + """ + return self + + def __neg__(self): + """Handle `-self`. + + Negate all entries of a Matrix. + """ + return self.__class__((-x for x in r) for r in self._entries) + + def __abs__(self): + """The Frobenius norm of a Matrix.""" + return utils.norm(self) # uses the norm() function shared vector.Vector + + def __bool__(self): + """A Matrix is truthy if its Frobenius norm is strictly positive.""" + return bool(abs(self)) + + def __float__(self): + """Cast a Matrix as a scalar. + + Returns: + scalar (float) + + Raises: + RuntimeError: if the Matrix has more than one entry + """ + if not (self.n_rows == 1 and self.n_cols == 1): + raise RuntimeError("matrix must have exactly one entry to become a scalar") + return self[0] + + def as_vector(self): + """Get a Vector representation of a Matrix. + + Returns: + vector (vector.Vector) + + Raises: + RuntimeError: if one of the two dimensions, .n_rows or .n_cols, is not 1 + + Example Usage: + >>> Matrix([(1, 2, 3)]).as_vector() + Vector((1.0, 2.0, 3.0)) + """ + if not (self.n_rows == 1 or self.n_cols == 1): + raise RuntimeError("one dimension (m or n) must be 1") + return self.vector_cls(x for x in self) + + def transpose(self): + """Switch the rows and columns of a Matrix. + + Returns: + matrix (Matrix) + + Example Usage: + >>> m = Matrix([(1, 2), (3, 4)]) + >>> m + Matrix(((1.0, 2.0,), (3.0, 4.0,))) + >>> m.transpose() + Matrix(((1.0, 3.0,), (2.0, 4.0,))) + """ + return self.__class__(zip(*self._entries)) + + +# This import needs to be made here as otherwise an ImportError is raised. +# That is so as both the matrix and vector modules import a class from each other. +# We call that a circular import. Whereas Python handles "circular" references +# (e.g., both the Matrix and Vector classes have methods that reference the +# respective other class), that is forbidden for imports. +from sample_package import vector + +# This attribute cannot be set in the class definition +# as the vector module is only imported down here. +Matrix.vector_cls = vector.Vector diff --git a/11_classes/sample_package/utils.py b/11_classes/sample_package/utils.py new file mode 100644 index 0000000..2720372 --- /dev/null +++ b/11_classes/sample_package/utils.py @@ -0,0 +1,35 @@ +"""This module provides utilities for the whole package. + +The defined constants are used as defaults in the Vector and Matrix classes. + +The norm() function is shared by Vector.__abs__() and Matrix.__abs__(). +""" + +import math + + +# Define constants (i.e., normal variables that are, by convention, named in UPPERCASE) +# that are used as the defaults for class attributes within Vector and Matrix. +DEFAULT_ENTRIES_STORAGE = tuple +DEFAULT_ENTRY_TYPE = float +ZERO_THRESHOLD = 1e-12 + + +def norm(vec_or_mat): + """Calculate the Frobenius or Euclidean norm of a matrix or vector. + + Find more infos here: https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm + + Args: + vec_or_mat (Vector / Matrix): object whose entries are squared and summed up + + Returns: + norm (float) + + Example Usage: + As Vector and Matrix objects are by design non-empty sequences, + norm() may be called, for example, with `[3, 4]` as the argument: + >>> norm([3, 4]) + 5.0 + """ + return math.sqrt(sum(x ** 2 for x in vec_or_mat)) diff --git a/11_classes/sample_package/vector.py b/11_classes/sample_package/vector.py new file mode 100644 index 0000000..e9315c4 --- /dev/null +++ b/11_classes/sample_package/vector.py @@ -0,0 +1,262 @@ +"""This module defines a Vector class.""" + +# Imports from the standard library go first ... +import numbers + +# ... and are followed by project-internal ones. +# If third-party libraries are needed, they are +# put into a group on their own in between. +# Within a group, imports are sorted lexicographically. +from sample_package import matrix +from sample_package import utils + + +class Vector: + """A one-dimensional vector from linear algebra. + + All entries are converted to floats, or whatever is set in the typing attribute. + + Attributes: + matrix_cls (matrix.Matrix): a reference to the Matrix class to work with + storage (callable): data type used to store the entries internally; + defaults to tuple + typing (callable): type casting applied to all entries upon creation; + defaults to float + zero_threshold (float): max. tolerance when comparing an entry to zero; + defaults to 1e-12 + """ + + matrix_cls = matrix.Matrix + storage = utils.DEFAULT_ENTRIES_STORAGE + typing = utils.DEFAULT_ENTRY_TYPE + zero_threshold = utils.ZERO_THRESHOLD + + def __init__(self, data): + """Create a new vector. + + Args: + data (sequence): the vector's entries + + Raises: + ValueError: if no entries are provided + + Example Usage: + >>> Vector([1, 2, 3]) + Vector((1.0, 2.0, 3.0)) + + >>> Vector(range(3)) + Vector((0.0, 1.0, 2.0)) + """ + self._entries = self.storage(self.typing(x) for x in data) + if len(self) == 0: + raise ValueError("a vector must have at least one entry") + + def __repr__(self): + """Text representation of a Vector.""" + name = self.__class__.__name__ + args = ", ".join(repr(x) for x in self) + return f"{name}(({args}))" + + def __str__(self): + """Human-readable text representation of a Vector.""" + name = self.__class__.__name__ + first, last, n_entries = self[0], self[-1], len(self) + return f"{name}({first!r}, ..., {last!r})[{n_entries:d}]" + + def __len__(self): + """Number of entries in a Vector.""" + return len(self._entries) + + def __getitem__(self, index): + """Obtain an individual entry of a Vector.""" + if not isinstance(index, int): + raise TypeError("index must be an integer") + return self._entries[index] + + def __iter__(self): + """Loop over a Vector's entries.""" + return iter(self._entries) + + def __reversed__(self): + """Loop over a Vector's entries in reverse order.""" + return reversed(self._entries) + + def __add__(self, other): + """Handle `self + other` and `other + self`. + + This may be either vector addition or broadcasting addition. + + Example Usage: + >>> Vector([1, 2, 3]) + Vector([2, 3, 4]) + Vector((3.0, 5.0, 7.0)) + + >>> Vector([1, 2, 3]) + 4 + Vector((5.0, 6.0, 7.0)) + + >>> 10 + Vector([1, 2, 3]) + Vector((11.0, 12.0, 13.0)) + """ + # Vector addition + if isinstance(other, self.__class__): + if len(self) != len(other): + raise ValueError("vectors must be of the same length") + return self.__class__(x + y for (x, y) in zip(self, other)) + # Broadcasting addition + elif isinstance(other, numbers.Number): + return self.__class__(x + other for x in self) + return NotImplemented + + def __radd__(self, other): + """See docstring for .__add__().""" + # As both vector and broadcasting addition are commutative, + # we dispatch to .__add__(). + return self + other + + def __sub__(self, other): + """Handle `self - other` and `other - self`. + + This may be either vector subtraction or broadcasting subtraction. + + Example Usage: + >>> Vector([7, 8, 9]) - Vector([1, 2, 3]) + Vector((6.0, 6.0, 6.0)) + + >>> Vector([1, 2, 3]) - 1 + Vector((0.0, 1.0, 2.0)) + + >>> 10 - Vector([1, 2, 3]) + Vector((9.0, 8.0, 7.0)) + """ + # As subtraction is the inverse of addition, + # we first dispatch to .__neg__() to invert the signs of + # all entries in other and then dispatch to .__add__(). + return self + (-other) + + def __rsub__(self, other): + """See docstring for .__sub__().""" + # Same comments as in .__sub__() apply + # with the roles of self and other swapped. + return (-self) + other + + def __mul__(self, other): + """Handle `self * other` and `other * self`. + + This may be either the dot product of two vectors or scalar multiplication. + + Example Usage: + >>> Vector([1, 2, 3]) * Vector([2, 3, 4]) + 20.0 + + >>> 2 * Vector([1, 2, 3]) + Vector((2.0, 4.0, 6.0)) + + >>> Vector([1, 2, 3]) * 3 + Vector((3.0, 6.0, 9.0)) + """ + # Dot product + if isinstance(other, self.__class__): + if len(self) != len(other): + raise ValueError("vectors must be of the same length") + return sum(x * y for (x, y) in zip(self, other)) + # Scalar multiplication + elif isinstance(other, numbers.Number): + return self.__class__(x * other for x in self) + return NotImplemented + + def __rmul__(self, other): + """See docstring for .__mul__().""" + # As both dot product and scalar multiplication are commutative, + # we dispatch to .__mul__(). + return self * other + + def __truediv__(self, other): + """Handle `self / other`. + + Divide a Vector by a scalar. + + Example Usage: + >>> Vector([9, 6, 12]) / 3 + Vector((3.0, 2.0, 4.0)) + """ + # As scalar division division is the same as multiplication + # with the inverse, we dispatch to .__mul__(). + if isinstance(other, numbers.Number): + return self * (1 / other) + return NotImplemented + + def __eq__(self, other): + """Handle `self == other`. + + Compare two Vectors for equality. + + Example Usage: + >>> Vector([1, 2, 3]) == Vector([1, 2, 3]) + True + + >>> Vector([1, 2, 3]) == Vector([4, 5, 6]) + False + """ + if isinstance(other, self.__class__): + if len(self) != len(other): + raise ValueError("vectors must be of the same length") + for x, y in zip(self, other): + if abs(x - y) > self.zero_threshold: + return False # exit early if two corresponding entries differ + return True + return NotImplemented + + def __pos__(self): + """Handle `+self`. + + This is simply an identity operator returning the Vector itself. + """ + return self + + def __neg__(self): + """Handle `-self`. + + Negate all entries of a Vector. + """ + return self.__class__(-x for x in self) + + def __abs__(self): + """The Euclidean norm of a vector.""" + return utils.norm(self) # uses the norm() function shared matrix.Matrix + + def __bool__(self): + """A Vector is truthy if its Euclidean norm is strictly positive.""" + return bool(abs(self)) + + def __float__(self): + """Cast a Vector as a scalar. + + Returns: + scalar (float) + + Raises: + RuntimeError: if the Vector has more than one entry + """ + if len(self) != 1: + raise RuntimeError("vector must have exactly one entry to become a scalar") + return self[0] + + def as_matrix(self, *, column=True): + """Get a Matrix representation of a Vector. + + Args: + column (bool): if the vector is interpreted as a + column vector or a row vector; defaults to True + + Returns: + matrix (matrix.Matrix) + + Example Usage: + >>> v = Vector([1, 2, 3]) + >>> v.as_matrix() + Matrix(((1.0,), (2.0,), (3.0,))) + >>> v.as_matrix(column=False) + Matrix(((1.0, 2.0, 3.0,))) + """ + if column: + return self.matrix_cls([x] for x in self) + return self.matrix_cls([(x for x in self)]) diff --git a/CONTENTS.md b/CONTENTS.md new file mode 100644 index 0000000..c958a72 --- /dev/null +++ b/CONTENTS.md @@ -0,0 +1,294 @@ +# Table of Contents + +The materials are designed to resemble an *interactive* book. + +The files come + primarily in the [Jupyter Notebook](https://jupyter-notebook.readthedocs.io/en/stable/) + format (i.e., \*.ipynb) + but also as [modules and packages ](https://docs.python.org/3/tutorial/modules.html) + (i.e., \*.py). +Together with some other static files (e.g., images), + they are stored in one folder per chapter in this repository. +They are to be opened + from within the [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) application, + even though other ways are certainly possible as well. +Both the files and the folders + are appropriately named with prefixes + indicating the order in which they should be read + and starting with "00_". + +It is recommended + to follow the [installation instructions](https://github.com/webartifex/intro-to-python#installation) + in the [README.md](README.md) file + and work through the content on one's own computer. + +If this is not possible, + the files can be viewed in a web browser + either statically (i.e., read-only) on [nbviewer ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/tree/main/) + or interactively (i.e., code can be executed) on [Binder ](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab). + +- *Chapter 0*: Introduction + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/00_intro/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/00_intro/00_content.ipynb) + (Python's History & Background; + Open-source & Communities; + JupyterLab; + Programming vs. Computer Science; + Learning Tips) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/00_intro/01_exercises_markdown.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/00_intro/01_exercises_markdown.ipynb) + (Mastering Markdown) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/00_intro/02_review.ipynb) +- **Part A: Expressing Logic** + - *Chapter 1*: Elements of a Program + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/00_content.ipynb) + (A first Example: Averaging Even Numbers; + Operators; + Objects & Data Types; + Errors) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/01_exercises_print.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/01_exercises_print.ipynb) + (Printing Output) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/02_exercises_for-loops.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/02_exercises_for-loops.ipynb) + (Simple `for`-loops) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/03_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/03_content.ipynb) + (Memory in Detail; + Variables & References; + Mutability; + Expressions & Statements) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/04_exercises_calculator.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/04_exercises_calculator.ipynb) + (Python as a Calculator) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/05_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/06_review.ipynb) + - [further resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/01_elements/07_resources.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/01_elements/07_resources.ipynb) + - *Chapter 2*: Functions & Modularization + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/02_functions/00_content.ipynb) + (Built-in Functions & Constructors; + Function Definitions; + Function Calls & Scoping Rules; + Positional vs. Keyword Arguments; + Modularization) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/01_exercises_sphere-volume.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/02_functions/01_exercises_sphere-volume.ipynb) + (Volume of a Sphere) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/02_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/02_functions/02_content.ipynb) + (Standard Library: `math` & `random` Modules; + Third-party Packages: `numpy` Library; + Writing one's own Modules) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/03_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/02_functions/04_review.ipynb) + - *Chapter 3*: Conditionals & Exceptions + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/03_conditionals/00_content.ipynb) + (Boolean Expressions; + Relational Operators; + Logical Operators; + `if` statement; + Exception Handling) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/01_exercises_discounts.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/03_conditionals/01_exercises_discounts.ipynb) + (Discounting Customer Orders) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/02_exercises_fizz-buzz.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/03_conditionals/02_exercises_fizz-buzz.ipynb) + (Fizz Buzz) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/03_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/03_conditionals/04_review.ipynb) + - *Chapter 4*: Recursion & Looping + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/00_content.ipynb) + (Recursion; + Examples: Factorial, Euclid's Algorithm, & Fibonacci; + Duck Typing; + Type Casting & Checking; + Input Validation) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/01_exercises_hanoi-towers.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/01_exercises_hanoi-towers.ipynb) + (Towers of Hanoi) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/02_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/02_content.ipynb) + (Looping with `while` & `for`; + Examples: Collatz Conjecture, Factorial, Euclid's Algorithm, & Fibonacci; + Containers vs. Iterables) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/03_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/03_content.ipynb) + (Customizing Loops with `break` and `continue`; + Indefinite Loops) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/04_exercises_dice.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/04_iteration/04_exercises_dice.ipynb) + (Throwing Dice) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/05_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/04_iteration/06_review.ipynb) +- **Part B: Managing Data and Memory** + - *Chapter 5*: Numbers & Bits + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/00_content.ipynb) + (`int` Type; + Binary & Hexadecimal Representations; + Bit Arithmetic; + Bitwise Operators) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/01_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/01_content.ipynb) + (`float` Type; + Floating-point Standard; + Special Values; + Imprecision; + Binary & Hexadecimal Representations) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/02_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/02_content.ipynb) + (`complex` Type; + Numerical Tower; + Duck vs. Goose Typing) + - [appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/03_appendix.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/03_appendix.ipynb) + (`Decimal` Type; + `Fraction` Type) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/04_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/05_review.ipynb) + - [further resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/05_numbers/06_resources.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/05_numbers/06_resources.ipynb) + - *Chapter 6*: Text & Bytes + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/00_content.ipynb) + (`str` Type; + Reading Files; + Sequences; + Indexing & Slicing; + String Methods & Operations; + String Interpolation) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/01_exercises_palindromes.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/01_exercises_palindromes.ipynb) + (Detecting Palindromes) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/02_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/02_content.ipynb) + (Special Characters; + ASCII & Unicode; + Multi-line Strings; + `bytes` Type; + Character Encodings) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/03_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/04_review.ipynb) + - [further resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/06_text/05_resources.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/06_text/05_resources.ipynb) + - *Chapter 7*: Sequential Data + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/00_content.ipynb) + (Collections vs. Sequences; + ABCs: `Container`, `Iterable`, `Sized`, & `Reversible`) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/01_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/01_content.ipynb) + (`list` Type; + Indexing & Slicing; + Shallow vs. Deep Copies; + List Methods & Operations) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/02_exercises_lists.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/02_exercises_lists.ipynb) + (Working with Lists) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/03_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/03_content.ipynb) + (Modifiers vs. Pure Functions; + `tuple` Type; + Packing & Unpacking; + `*args` in Function Definitions) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/04_exercises_un-packing.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/04_exercises_un-packing.ipynb) + (Packing & Unpacking with Functions) + - [appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/05_appendix.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/07_sequences/05_appendix.ipynb) + (`namedtuple` Type) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/06_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/07_sequences/07_review.ipynb) + - *Chapter 8*: Map, Filter, & Reduce + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/00_content.ipynb) + (Mapping; + Filtering; + Reducing; + `lambda` Expression) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/01_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/01_content.ipynb) + (`list` Comprehension; + `generator` Expression; + Streams of Data; + Boolean Reducers) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/02_exercises_outliers.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/02_exercises_outliers.ipynb) + (Removing Outliers in Streaming Data) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/03_exercises_un-packing.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/03_exercises_un-packing.ipynb) + (Packing & Unpacking with Functions, continued) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/04_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/08_mfr/04_content.ipynb) + (Iterators vs. Iterables; + Example: `sorted()` vs. `reversed()`) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/05_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/08_mfr/06_review.ipynb) + - *Chapter 9*: Mappings & Sets + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/00_content.ipynb) + (`dict` Type; + Nested Data; + Hash Tables; + `dict` Methods & Behavior; + `dict` Comprehension) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/01_exercises_nested-data.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/01_exercises_nested-data.ipynb) + (Working with Nested Data) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/02_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/02_content.ipynb) + (`**kwargs` in Function Definitions; + Memoization) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/03_exercises_fibonacci.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/03_exercises_fibonacci.ipynb) + (Memoization without Side Effects) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/04_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/04_content.ipynb) + (`set` Type; + `set` Methods & Operations; + `set` Comprehension; + `frozenset` Type) + - [appendix ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/05_appendix.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/05_appendix.ipynb) + (`defaultdict` Type; + `Counter` Type; + `ChainMap` Type) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/06_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/07_review.ipynb) + - [further resources ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/09_mappings/08_resources.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/09_mappings/08_resources.ipynb) + - *Chapter 10*: Arrays & Dataframes + - *Chapter 11*: Classes & Instances + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/00_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/00_content.ipynb) + (`class` Statement; + Instantiation; + Text Representations; + Instance Methods vs. Class Methods; + Computed Properties) + - [exercises ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/01_exercises_tsp.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/01_exercises_tsp.ipynb) + (A Traveling Salesman Problem) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/02_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/02_content.ipynb) + (Sequence Emulation & Iteration; + Python's Data Model; + (Im)mutable Data Types; + Method Chaining; + Polymorphism) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/03_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/03_content.ipynb) + (Operator Overloading: Arithmetic & Relational Operators; + Number Emulation) + - [content ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/04_content.ipynb) + [](https://mybinder.org/v2/gh/webartifex/intro-to-python/main?urlpath=lab/tree/11_classes/04_content.ipynb) + (Writing one's own Packages; + The final `Vector` & `Matrix` Classes; + Comparison with `numpy`) + - [summary ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/05_summary.ipynb) + - [review questions ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/main/11_classes/06_review.ipynb) diff --git a/LICENSE.txt b/LICENSE.txt index 0ec444a..19656d3 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2020 Alexander Hess [alexander@webartifex.biz] +Copyright (c) 2018-2024 Alexander Hess [alexander@webartifex.biz] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4d95cf0..c1b0db9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,257 @@ # An Introduction to Python and Programming -This project is a thorough introductory course -in programming with **[Python ](https://www.python.org/)**. +This project is a *thorough* introductory course + in programming with **[Python ](https://www.python.org/)**. + + +### Table of Contents + +The following is a high-level overview of the contents. +For a more *detailed version* with **clickable links** + see the [CONTENTS.md](CONTENTS.md) file. + +- *Chapter 0*: Introduction +- **Part A: Expressing Logic** + - *Chapter 1*: Elements of a Program + - *Chapter 2*: Functions & Modularization + - *Chapter 3*: Conditionals & Exceptions + - *Chapter 4*: Recursion & Looping +- **Part B: Managing Data and Memory** + - *Chapter 5*: Numbers & Bits + - *Chapter 6*: Text & Bytes + - *Chapter 7*: Sequential Data + - *Chapter 8*: Map, Filter, & Reduce + - *Chapter 9*: Mappings & Sets + - *Chapter 10*: Arrays & Dataframes + - *Chapter 11*: Classes & Instances + + +#### Videos + +Presentations of the chapters are available on this [YouTube playlist ](https://www.youtube.com/playlist?list=PL-2JV1G3J10kRUPgP7EwLhyeN5lOZW2kH). +The recordings are about 25 hours long in total + and were made in spring 2020 + after a corresponding in-class Bachelor course was cancelled due to Corona. + + +### Objective The **main goal** is to **prepare** students -for **further studies** in the "field" of **data science**. + for **further studies** in the "field" of **data science**, + including but not limited to topics such as: +- algorithms & data structures +- data cleaning & wrangling +- data visualization +- data engineering (incl. SQL databases) +- data mining (incl. web scraping) +- linear algebra +- machine learning (incl. feature generation & deep learning) +- optimization & (meta-)heuristics (incl. management science & operations research) +- statistics & econometrics +- quantitative finance (e.g., option valuation) +- quantitative marketing (e.g., customer segmentation) +- quantitative supply chain management (e.g., forecasting) +- web development (incl. APIs) + + +### Prerequisites + +To be suitable for *beginners*, there are *no* formal prerequisites. +It is only expected that the student has: +- a *solid* understanding of the **English** language, +- knowledge of **basic mathematics** from high school, +- the ability to **think conceptually** and **reason logically**, and +- the willingness to **invest** around **90-120 hours** on this course. + + +## Getting started + +If you are a total beginner, + follow the instructions in the "Installation" section next. +If you are familiar with + the [git](https://git-scm.com/) + and [poetry](https://python-poetry.org/docs/) command-line tools, + you may want to look at the "Alternative Installation" section further below. + + +### Installation + +To follow this course, an installation of **Python 3.11** or higher is expected. + +A popular and beginner friendly way is + to install the [Anaconda Distribution](https://www.anaconda.com/download) + that not only ships Python itself + but also comes pre-packaged with a lot of third-party libraries. + + + +Scroll down to the "Anaconda Installers" section + and install the latest version for your operating system + (i.e., *2024-02* with Python 3.11 at the time of this writing). + +After installation, + you find an entry "[Anaconda Navigator](https://docs.anaconda.com/free/navigator/)" + in your start menu. +Click on it. + + + +A window opens giving you several options to start various applications. +In the beginning, we will work mostly with [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/). +Click on "Launch". + + + +A new tab in your web browser opens: +The website is "localhost" and some number (e.g., 8888). + +This is the [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) application + that is used to display the course materials. +On the left, you see the files and folders on your computer. +This file browser works like any other. +In the center, you see several options to launch (i.e., "create") new files. + + + +To check if your Python installation works, + double-click on the "Python 3" tile under the "Notebook" section. +That opens a new [Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/) + named "Untitled.ipynb". + + + +Enter some basic Python in the **code cell**, for example, `1 + 2`. +Then, press the **Enter** key *while* holding down the **Control** key + (if that does not work, try with the **Shift** key) + to **execute** the snippet. +The result of the calculation, `3` in the example, shows up below the cell. + + + +After setting up Python, + click on the green "Code" button on the top right on this website + to download the course materials. +As a beginner, choosing "Download ZIP" is likely the easiest option. +Then, unpack the ZIP file into a folder of your choice, + ideally somewhere within your personal user folder + so that the files show up right away in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/). + + + + +### Alternative Installation (for Instructors using Linux) + +Python can also be installed in a "pure" way + obtained directly from its core development team [here](https://www.python.org/downloads/). +Then, it comes *without* any third-party packages, + which is *not* a problem at all. +Managing third-party packages can be automated to a large degree, + for example, with tools such as [poetry](https://python-poetry.org/docs/). + +However, this may be too "advanced" for a beginner + as it involves working with a [command-line interface ](https://en.wikipedia.org/wiki/Command-line_interface) (CLI), + also called a **terminal**, + which looks like the one below. +It is used *without* a mouse by typing commands into it. +The following instructions assume that + [git](https://git-scm.com/), [poetry](https://python-poetry.org/docs/), + and [pyenv](https://github.com/pyenv/pyenv) are installed. + + + +The screenshot above shows how this project can be set up in an alternative way + with the [zsh](https://en.wikipedia.org/wiki/Z_shell) CLI. + +First, [git](https://git-scm.com/) is used + to **clone** the course materials as a **repository** + into a new folder called "*intro-to-python*" + that lives under a "*repos*" folder. + +- `git clone https://github.com/webartifex/intro-to-python.git` + +The `cd` command is used to "change directories". + +In the screenshot, [pyenv](https://github.com/pyenv/pyenv) is used + to set the project's Python version. +[pyenv](https://github.com/pyenv/pyenv)'s purpose is + to manage *many* parallel Python installations on the same computer. +It is highly recommended for professional users; + however, any other way of installing Python works as well. + +- `pyenv local ...` + +On the contrary, [poetry](https://python-poetry.org/docs/)'s purpose is + to manage third-party packages within the *same* Python installation + and, more importantly, on a per-project basis. +So, for example, +whereas "Project A" may depend on [numpy](https://numpy.org/) *v1.19* + from June 2020 be installed, + "Project B" may use *v1.14* from January 2018 instead + (cf., numpy's [release history](https://pypi.org/project/numpy/#history)). +To achieve this per-project **isolation**, +[poetry](https://python-poetry.org/docs/) uses so-called **virtual environments** + behind the scenes. +While one could do that manually, + for example, by using Python's built-in + [venv ](https://docs.python.org/3/library/venv.html) module, + it is more convenient and reliable to have [poetry](https://python-poetry.org/docs/) + automate this. +The following *one* command not only + creates a new virtual environment (manually: `python -m venv venv`) + and *activates* it (manually: `source venv/bin/activate`), + it also installs the versions of the project's third-party dependencies + as specified in the [poetry.lock](poetry.lock) file + (manually: `python -m pip install -r requirements.txt` + if a [requirements.txt](https://docs.python.org/3/tutorial/venv.html#managing-packages-with-pip) + file is used; + the `python -m` part is often left out [but should not be](https://snarky.ca/why-you-should-use-python-m-pip/)): + +- `poetry install` + +[poetry](https://python-poetry.org/docs/) is also used + to execute commands in the project's (virtual) environment. +To do that, the command is prefixed with `poetry run ...`. + +The project uses [nox](https://nox.thea.codes/en/stable/) + to manage various maintenance tasks. +After cloning the repository and setting up the virual environment, + it is recommended to run the initialization task. +That needs to be done only once. + +- `poetry run nox -s init-project` + +To do the equivalent of clicking "Launch" in the Anaconda Navigator: + +- `poetry run jupyter lab` + +This opens a new tab in your web browser just as above. +The command-line interface stays open in the background, + like in the screenshot below, + and prints log messages as we work in [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/). + + + + +## Contributing + +Feedback **is highly encouraged** and will be incorporated. +Open an issue in the [issues tracker ](https://github.com/webartifex/intro-to-python/issues) + or initiate a [pull request ](https://help.github.com/en/articles/about-pull-requests) + if you are familiar with the concept. +Simple issues that *anyone* can **help fix** are, for example, + **spelling mistakes** or **broken links**. +If you feel that some topic is missing entirely, you may also mention that. +The materials here are considered a **permanent work-in-progress**. + +A "Show HN" post about this course was made on [Hacker News ](https://news.ycombinator.com/item?id=22669084) + and some ideas for improvement were discussed there. + + +## About the Author + +Alexander Hess is a PhD student + at the Chair of Logistics Management at [WHU - Otto Beisheim School of Management](https://www.whu.edu) + where he conducts research on urban delivery platforms + and teaches coding courses based on Python in the BSc and MBA programs. + +Connect him on [LinkedIn](https://www.linkedin.com/in/webartifex). diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..7a54a68 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,154 @@ +"""Configure nox as the task runner. + +Nox provides the following tasks: + +- "init-project": install the pre-commit hooks + +- "doctests": run the xdoctests in the source files + +- "fix-branch-references": adjusts links with git branch references in + various files (e.g., Mardown or notebooks) + +""" + +import contextlib +import glob +import os +import re +import shutil +import subprocess +import tempfile + +import nox + + +REPOSITORY = "webartifex/intro-to-python" + +SRC_LOCATIONS = ( + "02_functions/sample_module.py", + "11_classes/sample_package", +) + +# Use a unified .cache/ folder for all develop tools. +nox.options.envdir = ".cache/nox" + +# All tools except git and poetry are project dependencies. +# Avoid accidental successes if the environment is not set up properly. +nox.options.error_on_external_run = True + + +@nox.session(name="init-project", venv_backend="none") +def init_project(session): + """Install the pre-commit hooks.""" + for type_ in ( + "pre-commit", + "pre-merge-commit", + ): + session.run("poetry", "run", "pre-commit", "install", f"--hook-type={type_}") + + +@nox.session(venv_backend="none") +def doctests(session): + """Run the xdoctests in the source files.""" + for location in SRC_LOCATIONS: + session.run("poetry", "run", "xdoctest", "--silent", location) + + +@nox.session(name="fix-branch-references", venv_backend="none") +def fix_branch_references(_session): + """Change git branch references. + + Intended to be run as a pre-commit hook. + + Many files in the project (e.g., README.md) contain links to resources on + github.com, nbviewer.jupyter.org, or mybinder.org that contain git branch + labels. + + This task rewrites branch labels into either "main" or "develop". + """ + # Glob patterns that expand into the files whose links are re-written. + paths = ["*.md", "**/*.ipynb"] + + branch = ( + subprocess.check_output( + ("git", "rev-parse", "--abbrev-ref", "HEAD"), + ) + .decode() + .strip() + ) + # If the current branch is only temporary and will be merged into "main", ... + if branch.startswith("release-") or branch.startswith("hotfix-"): + branch = "main" + # If the branch is not "main", we assume it is a feature branch. + elif branch != "main": + branch = "develop" + + rewrites = [ + { + "name": "github", + "pattern": re.compile( + rf"((((http)|(https))://github\.com/{REPOSITORY}/((blob)|(tree))/)([\w-]+)/)" + ), + "replacement": rf"\2{branch}/", + }, + { + "name": "nbviewer", + "pattern": re.compile( + rf"((((http)|(https))://nbviewer\.jupyter\.org/github/{REPOSITORY}/((blob)|(tree))/)([\w-]+)/)", + ), + "replacement": rf"\2{branch}/", + }, + { + "name": "mybinder", + "pattern": re.compile( + rf"((((http)|(https))://mybinder\.org/v2/gh/{REPOSITORY}/)([\w-]+)\?)", + ), + "replacement": rf"\2{branch}?", + }, + ] + + for expanded in _expand(*paths): + with _line_by_line_replace(expanded) as (old_file, new_file): + for line in old_file: + for rewrite in rewrites: + line = re.sub(rewrite["pattern"], rewrite["replacement"], line) + new_file.write(line) + + +def _expand(*patterns): + """Expand glob patterns into paths. + + Args: + *patterns: the patterns to be expanded + + Yields: + path: a single expanded path + """ + for pattern in patterns: + yield from glob.glob(pattern.strip()) + + +@contextlib.contextmanager +def _line_by_line_replace(path): + """Replace/change the lines in a file one by one. + + This generator function yields two file handles, one to the current file + (i.e., `old_file`) and one to its replacement (i.e., `new_file`). + + Usage: loop over the lines in `old_file` and write the files to be kept + to `new_file`. Files not written to `new_file` are removed! + + Args: + path: the file whose lines are to be replaced + + Yields: + old_file, new_file: handles to a file and its replacement + """ + file_handle, new_file_path = tempfile.mkstemp() + with os.fdopen(file_handle, "w") as new_file: + with open(path) as old_file: + yield old_file, new_file + + shutil.copymode(path, new_file_path) + os.remove(path) + shutil.move(new_file_path, path) diff --git a/poetry.lock b/poetry.lock index b9e1c09..ac23fb3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,8 +1,2234 @@ -package = [] +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "argcomplete" +version = "3.2.3" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +files = [ + {file = "argcomplete-3.2.3-py3-none-any.whl", hash = "sha256:c12355e0494c76a2a7b73e3a59b09024ca0ba1e279fb9ed6c1b82d5b74b6a70c"}, + {file = "argcomplete-3.2.3.tar.gz", hash = "sha256:bf7900329262e481be5a15f56f19736b376df6f82ed27576fa893652c5de6c23"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "argon2-cffi" +version = "23.1.0" +description = "Argon2 for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["argon2-cffi[tests,typing]", "tox (>4)"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] +tests = ["hypothesis", "pytest"] +typing = ["mypy"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "async-lru" +version = "2.0.4" +description = "Simple LRU cache for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, + {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "colorlog" +version = "6.8.2" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"}, + {file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "debugpy" +version = "1.8.1" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, + {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, + {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, + {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, + {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, + {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, + {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, + {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, + {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, + {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, + {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, + {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, + {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, + {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, + {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, + {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, + {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, + {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, + {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, + {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, + {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, + {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "fastjsonschema" +version = "2.19.1" +description = "Fastest Python implementation of JSON schema" +optional = false +python-versions = "*" +files = [ + {file = "fastjsonschema-2.19.1-py3-none-any.whl", hash = "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0"}, + {file = "fastjsonschema-2.19.1.tar.gz", hash = "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"}, +] + +[package.extras] +devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] + +[[package]] +name = "filelock" +version = "3.13.3" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, + {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "ipykernel" +version = "6.29.4" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, + {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.23.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.23.0-py3-none-any.whl", hash = "sha256:07232af52a5ba146dc3372c7bf52a0f890a23edf38d77caef8d53f9cdc2584c1"}, + {file = "ipython-8.23.0.tar.gz", hash = "sha256:7468edaf4f6de3e1b912e57f66c241e6fd3c7099f2ec2136e239e142e800274d"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5.13.0" +typing-extensions = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] +kernel = ["ipykernel"] +matplotlib = ["matplotlib"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "json5" +version = "0.9.24" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8" +files = [ + {file = "json5-0.9.24-py3-none-any.whl", hash = "sha256:4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13"}, + {file = "json5-0.9.24.tar.gz", hash = "sha256:0c638399421da959a20952782800e5c1a78c14e08e1dc9738fa10d8ec14d58c8"}, +] + +[[package]] +name = "jsonpointer" +version = "2.4" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} +rpds-py = ">=0.7.1" +uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jupyter-client" +version = "8.6.1" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.1-py3-none-any.whl", hash = "sha256:3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f"}, + {file = "jupyter_client-8.6.1.tar.gz", hash = "sha256:e842515e2bab8e19186d89fdfea7abd15e39dd581f94e399f00e2af5a1652d3f"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyter-events" +version = "0.10.0" +description = "Jupyter Event System library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, + {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, +] + +[package.dependencies] +jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} +python-json-logger = ">=2.0.4" +pyyaml = ">=5.3" +referencing = "*" +rfc3339-validator = "*" +rfc3986-validator = ">=0.1.1" +traitlets = ">=5.3" + +[package.extras] +cli = ["click", "rich"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] + +[[package]] +name = "jupyter-lsp" +version = "2.2.4" +description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter-lsp-2.2.4.tar.gz", hash = "sha256:5e50033149344065348e688608f3c6d654ef06d9856b67655bd7b6bac9ee2d59"}, + {file = "jupyter_lsp-2.2.4-py3-none-any.whl", hash = "sha256:da61cb63a16b6dff5eac55c2699cc36eac975645adee02c41bdfc03bf4802e77"}, +] + +[package.dependencies] +jupyter-server = ">=1.1.2" + +[[package]] +name = "jupyter-server" +version = "2.13.0" +description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server-2.13.0-py3-none-any.whl", hash = "sha256:77b2b49c3831fbbfbdb5048cef4350d12946191f833a24e5f83e5f8f4803e97b"}, + {file = "jupyter_server-2.13.0.tar.gz", hash = "sha256:c80bfb049ea20053c3d9641c2add4848b38073bf79f1729cea1faed32fc1c78e"}, +] + +[package.dependencies] +anyio = ">=3.1.0" +argon2-cffi = "*" +jinja2 = "*" +jupyter-client = ">=7.4.4" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-events = ">=0.9.0" +jupyter-server-terminals = "*" +nbconvert = ">=6.4.4" +nbformat = ">=5.3.0" +overrides = "*" +packaging = "*" +prometheus-client = "*" +pywinpty = {version = "*", markers = "os_name == \"nt\""} +pyzmq = ">=24" +send2trash = ">=1.8.2" +terminado = ">=0.8.3" +tornado = ">=6.2.0" +traitlets = ">=5.6.0" +websocket-client = "*" + +[package.extras] +docs = ["ipykernel", "jinja2", "jupyter-client", "jupyter-server", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] +test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +description = "A Jupyter Server Extension Providing Terminals." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, + {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, +] + +[package.dependencies] +pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} +terminado = ">=0.8.3" + +[package.extras] +docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] +test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] + +[[package]] +name = "jupyterlab" +version = "4.1.5" +description = "JupyterLab computational environment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab-4.1.5-py3-none-any.whl", hash = "sha256:3bc843382a25e1ab7bc31d9e39295a9f0463626692b7995597709c0ab236ab2c"}, + {file = "jupyterlab-4.1.5.tar.gz", hash = "sha256:c9ad75290cb10bfaff3624bf3fbb852319b4cce4c456613f8ebbaa98d03524db"}, +] + +[package.dependencies] +async-lru = ">=1.0.0" +httpx = ">=0.25.0" +ipykernel = "*" +jinja2 = ">=3.0.3" +jupyter-core = "*" +jupyter-lsp = ">=2.0.0" +jupyter-server = ">=2.4.0,<3" +jupyterlab-server = ">=2.19.0,<3" +notebook-shim = ">=0.2" +packaging = "*" +tornado = ">=6.2.0" +traitlets = "*" + +[package.extras] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.2.0)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.2.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.1)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.0.post6)", "matplotlib (==3.8.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.0)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] +test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +description = "Pygments theme using JupyterLab CSS variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, + {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, +] + +[[package]] +name = "jupyterlab-server" +version = "2.26.0" +description = "A set of server components for JupyterLab and JupyterLab like applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyterlab_server-2.26.0-py3-none-any.whl", hash = "sha256:54622cbd330526a385ee0c1fdccdff3a1e7219bf3e864a335284a1270a1973df"}, + {file = "jupyterlab_server-2.26.0.tar.gz", hash = "sha256:9b3ba91cf2837f7f124fca36d63f3ca80ace2bed4898a63dd47e6598c1ab006f"}, +] + +[package.dependencies] +babel = ">=2.10" +jinja2 = ">=3.0.3" +json5 = ">=0.9.0" +jsonschema = ">=4.18.0" +jupyter-server = ">=1.21,<3" +packaging = ">=21.3" +requests = ">=2.31" + +[package.extras] +docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] +openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] +test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mistune" +version = "3.0.2" +description = "A sane and fast Markdown parser with useful plugins and renderers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, + {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, +] + +[[package]] +name = "nbclient" +version = "0.10.0" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, + {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, +] + +[package.dependencies] +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +nbformat = ">=5.1" +traitlets = ">=5.4" + +[package.extras] +dev = ["pre-commit"] +docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] + +[[package]] +name = "nbconvert" +version = "7.16.3" +description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbconvert-7.16.3-py3-none-any.whl", hash = "sha256:ddeff14beeeedf3dd0bc506623e41e4507e551736de59df69a91f86700292b3b"}, + {file = "nbconvert-7.16.3.tar.gz", hash = "sha256:a6733b78ce3d47c3f85e504998495b07e6ea9cf9bf6ec1c98dda63ec6ad19142"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +bleach = "!=5.0.0" +defusedxml = "*" +jinja2 = ">=3.0" +jupyter-core = ">=4.7" +jupyterlab-pygments = "*" +markupsafe = ">=2.0" +mistune = ">=2.0.3,<4" +nbclient = ">=0.5.0" +nbformat = ">=5.7" +packaging = "*" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +tinycss2 = "*" +traitlets = ">=5.1" + +[package.extras] +all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"] +docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] +qtpdf = ["nbconvert[qtpng]"] +qtpng = ["pyqtwebengine (>=5.15)"] +serve = ["tornado (>=6.1)"] +test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] +webpdf = ["playwright"] + +[[package]] +name = "nbformat" +version = "5.10.4" +description = "The Jupyter Notebook format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[package.dependencies] +fastjsonschema = ">=2.15" +jsonschema = ">=2.6" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +traitlets = ">=5.1" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["pep440", "pre-commit", "pytest", "testpath"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "notebook-shim" +version = "0.2.4" +description = "A shim layer for notebook traits and config" +optional = false +python-versions = ">=3.7" +files = [ + {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, + {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, +] + +[package.dependencies] +jupyter-server = ">=1.8,<3" + +[package.extras] +test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] + +[[package]] +name = "nox" +version = "2024.3.2" +description = "Flexible test automation." +optional = false +python-versions = ">=3.7" +files = [ + {file = "nox-2024.3.2-py3-none-any.whl", hash = "sha256:e53514173ac0b98dd47585096a55572fe504fecede58ced708979184d05440be"}, + {file = "nox-2024.3.2.tar.gz", hash = "sha256:f521ae08a15adbf5e11f16cb34e8d0e6ea521e0b92868f684e91677deb974553"}, +] + +[package.dependencies] +argcomplete = ">=1.9.4,<4.0" +colorlog = ">=2.6.1,<7.0.0" +packaging = ">=20.9" +virtualenv = ">=20.14.1" + +[package.extras] +tox-to-nox = ["jinja2", "tox"] +uv = ["uv"] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "overrides" +version = "7.7.0" +description = "A decorator to automatically detect mismatch when overriding a method." +optional = false +python-versions = ">=3.6" +files = [ + {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, + {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +description = "Utilities for writing pandoc filters in python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, + {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, +] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pre-commit" +version = "3.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prometheus-client" +version = "0.20.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-json-logger" +version = "2.0.7" +description = "A python library adding a json log formatter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, + {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, +] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pywinpty" +version = "2.0.13" +description = "Pseudo terminal support for Windows from Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"}, + {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"}, + {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"}, + {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"}, + {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"}, + {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyzmq" +version = "25.1.2" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, + {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, + {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, + {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, + {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, + {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, + {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, + {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, + {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, + {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, + {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, + {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, + {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, + {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, + {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, + {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, + {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, + {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, + {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, + {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, + {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, + {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, + {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, + {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, + {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, + {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, + {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, + {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, + {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, + {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, + {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, + {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, + {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, + {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, + {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, + {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, + {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, + {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "referencing" +version = "0.34.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, + {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +description = "Pure python rfc3986 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, + {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, +] + +[[package]] +name = "rpds-py" +version = "0.18.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, + {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, + {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, + {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, + {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, + {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, + {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, + {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, + {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, + {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, + {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, + {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, + {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +description = "Send file to trash natively under Mac OS X, Windows and Linux" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, + {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, +] + +[package.extras] +nativelib = ["pyobjc-framework-Cocoa", "pywin32"] +objc = ["pyobjc-framework-Cocoa"] +win32 = ["pywin32"] + +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "terminado" +version = "0.18.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, + {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, +] + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "tinycss2" +version = "1.2.1" +description = "A tiny CSS parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, + {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, +] + +[package.dependencies] +webencodings = ">=0.4" + +[package.extras] +doc = ["sphinx", "sphinx_rtd_theme"] +test = ["flake8", "isort", "pytest"] + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "traitlets" +version = "5.14.2" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20240316" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, +] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, +] + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "webcolors" +version = "1.13" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.7" +files = [ + {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, + {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "websocket-client" +version = "1.7.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "xdoctest" +version = "1.1.3" +description = "A rewrite of the builtin doctest module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "xdoctest-1.1.3-py3-none-any.whl", hash = "sha256:9360535bd1a971ffc216d9613898cedceb81d0fd024587cc3c03c74d14c00a31"}, + {file = "xdoctest-1.1.3.tar.gz", hash = "sha256:84e76a42a11a5926ff66d9d84c616bc101821099672550481ad96549cbdd02ae"}, +] + +[package.extras] +all = ["IPython (>=7.10.0)", "IPython (>=7.23.1)", "Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "attrs (>=19.2.0)", "colorama (>=0.4.1)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=5.2.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=6.1.5)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)", "pyflakes (>=2.2.0)", "pytest (>=4.6.0)", "pytest (>=4.6.0)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "tomli (>=0.2.0)", "typing (>=3.7.4)"] +all-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)", "pyflakes (==2.2.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "tomli (==0.2.0)", "typing (==3.7.4)"] +colors = ["Pygments", "Pygments", "colorama"] +jupyter = ["IPython", "IPython", "attrs", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert", "nbconvert"] +optional = ["IPython (>=7.10.0)", "IPython (>=7.23.1)", "Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "attrs (>=19.2.0)", "colorama (>=0.4.1)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=5.2.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=6.1.5)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)", "pyflakes (>=2.2.0)", "tomli (>=0.2.0)"] +optional-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)", "pyflakes (==2.2.0)", "tomli (==0.2.0)"] +tests = ["pytest (>=4.6.0)", "pytest (>=4.6.0)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "typing (>=3.7.4)"] +tests-binary = ["cmake", "cmake", "ninja", "ninja", "pybind11", "pybind11", "scikit-build", "scikit-build"] +tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] +tests-strict = ["pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "typing (==3.7.4)"] [metadata] -content-hash = "fafb334cb038533f851c23d0b63254223abf72ce4f02987e7064b0c95566699a" -lock-version = "1.0" -python-versions = "^3.8" - -[metadata.files] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "c6c80c1e03b497a4533d3c622b77740b172fa4818921210d88759b224a3654b4" diff --git a/pyproject.toml b/pyproject.toml index 6f05059..8d26fc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,26 @@ [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" [tool.poetry] name = "intro-to-python" -version = "0.1.0.dev0" +version = "0.1.0" +package-mode = false authors = ["Alexander Hess "] description = "An intro to Python & programming for wanna-be data scientists" license = "MIT" [tool.poetry.dependencies] -python = "^3.8" +python = "^3.11" + +jupyterlab = "^4.1.0" +numpy = "^1.26.0" [tool.poetry.dev-dependencies] +# Task runners +nox = "^2024.3.2" +pre-commit = "^3.7.0" + +# Testing +xdoctest = "^1.1.0" diff --git a/static/anaconda_download.png b/static/anaconda_download.png new file mode 100644 index 0000000..037678b Binary files /dev/null and b/static/anaconda_download.png differ diff --git a/static/anaconda_navigator.png b/static/anaconda_navigator.png new file mode 100644 index 0000000..d2ba796 Binary files /dev/null and b/static/anaconda_navigator.png differ diff --git a/static/anaconda_start_menu.png b/static/anaconda_start_menu.png new file mode 100644 index 0000000..f3d5d9a Binary files /dev/null and b/static/anaconda_start_menu.png differ diff --git a/static/cli_install.png b/static/cli_install.png new file mode 100644 index 0000000..08144dc Binary files /dev/null and b/static/cli_install.png differ diff --git a/static/cli_jupyter_lab.png b/static/cli_jupyter_lab.png new file mode 100644 index 0000000..ffca31f Binary files /dev/null and b/static/cli_jupyter_lab.png differ diff --git a/static/jupyter_lab.png b/static/jupyter_lab.png new file mode 100644 index 0000000..97f8a8a Binary files /dev/null and b/static/jupyter_lab.png differ diff --git a/static/jupyter_notebook_blank.png b/static/jupyter_notebook_blank.png new file mode 100644 index 0000000..d19b8a9 Binary files /dev/null and b/static/jupyter_notebook_blank.png differ diff --git a/static/jupyter_notebook_example.png b/static/jupyter_notebook_example.png new file mode 100644 index 0000000..21a17aa Binary files /dev/null and b/static/jupyter_notebook_example.png differ diff --git a/static/link/to_gh.png b/static/link/to_gh.png new file mode 100644 index 0000000..01f1a8a Binary files /dev/null and b/static/link/to_gh.png differ diff --git a/static/link/to_hn.png b/static/link/to_hn.png new file mode 100644 index 0000000..b8a4a66 Binary files /dev/null and b/static/link/to_hn.png differ diff --git a/static/link/to_mb.png b/static/link/to_mb.png new file mode 100644 index 0000000..ae37d50 Binary files /dev/null and b/static/link/to_mb.png differ diff --git a/static/link/to_nb.png b/static/link/to_nb.png new file mode 100644 index 0000000..72a4285 Binary files /dev/null and b/static/link/to_nb.png differ diff --git a/static/link/to_so.png b/static/link/to_so.png new file mode 100644 index 0000000..10f16bf Binary files /dev/null and b/static/link/to_so.png differ diff --git a/static/link/to_wiki.png b/static/link/to_wiki.png new file mode 100644 index 0000000..39ccb8c Binary files /dev/null and b/static/link/to_wiki.png differ diff --git a/static/link/to_yt.png b/static/link/to_yt.png new file mode 100644 index 0000000..b3955f6 Binary files /dev/null and b/static/link/to_yt.png differ diff --git a/static/presentation_mode.png b/static/presentation_mode.png new file mode 100644 index 0000000..898d8e1 Binary files /dev/null and b/static/presentation_mode.png differ diff --git a/static/repo_download.png b/static/repo_download.png new file mode 100644 index 0000000..afb2193 Binary files /dev/null and b/static/repo_download.png differ