diff --git a/12_functions_advanced.ipynb b/12_functions_advanced.ipynb
index e29bd660..b4659cf4 100644
--- a/12_functions_advanced.ipynb
+++ b/12_functions_advanced.ipynb
@@ -13,19 +13,22 @@
"id": "1",
"metadata": {},
"source": [
- "# Table of Contents\n",
- "\n",
+ "## Table of Contents\n",
"- [Recap](#Recap)\n",
" - [Functions are objects](#Functions-are-objects)\n",
" - [Scopes and namespaces](#Scopes-and-namespaces)\n",
"- [Mutable objects as default values of function's parameters](#Mutable-objects-as-default-values-of-function's-parameters)\n",
+ " - [Quiz: Recap and Mutable Objects](#Quiz:-Recap-and-Mutable-Objects)\n",
"- [Lambdas](#Lambdas)\n",
" - [Lambdas and sorting](#Lambdas-and-sorting)\n",
+ " - [Exercise: Lambdas](#Exercise:-Lambdas)\n",
+ " - [Quiz: Lambdas](#Quiz:-Lambdas)\n",
"- [Closures](#Closures)\n",
" - [Modifying the free variable](#Modifying-the-free-variable)\n",
" - [Multiple instances of closures](#Multiple-instances-of-closures)\n",
" - [Closures can be tricky](#Closures-can-be-tricky)\n",
" - [Nested closures](#Nested-closures)\n",
+ " - [Quiz: Closures](#Quiz:-Closures)\n",
" - [Closures: examples](#Closures:-examples)\n",
" - [Example 1](#Example-1)\n",
" - [Example 2](#Example-2)\n",
@@ -37,27 +40,27 @@
" - [Fibonacci using `reduce`](#Fibonacci-using-reduce)\n",
" - [Example 2: memoization](#Example-2:-memoization)\n",
" - [Parametrized decorators](#Parametrized-decorators)\n",
+ " - [Quiz: Decorators](#Quiz:-Decorators)\n",
"- [Generators](#Generators)\n",
" - [Create an interable from a generator](#Create-an-interable-from-a-generator)\n",
" - [Combining generators](#Combining-generators)\n",
+ " - [Beware: Generators can only be consumed once!](#Beware:-Generators-can-only-be-consumed-once!)\n",
+ " - [Quiz: Generators](#Quiz:-Generators)\n",
"- [Exercises](#Exercises)\n",
- " - [Password checker factory](#Password-checker-factory)\n",
- " - [String range](#String-range)\n",
- " - [Read `n` lines](#Read-n-lines)\n",
- " - [Only run once](#Only-run-once)"
+ " - [Exercise 1: Password checker factory](#Exercise-1:-Password-checker-factory)\n",
+ " - [Exercise 2: String range](#Exercise-2:-String-range)\n",
+ " - [Exercise 3: Read `n` lines](#Exercise-3:-Read-n-lines)\n",
+ " - [Exercise 4: Only run once](#Exercise-4:-Only-run-once)"
]
},
{
"cell_type": "markdown",
"id": "2",
- "metadata": {},
+ "metadata": {
+ "jp-MarkdownHeadingCollapsed": true
+ },
"source": [
- "We are going to cover the following topics:\n",
- "\n",
- "- Decorators\n",
- "- Lambdas\n",
- "- Arguments and object's mutability\n",
- "- Generators"
+ "## Recap"
]
},
{
@@ -65,34 +68,24 @@
"id": "3",
"metadata": {},
"source": [
- "## Recap"
+ "Before starting our deep dive on functions, we must revise quickly two important concepts.\n",
+ "Have a look at the [Functions](./03_functions.ipynb#The-scope-of-a-function) notebook for more detail."
]
},
{
"cell_type": "markdown",
"id": "4",
"metadata": {},
- "source": [
- "Before starting our deep dive on functions, we must revise quickly two important concepts. Have a look at the [Functions](./03_functions.ipynb#The-scope-of-a-function) notebook for more detail.\n",
- "\n",
- "1. Scopes and namespaces\n",
- "2. Functions are objects"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "5",
- "metadata": {},
"source": [
"### Functions are objects"
]
},
{
"cell_type": "markdown",
- "id": "6",
+ "id": "5",
"metadata": {},
"source": [
- "As an example, suppose that we want to create a \"password checker\", that is, a function that can verify if an input password complies with some rules (e.g., minumum length, a given number of special characters). We could create a function with the following signature:\n",
+ "As an example, suppose that we want to create a \"password checker\", that is, a function that can verify if an input password complies with some rules (e.g., minimum length, a given number of special characters). We could create a function with the following signature:\n",
"\n",
"```python\n",
"def check_password(\n",
@@ -110,7 +103,7 @@
},
{
"cell_type": "markdown",
- "id": "7",
+ "id": "6",
"metadata": {},
"source": [
"We can instead define a so-called **higher-order function** (see [Functional programming](./11_functional_programming.ipynb#Higher-Order-Functions-/-Functions-as-Values)): a function that returns another function.\n",
@@ -119,7 +112,7 @@
},
{
"cell_type": "markdown",
- "id": "8",
+ "id": "7",
"metadata": {},
"source": [
"```python\n",
@@ -144,7 +137,7 @@
},
{
"cell_type": "markdown",
- "id": "9",
+ "id": "8",
"metadata": {},
"source": [
"You would first call your factory function with some password requirements:\n",
@@ -162,7 +155,7 @@
},
{
"cell_type": "markdown",
- "id": "10",
+ "id": "9",
"metadata": {},
"source": [
"### Scopes and namespaces"
@@ -170,7 +163,7 @@
},
{
"cell_type": "markdown",
- "id": "11",
+ "id": "10",
"metadata": {},
"source": [
"Python's variables are just names (i.e., labels) that we can **bind** to objects. Each variable is simply telling Python where to look in our computer's memory to retrieve some data. These bindings are **not global**: some of them exist only in specific parts of our code.\n",
@@ -180,7 +173,7 @@
},
{
"cell_type": "markdown",
- "id": "12",
+ "id": "11",
"metadata": {},
"source": [
"We always have the following scopes:\n",
@@ -194,7 +187,7 @@
},
{
"cell_type": "markdown",
- "id": "13",
+ "id": "12",
"metadata": {},
"source": [
"When Python needs to retrieve which object is referenced by a given name, it always starts from the current scope (the `local` one if we are inside a function's body). If a name binding is not found there, it searches in the scope immediately up in the hierarchy.\n",
@@ -204,7 +197,7 @@
},
{
"cell_type": "markdown",
- "id": "14",
+ "id": "13",
"metadata": {},
"source": [
"When Python encounters a function **definition** (i.e., at compile-time), it does two things:\n",
@@ -215,7 +208,7 @@
},
{
"cell_type": "markdown",
- "id": "15",
+ "id": "14",
"metadata": {},
"source": [
"Examples:\n",
@@ -241,7 +234,7 @@
},
{
"cell_type": "markdown",
- "id": "16",
+ "id": "15",
"metadata": {},
"source": [
"A function gets its local scope upon calling. Since we can have function definitions inside of other functions, there can be **nested scopes**. This is where the `nonlocal` keyword becomes useful or even needed.\n",
@@ -254,7 +247,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "17",
+ "id": "16",
"metadata": {},
"outputs": [],
"source": [
@@ -274,12 +267,13 @@
" # prints \"inner\" again, because we modified `x` from a nested scope\n",
" print(\"Inside outer_function:\", x)\n",
"\n",
+ "\n",
"outer_function()"
]
},
{
"cell_type": "markdown",
- "id": "18",
+ "id": "17",
"metadata": {},
"source": [
"Two important notes about the `nonlocal` keyword:\n",
@@ -290,7 +284,7 @@
},
{
"cell_type": "markdown",
- "id": "19",
+ "id": "18",
"metadata": {},
"source": [
"
\n",
@@ -300,7 +294,7 @@
},
{
"cell_type": "markdown",
- "id": "20",
+ "id": "19",
"metadata": {},
"source": [
"## Mutable objects as default values of function's parameters"
@@ -308,7 +302,7 @@
},
{
"cell_type": "markdown",
- "id": "21",
+ "id": "20",
"metadata": {},
"source": [
"Using mutable objects like lists or dictionaries as a function's parameters default values requires extra care, as it can lead to some unexpected or unwanted behaviors.\n",
@@ -317,7 +311,7 @@
},
{
"cell_type": "markdown",
- "id": "22",
+ "id": "21",
"metadata": {},
"source": [
"Consider what happens when Python evaluates the following code:\n",
@@ -337,19 +331,20 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "23",
+ "id": "22",
"metadata": {},
"outputs": [],
"source": [
"from datetime import datetime\n",
"\n",
+ "\n",
"def log(msg, *, dt=datetime.utcnow()):\n",
- " print(f'{dt}: {msg}')"
+ " print(f\"{dt}: {msg}\")"
]
},
{
"cell_type": "markdown",
- "id": "24",
+ "id": "23",
"metadata": {},
"source": [
"A simple logging function.\n",
@@ -359,8 +354,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "25",
- "metadata": {},
+ "id": "24",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"from time import sleep\n",
@@ -372,7 +369,7 @@
},
{
"cell_type": "markdown",
- "id": "26",
+ "id": "25",
"metadata": {},
"source": [
"Something is wrong here: we waited 5 seconds, but the timestamp of our log message **did not change**.\n",
@@ -381,8 +378,10 @@
},
{
"cell_type": "markdown",
- "id": "27",
- "metadata": {},
+ "id": "26",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"The correct pattern to avoid this unwanted behavior is setting a default value of `None`, so that the argument remains optional.\n",
"Then, inside the function's body, we can assign the correct or updated value:"
@@ -391,20 +390,22 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "28",
+ "id": "27",
"metadata": {},
"outputs": [],
"source": [
"def log(msg, *, dt=None):\n",
- " dt = dt or datetime.utcnow()\n",
- " print(f'{dt}: {msg}')"
+ " dt = dt or datetime.now(datetime.timezone.utc)\n",
+ " print(f\"{dt}: {msg}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "29",
- "metadata": {},
+ "id": "28",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"log(\"my first message\")\n",
@@ -414,7 +415,7 @@
},
{
"cell_type": "markdown",
- "id": "30",
+ "id": "29",
"metadata": {},
"source": [
"Now the output is what we expected."
@@ -422,8 +423,10 @@
},
{
"cell_type": "markdown",
- "id": "31",
- "metadata": {},
+ "id": "30",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Another problematic context is when we're dealing with **mutable objects** (e.g., lists, sets, dictionaries).\n",
"This definition includes custom classes, if we are not careful.\n",
@@ -435,7 +438,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "32",
+ "id": "31",
"metadata": {},
"outputs": [],
"source": [
@@ -446,7 +449,7 @@
},
{
"cell_type": "markdown",
- "id": "33",
+ "id": "32",
"metadata": {},
"source": [
"We now have two stores and want to add some items to them:"
@@ -455,24 +458,28 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "34",
- "metadata": {},
+ "id": "33",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"store_1 = []\n",
"store_2 = []\n",
"\n",
- "add_item('bananas', 2, 'units', store_1)\n",
- "add_item('grapes', 1, 'bunch', store_1)\n",
- "add_item('python', 1, 'medium-rare', store_2)\n",
+ "add_item(\"bananas\", 2, \"units\", store_1)\n",
+ "add_item(\"grapes\", 1, \"bunch\", store_1)\n",
+ "add_item(\"python\", 1, \"medium-rare\", store_2)\n",
"\n",
"print(store_1, store_2)"
]
},
{
"cell_type": "markdown",
- "id": "35",
- "metadata": {},
+ "id": "34",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"All good.\n",
"What if we don't supply the store list where we want to add the new item?\n",
@@ -482,7 +489,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "36",
+ "id": "35",
"metadata": {},
"outputs": [],
"source": [
@@ -494,19 +501,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "37",
+ "id": "36",
"metadata": {},
"outputs": [],
"source": [
- "store_1 = add_item('bananas', 2, 'units')\n",
- "add_item('grapes', 1, 'bunch', store_1)\n",
+ "store_1 = add_item(\"bananas\", 2, \"units\")\n",
+ "add_item(\"grapes\", 1, \"bunch\", store_1)\n",
"\n",
"print(store_1)"
]
},
{
"cell_type": "markdown",
- "id": "38",
+ "id": "37",
"metadata": {},
"source": [
"Okay, all good.\n",
@@ -516,22 +523,26 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "39",
- "metadata": {},
+ "id": "38",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
- "store_2 = add_item('milk', 1, 'gallon')\n",
+ "store_2 = add_item(\"milk\", 1, \"gallon\")\n",
"\n",
"print(store_2)"
]
},
{
"cell_type": "markdown",
- "id": "40",
- "metadata": {},
+ "id": "39",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
- "Again, not what we expected, right?\n",
- "`store_2` should be a completeley new list, while Python is still adding to the empty list used to initialize the default value of the `grocery_list` parameter.\n",
+ "That's not what we expected, right?\n",
+ "We want `store_2` to be a completely new list, while Python is still adding to the empty list used to initialize the default value of the `grocery_list` parameter.\n",
"\n",
"The solution pattern is similar to the logging function:"
]
@@ -539,7 +550,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "41",
+ "id": "40",
"metadata": {},
"outputs": [],
"source": [
@@ -553,18 +564,18 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "42",
+ "id": "41",
"metadata": {},
"outputs": [],
"source": [
- "store_1 = add_item('bananas', 2, 'units')\n",
- "add_item('grapes', 1, 'bunch', store_1)"
+ "store_1 = add_item(\"bananas\", 2, \"units\")\n",
+ "add_item(\"grapes\", 1, \"bunch\", store_1)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "43",
+ "id": "42",
"metadata": {},
"outputs": [],
"source": [
@@ -574,17 +585,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "44",
- "metadata": {},
+ "id": "43",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
- "store_2 = add_item('milk', 1, 'gallon')\n",
+ "store_2 = add_item(\"milk\", 1, \"gallon\")\n",
"store_2"
]
},
{
"cell_type": "markdown",
- "id": "45",
+ "id": "44",
"metadata": {},
"source": [
"And now everything works as we expected."
@@ -592,10 +605,12 @@
},
{
"cell_type": "markdown",
- "id": "46",
- "metadata": {},
+ "id": "45",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
- "Using mutable objects as default values usually lead to unwanted results.\n",
+ "Using mutable objects as default values usually leads to unwanted results.\n",
"But there are some cases when this is precisely what we want.\n",
"\n",
"As an example where this might be useful, consider a function to calculate the factorial:"
@@ -604,7 +619,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "47",
+ "id": "46",
"metadata": {},
"outputs": [],
"source": [
@@ -612,14 +627,14 @@
" if n < 1:\n",
" return 1\n",
" else:\n",
- " print(f'Calculating {n}!')\n",
- " return n * factorial(n-1)"
+ " print(f\"Calculating {n}!\")\n",
+ " return n * factorial(n - 1)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "48",
+ "id": "47",
"metadata": {},
"outputs": [],
"source": [
@@ -629,8 +644,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "49",
- "metadata": {},
+ "id": "48",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"factorial(6)"
@@ -638,8 +655,10 @@
},
{
"cell_type": "markdown",
- "id": "50",
- "metadata": {},
+ "id": "49",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We had to recalculate some values the second time, values that we could have saved for any subsequent call.\n",
"We will see a much better approach later on, but now consider the following `factorial` function:"
@@ -648,7 +667,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "51",
+ "id": "50",
"metadata": {},
"outputs": [],
"source": [
@@ -658,8 +677,8 @@
" elif n in cache:\n",
" return cache[n]\n",
" else:\n",
- " print(f'Calculating {n}!')\n",
- " result = n * factorial(n-1)\n",
+ " print(f\"Calculating {n}!\")\n",
+ " result = n * factorial(n - 1)\n",
" cache[n] = result\n",
" return result"
]
@@ -667,7 +686,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "52",
+ "id": "51",
"metadata": {},
"outputs": [],
"source": [
@@ -677,7 +696,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "53",
+ "id": "52",
"metadata": {},
"outputs": [],
"source": [
@@ -687,7 +706,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "54",
+ "id": "53",
"metadata": {},
"outputs": [],
"source": [
@@ -696,7 +715,7 @@
},
{
"cell_type": "markdown",
- "id": "55",
+ "id": "54",
"metadata": {},
"source": [
"Since the dictionary `cache` is initialized as an empty `dict()` **at compile-time** (when we define the function), we can update its content in any subsequent call to `factorial`.\n",
@@ -705,16 +724,36 @@
},
{
"cell_type": "markdown",
+ "id": "55",
+ "metadata": {},
+ "source": [
+ "### Quiz: Recap and Mutable Objects"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"id": "56",
"metadata": {},
+ "outputs": [],
"source": [
- "## Lambdas"
+ "import tutorial.quiz.functions_advanced as fp\n",
+ "\n",
+ "fp.FunctionsAdvancedQuiz()"
]
},
{
"cell_type": "markdown",
"id": "57",
"metadata": {},
+ "source": [
+ "## Lambdas"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "58",
+ "metadata": {},
"source": [
"We already now how to create a function: we use the `def` keyword:\n",
"\n",
@@ -726,7 +765,7 @@
},
{
"cell_type": "markdown",
- "id": "58",
+ "id": "59",
"metadata": {},
"source": [
"A function can have **parameters** and a `return` statement.\n",
@@ -735,7 +774,7 @@
},
{
"cell_type": "markdown",
- "id": "59",
+ "id": "60",
"metadata": {},
"source": [
"There's another way to define a function object: with **lambda expressions** (or lambdas).\n",
@@ -755,7 +794,7 @@
},
{
"cell_type": "markdown",
- "id": "60",
+ "id": "61",
"metadata": {},
"source": [
"Lambdas are objects, as much as functions are, so we can define a lambda and assign it to a name:"
@@ -764,17 +803,17 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "61",
+ "id": "62",
"metadata": {},
"outputs": [],
"source": [
- "f = lambda x: x ** 2"
+ "f = lambda x: x**2"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "62",
+ "id": "63",
"metadata": {},
"outputs": [],
"source": [
@@ -783,7 +822,7 @@
},
{
"cell_type": "markdown",
- "id": "63",
+ "id": "64",
"metadata": {},
"source": [
"We can also define lambdas with parameters **with a default value**:"
@@ -792,7 +831,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "64",
+ "id": "65",
"metadata": {},
"outputs": [],
"source": [
@@ -802,7 +841,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "65",
+ "id": "66",
"metadata": {},
"outputs": [],
"source": [
@@ -812,7 +851,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "66",
+ "id": "67",
"metadata": {},
"outputs": [],
"source": [
@@ -821,7 +860,7 @@
},
{
"cell_type": "markdown",
- "id": "67",
+ "id": "68",
"metadata": {},
"source": [
"Lambdas are very handy when we need something that behaves like a function, but we don't plan to use it many times. Examples:\n",
@@ -837,7 +876,7 @@
},
{
"cell_type": "markdown",
- "id": "68",
+ "id": "69",
"metadata": {},
"source": [
"Lambdas are **anonymous function objects**."
@@ -846,8 +885,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "69",
- "metadata": {},
+ "id": "70",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"type(g)"
@@ -855,16 +896,18 @@
},
{
"cell_type": "markdown",
- "id": "70",
- "metadata": {},
+ "id": "71",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
- "Since are objects, they can be passed to (or returned from) other functions:"
+ "Since lambdas are objects, they can be passed to (or returned from) other functions:"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "71",
+ "id": "72",
"metadata": {},
"outputs": [],
"source": [
@@ -875,17 +918,17 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "72",
+ "id": "73",
"metadata": {},
"outputs": [],
"source": [
- "apply_func(lambda x, y: x+y, 1, 2)"
+ "apply_func(lambda x, y: x + y, 1, 2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "73",
+ "id": "74",
"metadata": {},
"outputs": [],
"source": [
@@ -894,7 +937,7 @@
},
{
"cell_type": "markdown",
- "id": "74",
+ "id": "75",
"metadata": {},
"source": [
"The previous example is **not** the suggested way to sum values of an iterable: we should use the built-in `sum()` when appropriate:"
@@ -903,7 +946,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "75",
+ "id": "76",
"metadata": {},
"outputs": [],
"source": [
@@ -913,7 +956,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "76",
+ "id": "77",
"metadata": {},
"outputs": [],
"source": [
@@ -922,7 +965,7 @@
},
{
"cell_type": "markdown",
- "id": "77",
+ "id": "78",
"metadata": {},
"source": [
"### Lambdas and sorting"
@@ -930,17 +973,17 @@
},
{
"cell_type": "markdown",
- "id": "78",
+ "id": "79",
"metadata": {},
"source": [
- "Python has a built-in `sorted` method returns any iterable sorted according to a default ordering.\n",
+ "Python has a built-in `sorted` method which returns any iterable sorted according to a default ordering.\n",
"Sometimes you may want to (or need to) specify a different criteria for sorting."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "79",
+ "id": "80",
"metadata": {},
"outputs": [],
"source": [
@@ -950,7 +993,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "80",
+ "id": "81",
"metadata": {},
"outputs": [],
"source": [
@@ -959,7 +1002,7 @@
},
{
"cell_type": "markdown",
- "id": "81",
+ "id": "82",
"metadata": {},
"source": [
"Python's `sorted` has a keyword-only argument named `key=` that takes a function used to return the key – that is, the ordinal criteria according to which we want to sort the iterable."
@@ -968,7 +1011,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "82",
+ "id": "83",
"metadata": {},
"outputs": [],
"source": [
@@ -977,30 +1020,32 @@
},
{
"cell_type": "markdown",
- "id": "83",
+ "id": "84",
"metadata": {},
"source": [
"Let's see how we can created a \"sorted dictionary\".\n",
"Recall that a dictionary is an **unordered collection**, so it doesn't have a built-in order.\n",
"\n",
- "_(Well, that's not completely true. The most recent versions of Python store the key-value pairs in the order they are entered or supplied.\n",
+ "_(Well, that's not completely true.\n",
+ "Regarding insertion order, up until Python 3.5 dictionaries were unordered.\n",
+ "Starting from Python 3.6 dictionaries remember the order of items inserted.\n",
"The thing is: accessing a dictionary **by index** doesn't make sense.)_"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "84",
+ "id": "85",
"metadata": {},
"outputs": [],
"source": [
- "d = {'def': 300, 'abc': 200, 'ghi': 100}\n",
+ "d = {\"def\": 300, \"abc\": 200, \"ghi\": 100}\n",
"sorted(d)"
]
},
{
"cell_type": "markdown",
- "id": "85",
+ "id": "86",
"metadata": {},
"source": [
"Iterating over a dictionary is equivalent to iterate **over keys**.\n",
@@ -1010,7 +1055,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "86",
+ "id": "87",
"metadata": {},
"outputs": [],
"source": [
@@ -1019,7 +1064,7 @@
},
{
"cell_type": "markdown",
- "id": "87",
+ "id": "88",
"metadata": {},
"source": [
"But wait: now we lost our values!\n",
@@ -1029,7 +1074,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "88",
+ "id": "89",
"metadata": {},
"outputs": [],
"source": [
@@ -1038,7 +1083,7 @@
},
{
"cell_type": "markdown",
- "id": "89",
+ "id": "90",
"metadata": {},
"source": [
"Another useful application of lambdas is when Python doesn't know how to apply an ordering to some kind of data.\n",
@@ -1048,26 +1093,26 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "90",
+ "id": "91",
"metadata": {},
"outputs": [],
"source": [
- "complex = [3+3j, 1+1j, 0, 4-2j]"
+ "complex_numbers = [3 + 3j, 1 + 1j, 0, 4 - 2j]"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "91",
+ "id": "92",
"metadata": {},
"outputs": [],
"source": [
- "sorted(complex)"
+ "sorted(complex_numbers)"
]
},
{
"cell_type": "markdown",
- "id": "92",
+ "id": "93",
"metadata": {},
"source": [
"We can sort complex numbers based on their modulus:"
@@ -1076,26 +1121,34 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "93",
+ "id": "94",
"metadata": {},
"outputs": [],
"source": [
- "sorted(complex, key=lambda z: (z.real)**2 + (z.imag)**2)"
+ "sorted(complex_numbers, key=lambda z: (z.real) ** 2 + (z.imag) ** 2)"
]
},
{
"cell_type": "markdown",
- "id": "94",
+ "id": "95",
"metadata": {},
"source": [
- "
\n",
- "
Question
Can you find a way to randomize a list using sorted and lambdas?\n",
- ""
+ "#### Exercise: Lambdas"
]
},
{
"cell_type": "markdown",
- "id": "95",
+ "id": "96",
+ "metadata": {},
+ "source": [
+ "
\n",
+ "
Question
Can you find a way to randomize a list using sorted and lambdas?\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "97",
"metadata": {},
"source": [
"
\n",
@@ -1103,18 +1156,77 @@
"
"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "98",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%reload_ext tutorial.tests.testsuite"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "99",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%ipytest\n",
+ "\n",
+ "import random\n",
+ "\n",
+ "\n",
+ "def solution_randomize_list(my_list: list[int]) -> list[int]:\n",
+ " \"\"\"\n",
+ " A function that randomizes the order of the elements in a list.\n",
+ "\n",
+ " Args:\n",
+ " my_list: A list of integers\n",
+ " Returns:\n",
+ " - A new list with the elements of `my_list` in random order\n",
+ " \"\"\"\n",
+ "\n",
+ " return"
+ ]
+ },
{
"cell_type": "markdown",
- "id": "96",
+ "id": "100",
"metadata": {},
"source": [
- "## Closures"
+ "#### Quiz: Lambdas"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "101",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
+ "outputs": [],
+ "source": [
+ "import tutorial.quiz.functions_advanced as fp\n",
+ "\n",
+ "fp.FunctionsAdvancedQuizLambdas()"
]
},
{
"cell_type": "markdown",
- "id": "97",
+ "id": "102",
"metadata": {},
+ "source": [
+ "## Closures"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "103",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's consider the following code:"
]
@@ -1122,8 +1234,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "98",
- "metadata": {},
+ "id": "104",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"def outer():\n",
@@ -1134,26 +1248,30 @@
"\n",
" inner()\n",
"\n",
+ "\n",
"outer()"
]
},
{
"cell_type": "markdown",
- "id": "99",
+ "id": "105",
"metadata": {},
"source": [
"Here the `lang` variable is **non-local** to `inner()` because it's only referenced. `lang` is also called **free variable**.\n",
"\n",
"> A **free variable** is a variable referenced locally but defined in the enclosing scope.\n",
"\n",
- "Also, `lang` and `inner()` both belongs to the local scope of `outer()`. Since this bound is particularly special, it has a special name: it's called a **closure**.\n",
- "The name \"closure\" come from the function `inner()` _enclosing_ its free variable `lang`."
+ "Both `lang` and `inner()` exist within `outer()`'s local scope.\n",
+ "This relationship creates a special binding known as a **closure**.\n",
+ "The term \"closure\" reflects how the function `inner()` \"closes over\" or captures its free variable `lang`, maintaining access to it even after `outer()` has finished executing."
]
},
{
"cell_type": "markdown",
- "id": "100",
- "metadata": {},
+ "id": "106",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's make a small adjustment that will change a lot of things:"
]
@@ -1161,7 +1279,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "101",
+ "id": "107",
"metadata": {},
"outputs": [],
"source": [
@@ -1173,12 +1291,13 @@
"\n",
" return inner\n",
"\n",
+ "\n",
"outer()"
]
},
{
"cell_type": "markdown",
- "id": "102",
+ "id": "108",
"metadata": {},
"source": [
"We turned `outer()` into an higher-order function that does not return a simple function, but a closure (`inner()` + the free variable).\n",
@@ -1189,7 +1308,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "103",
+ "id": "109",
"metadata": {},
"outputs": [],
"source": [
@@ -1198,7 +1317,7 @@
},
{
"cell_type": "markdown",
- "id": "104",
+ "id": "110",
"metadata": {},
"source": [
"And then call that function as any other function:"
@@ -1207,7 +1326,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "105",
+ "id": "111",
"metadata": {},
"outputs": [],
"source": [
@@ -1216,7 +1335,7 @@
},
{
"cell_type": "markdown",
- "id": "106",
+ "id": "112",
"metadata": {},
"source": [
"But wait a second! How's that possible? 🤔\n",
@@ -1229,17 +1348,21 @@
},
{
"cell_type": "markdown",
- "id": "107",
+ "id": "113",
"metadata": {},
"source": [
- "If we look once again to the example above, we see that the name `lang` is **shared** between the local scope of `outer()` and the `print` statement inside `inner()`.\n",
- "When Python sees this, it does something different: it creates an **intermediary** object – called a _cell object_ – that only contains a memory address.\n",
- "A memory address of what? Of whatever object (i.e. data) is assigned to `lang`, the free variable."
+ "Looking at our example, we can see that the variable `lang` needs to be accessible in both the `outer()` function and inside the `inner()` function.\n",
+ "To make this possible, Python uses a special mechanism for closures: it creates an **intermediary container** called a _cell object_.\n",
+ "This cell object doesn't store the value of `lang` directly.\n",
+ "Instead, it stores a memory address (a pointer) that references where the actual string \"Python\" is stored in memory.\n",
+ "Both the \"outer\" and \"inner\" functions reference this same cell object, creating a persistent link to the data.\n",
+ "This is why when `outer()` finishes executing and its local scope would normally disappear, the inner function can still\n",
+ "access the value of `lang` - it's accessing the data through this persistent cell object reference."
]
},
{
"cell_type": "markdown",
- "id": "108",
+ "id": "114",
"metadata": {},
"source": [
""
@@ -1247,7 +1370,7 @@
},
{
"cell_type": "markdown",
- "id": "109",
+ "id": "115",
"metadata": {},
"source": [
"We can see all that by inspecting some _hidden_ attributes of `fn`:"
@@ -1256,7 +1379,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "110",
+ "id": "116",
"metadata": {},
"outputs": [],
"source": [
@@ -1266,8 +1389,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "111",
- "metadata": {},
+ "id": "117",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fn.__closure__"
@@ -1275,7 +1400,7 @@
},
{
"cell_type": "markdown",
- "id": "112",
+ "id": "118",
"metadata": {},
"source": [
"We can see now the reason why we can call `fn()` and see the string \"Python rocks!\" printed although the variable `lang` is now out of scope (it's been destroyed).\n",
@@ -1285,7 +1410,7 @@
},
{
"cell_type": "markdown",
- "id": "113",
+ "id": "119",
"metadata": {},
"source": [
"### Modifying the free variable"
@@ -1293,8 +1418,10 @@
},
{
"cell_type": "markdown",
- "id": "114",
- "metadata": {},
+ "id": "120",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We know that the `nonlocal` keyword allows us to modify variables from the **enclosing scope**. Therefore, the following closure will work as expected:"
]
@@ -1302,19 +1429,23 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "115",
- "metadata": {},
+ "id": "121",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"def counter():\n",
- " count = 0 # local variable\n",
+ " count = 0 # local variable\n",
"\n",
" def inc():\n",
" nonlocal count\n",
" count += 1\n",
" return count\n",
+ "\n",
" return inc\n",
"\n",
+ "\n",
"c = counter()\n",
"\n",
"c()"
@@ -1322,7 +1453,7 @@
},
{
"cell_type": "markdown",
- "id": "116",
+ "id": "122",
"metadata": {},
"source": [
"The `inc()` function and the `count` variable are the closure, but the `count` variable is not only accessed, but also modified."
@@ -1330,8 +1461,10 @@
},
{
"cell_type": "markdown",
- "id": "117",
- "metadata": {},
+ "id": "123",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We can also have multiple closures that reference (and modify) the same free variable:"
]
@@ -1339,7 +1472,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "118",
+ "id": "124",
"metadata": {},
"outputs": [],
"source": [
@@ -1358,13 +1491,14 @@
"\n",
" return add_1, add_2\n",
"\n",
+ "\n",
"fn1, fn2 = adders()"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "119",
+ "id": "125",
"metadata": {},
"outputs": [],
"source": [
@@ -1374,8 +1508,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "120",
- "metadata": {},
+ "id": "126",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fn2()"
@@ -1383,7 +1519,7 @@
},
{
"cell_type": "markdown",
- "id": "121",
+ "id": "127",
"metadata": {},
"source": [
"### Multiple instances of closures"
@@ -1391,8 +1527,10 @@
},
{
"cell_type": "markdown",
- "id": "122",
- "metadata": {},
+ "id": "128",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"As we saw before when talking about scopes, every time a function is **called** a new **local scope** is created.\n",
"A closure can then be created multiple times, and each time we are calling it a new **extended scope** is created.\n",
@@ -1403,20 +1541,21 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "123",
+ "id": "129",
"metadata": {},
"outputs": [],
"source": [
"def power(n):\n",
" # `n` is a local variable\n",
" def op(x):\n",
- " return x ** n\n",
+ " return x**n\n",
+ "\n",
" return op"
]
},
{
"cell_type": "markdown",
- "id": "124",
+ "id": "130",
"metadata": {},
"source": [
"`n` is our free variable, and we have a closure that contains the function `op()` and the free variable."
@@ -1425,7 +1564,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "125",
+ "id": "131",
"metadata": {},
"outputs": [],
"source": [
@@ -1436,7 +1575,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "126",
+ "id": "132",
"metadata": {},
"outputs": [],
"source": [
@@ -1446,7 +1585,7 @@
},
{
"cell_type": "markdown",
- "id": "127",
+ "id": "133",
"metadata": {},
"source": [
"We can verify that the two closures are completely different even though they were created from the same `power()`:"
@@ -1455,7 +1594,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "128",
+ "id": "134",
"metadata": {},
"outputs": [],
"source": [
@@ -1465,8 +1604,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "129",
- "metadata": {},
+ "id": "135",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"cube.__closure__"
@@ -1474,7 +1615,7 @@
},
{
"cell_type": "markdown",
- "id": "130",
+ "id": "136",
"metadata": {},
"source": [
"As we expected, the free variable `n` (of type `int`) is referenced by the cell objects of both closures.\n",
@@ -1483,7 +1624,7 @@
},
{
"cell_type": "markdown",
- "id": "131",
+ "id": "137",
"metadata": {},
"source": [
"### Closures can be tricky"
@@ -1491,7 +1632,7 @@
},
{
"cell_type": "markdown",
- "id": "132",
+ "id": "138",
"metadata": {},
"source": [
"One important aspect of closures can be the source of nasty bugs if we don't understand it well.\n",
@@ -1501,8 +1642,10 @@
},
{
"cell_type": "markdown",
- "id": "133",
- "metadata": {},
+ "id": "139",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's say we want to create a function to sum different factors to an arbitrary number.\n",
"We could do:"
@@ -1511,20 +1654,21 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "134",
+ "id": "140",
"metadata": {},
"outputs": [],
"source": [
"def adder(n):\n",
" def op(x):\n",
" return n + x\n",
+ "\n",
" return op"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "135",
+ "id": "141",
"metadata": {},
"outputs": [],
"source": [
@@ -1535,7 +1679,7 @@
},
{
"cell_type": "markdown",
- "id": "136",
+ "id": "142",
"metadata": {},
"source": [
"This works as expected:"
@@ -1544,8 +1688,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "137",
- "metadata": {},
+ "id": "143",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"add_1(10), add_2(10), add_3(10)"
@@ -1553,8 +1699,10 @@
},
{
"cell_type": "markdown",
- "id": "138",
- "metadata": {},
+ "id": "144",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Now we have the following idea to improve our code:"
]
@@ -1562,15 +1710,17 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "139",
+ "id": "145",
"metadata": {},
"outputs": [],
"source": [
"def fancy_adders():\n",
" adders = []\n",
" for n in range(1, 5):\n",
+ "\n",
" def op(x):\n",
" return x + n\n",
+ "\n",
" adders.append(op)\n",
" return adders"
]
@@ -1578,7 +1728,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "140",
+ "id": "146",
"metadata": {},
"outputs": [],
"source": [
@@ -1587,7 +1737,7 @@
},
{
"cell_type": "markdown",
- "id": "141",
+ "id": "147",
"metadata": {},
"source": [
"We have now 4 functions in the `adders` list:"
@@ -1596,7 +1746,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "142",
+ "id": "148",
"metadata": {},
"outputs": [],
"source": [
@@ -1605,7 +1755,7 @@
},
{
"cell_type": "markdown",
- "id": "143",
+ "id": "149",
"metadata": {},
"source": [
"Let's see what happens when we call them:"
@@ -1614,7 +1764,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "144",
+ "id": "150",
"metadata": {},
"outputs": [],
"source": [
@@ -1623,7 +1773,7 @@
},
{
"cell_type": "markdown",
- "id": "145",
+ "id": "151",
"metadata": {},
"source": [
"Wait, why?! It seems that we picked up the **same value** of the free variable.\n",
@@ -1633,7 +1783,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "146",
+ "id": "152",
"metadata": {},
"outputs": [],
"source": [
@@ -1642,7 +1792,7 @@
},
{
"cell_type": "markdown",
- "id": "147",
+ "id": "153",
"metadata": {},
"source": [
"And we can indeed verify that its value referenced by each closure is exactly the same:"
@@ -1651,7 +1801,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "148",
+ "id": "154",
"metadata": {},
"outputs": [],
"source": [
@@ -1660,7 +1810,7 @@
},
{
"cell_type": "markdown",
- "id": "149",
+ "id": "155",
"metadata": {},
"source": [
"Which value? The last iteration of our loop, `n=4`.\n",
@@ -1670,8 +1820,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "150",
- "metadata": {},
+ "id": "156",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"hex(id(4))"
@@ -1679,7 +1831,7 @@
},
{
"cell_type": "markdown",
- "id": "151",
+ "id": "157",
"metadata": {},
"source": [
"The key to understand this behavior is remembering that **closures captures _variables_ and not _values_**.\n",
@@ -1691,8 +1843,10 @@
},
{
"cell_type": "markdown",
- "id": "152",
- "metadata": {},
+ "id": "158",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"If we wanted to fix the example as we intended, we need to capture the **current** value of `n` when defining the closure:"
]
@@ -1700,15 +1854,17 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "153",
+ "id": "159",
"metadata": {},
"outputs": [],
"source": [
"def truly_fancy_adders():\n",
" adders = []\n",
" for n in range(1, 5):\n",
+ "\n",
" def op(x, n=n): # Capture the current value of n as a default argument\n",
" return x + n\n",
+ "\n",
" adders.append(op)\n",
" return adders"
]
@@ -1716,7 +1872,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "154",
+ "id": "160",
"metadata": {},
"outputs": [],
"source": [
@@ -1726,7 +1882,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "155",
+ "id": "161",
"metadata": {},
"outputs": [],
"source": [
@@ -1735,7 +1891,7 @@
},
{
"cell_type": "markdown",
- "id": "156",
+ "id": "162",
"metadata": {},
"source": [
"Now, let's inspect our correct closures:"
@@ -1744,7 +1900,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "157",
+ "id": "163",
"metadata": {},
"outputs": [],
"source": [
@@ -1753,7 +1909,7 @@
},
{
"cell_type": "markdown",
- "id": "158",
+ "id": "164",
"metadata": {},
"source": [
"Hey, why `None`? Let's check our free variable:"
@@ -1762,7 +1918,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "159",
+ "id": "165",
"metadata": {},
"outputs": [],
"source": [
@@ -1771,7 +1927,7 @@
},
{
"cell_type": "markdown",
- "id": "160",
+ "id": "166",
"metadata": {},
"source": [
"Nothing yet?\n",
@@ -1780,7 +1936,7 @@
},
{
"cell_type": "markdown",
- "id": "161",
+ "id": "167",
"metadata": {},
"source": [
"
\n",
@@ -1791,17 +1947,20 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "162",
- "metadata": {},
+ "id": "168",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"from tutorial.quiz.functions_advanced import tricky_closures as answer\n",
+ "\n",
"answer"
]
},
{
"cell_type": "markdown",
- "id": "163",
+ "id": "169",
"metadata": {},
"source": [
"### Nested closures"
@@ -1809,8 +1968,10 @@
},
{
"cell_type": "markdown",
- "id": "164",
- "metadata": {},
+ "id": "170",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We can also nest closures, as much as we can nest functions:"
]
@@ -1818,26 +1979,29 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "165",
+ "id": "171",
"metadata": {},
"outputs": [],
"source": [
"def incrementer(n):\n",
" def inner(start):\n",
" current = start\n",
+ "\n",
" def inc():\n",
" a = 10 # local variable, NOT a free variable\n",
" nonlocal current\n",
" current += n\n",
" return current\n",
+ "\n",
" return inc\n",
+ "\n",
" return inner"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "166",
+ "id": "172",
"metadata": {},
"outputs": [],
"source": [
@@ -1847,7 +2011,7 @@
},
{
"cell_type": "markdown",
- "id": "167",
+ "id": "173",
"metadata": {},
"source": [
"We create an incrementer function with the default increment, `n=2`, starting from `100`:"
@@ -1856,7 +2020,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "168",
+ "id": "174",
"metadata": {},
"outputs": [],
"source": [
@@ -1866,7 +2030,7 @@
},
{
"cell_type": "markdown",
- "id": "169",
+ "id": "175",
"metadata": {},
"source": [
"We can also create another _custom_ incrementer with a different increment value:"
@@ -1875,7 +2039,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "170",
+ "id": "176",
"metadata": {},
"outputs": [],
"source": [
@@ -1885,7 +2049,29 @@
},
{
"cell_type": "markdown",
- "id": "171",
+ "id": "177",
+ "metadata": {},
+ "source": [
+ "### Quiz: Closures"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "178",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
+ "outputs": [],
+ "source": [
+ "import tutorial.quiz.functions_advanced as fp\n",
+ "\n",
+ "fp.FunctionsAdvancedQuizClosures()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "179",
"metadata": {},
"source": [
"### Closures: examples"
@@ -1893,7 +2079,7 @@
},
{
"cell_type": "markdown",
- "id": "172",
+ "id": "180",
"metadata": {},
"source": [
"#### Example 1"
@@ -1901,8 +2087,10 @@
},
{
"cell_type": "markdown",
- "id": "173",
- "metadata": {},
+ "id": "181",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's see a practical example of using closures.\n",
"We're going to see how closures can replace classes and be more straightforward for simple tasks.\n",
@@ -1914,7 +2102,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "174",
+ "id": "182",
"metadata": {},
"outputs": [],
"source": [
@@ -1926,13 +2114,14 @@
" self.numbers.append(number)\n",
" return sum(self.numbers) / len(self.numbers)\n",
"\n",
+ "\n",
"a = Averager()"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "175",
+ "id": "183",
"metadata": {},
"outputs": [],
"source": [
@@ -1942,7 +2131,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "176",
+ "id": "184",
"metadata": {},
"outputs": [],
"source": [
@@ -1952,8 +2141,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "177",
- "metadata": {},
+ "id": "185",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"a.add(30)"
@@ -1961,8 +2152,10 @@
},
{
"cell_type": "markdown",
- "id": "178",
- "metadata": {},
+ "id": "186",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"How can we rewrite the our class as a closure?\n",
"The free variable will be the list `numbers`."
@@ -1971,24 +2164,27 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "179",
+ "id": "187",
"metadata": {},
"outputs": [],
"source": [
"def averager():\n",
" numbers = []\n",
+ "\n",
" def add(number):\n",
" numbers.append(number)\n",
" return sum(numbers) / len(numbers)\n",
+ "\n",
" return add\n",
"\n",
+ "\n",
"a = averager()"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "180",
+ "id": "188",
"metadata": {},
"outputs": [],
"source": [
@@ -1998,7 +2194,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "181",
+ "id": "189",
"metadata": {},
"outputs": [],
"source": [
@@ -2008,7 +2204,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "182",
+ "id": "190",
"metadata": {},
"outputs": [],
"source": [
@@ -2018,8 +2214,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "183",
- "metadata": {},
+ "id": "191",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"a(20)"
@@ -2027,8 +2225,10 @@
},
{
"cell_type": "markdown",
- "id": "184",
- "metadata": {},
+ "id": "192",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We can make it better: instead of accumulating all the numbers in a list, we only need to keep a running total and count."
]
@@ -2036,29 +2236,32 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "185",
+ "id": "193",
"metadata": {},
"outputs": [],
"source": [
"def averager():\n",
" total = 0\n",
" count = 0\n",
+ "\n",
" def add(number):\n",
" nonlocal total\n",
" nonlocal count\n",
- " \n",
+ "\n",
" total += number\n",
" count += 1\n",
" return total / count\n",
+ "\n",
" return add\n",
"\n",
+ "\n",
"a = averager()"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "186",
+ "id": "194",
"metadata": {},
"outputs": [],
"source": [
@@ -2068,7 +2271,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "187",
+ "id": "195",
"metadata": {},
"outputs": [],
"source": [
@@ -2078,8 +2281,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "188",
- "metadata": {},
+ "id": "196",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"a(30)"
@@ -2087,7 +2292,7 @@
},
{
"cell_type": "markdown",
- "id": "189",
+ "id": "197",
"metadata": {},
"source": [
"#### Example 2"
@@ -2095,8 +2300,10 @@
},
{
"cell_type": "markdown",
- "id": "190",
- "metadata": {},
+ "id": "198",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We want to create a counter function, that is, a function that increments a variable every time it's called:"
]
@@ -2104,7 +2311,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "191",
+ "id": "199",
"metadata": {},
"outputs": [],
"source": [
@@ -2113,13 +2320,14 @@
" nonlocal initial_value\n",
" initial_value += increment\n",
" return initial_value\n",
+ "\n",
" return inc"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "192",
+ "id": "200",
"metadata": {},
"outputs": [],
"source": [
@@ -2130,7 +2338,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "193",
+ "id": "201",
"metadata": {},
"outputs": [],
"source": [
@@ -2140,7 +2348,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "194",
+ "id": "202",
"metadata": {},
"outputs": [],
"source": [
@@ -2150,7 +2358,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "195",
+ "id": "203",
"metadata": {},
"outputs": [],
"source": [
@@ -2160,8 +2368,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "196",
- "metadata": {},
+ "id": "204",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"c100(10)"
@@ -2169,7 +2379,7 @@
},
{
"cell_type": "markdown",
- "id": "197",
+ "id": "205",
"metadata": {},
"source": [
"As you can see, each closure maintains a reference to the `initial_value` variable that was created when the `counter()` function was **called**.\n",
@@ -2179,8 +2389,10 @@
},
{
"cell_type": "markdown",
- "id": "198",
- "metadata": {},
+ "id": "206",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's extend this example to a **function counter**: a counter that keeps track how many times a function is run."
]
@@ -2188,8 +2400,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "199",
- "metadata": {},
+ "id": "207",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"def fcounter(function):\n",
@@ -2206,8 +2420,10 @@
},
{
"cell_type": "markdown",
- "id": "200",
- "metadata": {},
+ "id": "208",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's define a function we want to keep track of:"
]
@@ -2215,20 +2431,21 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "201",
+ "id": "209",
"metadata": {},
"outputs": [],
"source": [
"def add(a, b):\n",
" return a + b\n",
"\n",
+ "\n",
"counter_add = fcounter(add)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "202",
+ "id": "210",
"metadata": {},
"outputs": [],
"source": [
@@ -2237,7 +2454,7 @@
},
{
"cell_type": "markdown",
- "id": "203",
+ "id": "211",
"metadata": {},
"source": [
"We have **two** free variables, one of which is a function (remember: functions are objects)."
@@ -2246,7 +2463,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "204",
+ "id": "212",
"metadata": {},
"outputs": [],
"source": [
@@ -2256,8 +2473,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "205",
- "metadata": {},
+ "id": "213",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"counter_add(2, 3)"
@@ -2265,7 +2484,7 @@
},
{
"cell_type": "markdown",
- "id": "206",
+ "id": "214",
"metadata": {},
"source": [
"## Decorators"
@@ -2273,8 +2492,10 @@
},
{
"cell_type": "markdown",
- "id": "207",
- "metadata": {},
+ "id": "215",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Now that we know what closures are and what we can do with them, understanding what is a decorator is a (small) step away.\n",
"\n",
@@ -2284,8 +2505,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "208",
- "metadata": {},
+ "id": "216",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"def fcounter(function):\n",
@@ -2294,7 +2517,7 @@
" def inner(*args, **kwargs):\n",
" nonlocal count\n",
" count += 1\n",
- " print(f\"Function '{function.__name__}' has beel called {count} times.\")\n",
+ " print(f\"Function '{function.__name__}' has been called {count} times.\")\n",
" return function(*args, **kwargs)\n",
"\n",
" return inner"
@@ -2302,8 +2525,10 @@
},
{
"cell_type": "markdown",
- "id": "209",
- "metadata": {},
+ "id": "217",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"And then create a function we want to keep track:"
]
@@ -2311,7 +2536,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "210",
+ "id": "218",
"metadata": {},
"outputs": [],
"source": [
@@ -2319,7 +2544,7 @@
" product = 1\n",
" if n == 0:\n",
" return 1\n",
- " for i in range(2, n+1):\n",
+ " for i in range(2, n + 1):\n",
" product *= i\n",
" return product"
]
@@ -2327,7 +2552,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "211",
+ "id": "219",
"metadata": {},
"outputs": [],
"source": [
@@ -2337,7 +2562,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "212",
+ "id": "220",
"metadata": {},
"outputs": [],
"source": [
@@ -2347,7 +2572,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "213",
+ "id": "221",
"metadata": {},
"outputs": [],
"source": [
@@ -2356,7 +2581,7 @@
},
{
"cell_type": "markdown",
- "id": "214",
+ "id": "222",
"metadata": {},
"source": [
"Of course, `counter_fact` is an arbitrary name.\n",
@@ -2366,7 +2591,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "215",
+ "id": "223",
"metadata": {},
"outputs": [],
"source": [
@@ -2376,8 +2601,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "216",
- "metadata": {},
+ "id": "224",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"factorial(10)"
@@ -2385,8 +2612,10 @@
},
{
"cell_type": "markdown",
- "id": "217",
- "metadata": {},
+ "id": "225",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"This way of defining a function, creating a closure that \"wraps\" a function object, and then **renaming** the initial function is so common in Python that gained a special syntax:"
]
@@ -2394,7 +2623,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "218",
+ "id": "226",
"metadata": {},
"outputs": [],
"source": [
@@ -2407,7 +2636,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "219",
+ "id": "227",
"metadata": {},
"outputs": [],
"source": [
@@ -2416,7 +2645,7 @@
},
{
"cell_type": "markdown",
- "id": "220",
+ "id": "228",
"metadata": {},
"source": [
"The function `fcounter` is called a **decorator**, because it's placed **before** the function definition line with the special symbol `@`."
@@ -2424,7 +2653,7 @@
},
{
"cell_type": "markdown",
- "id": "221",
+ "id": "229",
"metadata": {},
"source": [
"There's one problem, though.\n",
@@ -2434,7 +2663,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "222",
+ "id": "230",
"metadata": {},
"outputs": [],
"source": [
@@ -2444,7 +2673,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "223",
+ "id": "231",
"metadata": {},
"outputs": [],
"source": [
@@ -2453,7 +2682,7 @@
},
{
"cell_type": "markdown",
- "id": "224",
+ "id": "232",
"metadata": {},
"source": [
"As you can see, we've also lost our docstring and type hints!\n",
@@ -2463,7 +2692,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "225",
+ "id": "233",
"metadata": {},
"outputs": [],
"source": [
@@ -2475,8 +2704,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "226",
- "metadata": {},
+ "id": "234",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"print(inspect.signature(mult))"
@@ -2484,8 +2715,10 @@
},
{
"cell_type": "markdown",
- "id": "227",
- "metadata": {},
+ "id": "235",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We _could_ put back that information, but it might not be straighforward:"
]
@@ -2493,7 +2726,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "228",
+ "id": "236",
"metadata": {},
"outputs": [],
"source": [
@@ -2515,7 +2748,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "229",
+ "id": "237",
"metadata": {},
"outputs": [],
"source": [
@@ -2528,7 +2761,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "230",
+ "id": "238",
"metadata": {},
"outputs": [],
"source": [
@@ -2538,7 +2771,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "231",
+ "id": "239",
"metadata": {},
"outputs": [],
"source": [
@@ -2547,7 +2780,7 @@
},
{
"cell_type": "markdown",
- "id": "232",
+ "id": "240",
"metadata": {},
"source": [
"Okay, at least our docstring and function's name are back.\n",
@@ -2557,7 +2790,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "233",
+ "id": "241",
"metadata": {},
"outputs": [],
"source": [
@@ -2566,7 +2799,7 @@
},
{
"cell_type": "markdown",
- "id": "234",
+ "id": "242",
"metadata": {},
"source": [
"Unfortunately, they stil belong to the `inner` function.\n",
@@ -2578,12 +2811,13 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "235",
+ "id": "243",
"metadata": {},
"outputs": [],
"source": [
"from functools import wraps\n",
"\n",
+ "\n",
"def fcounter(function):\n",
" count = 0\n",
"\n",
@@ -2600,7 +2834,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "236",
+ "id": "244",
"metadata": {},
"outputs": [],
"source": [
@@ -2613,7 +2847,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "237",
+ "id": "245",
"metadata": {},
"outputs": [],
"source": [
@@ -2623,7 +2857,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "238",
+ "id": "246",
"metadata": {},
"outputs": [],
"source": [
@@ -2633,7 +2867,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "239",
+ "id": "247",
"metadata": {},
"outputs": [],
"source": [
@@ -2642,7 +2876,7 @@
},
{
"cell_type": "markdown",
- "id": "240",
+ "id": "248",
"metadata": {},
"source": [
"And now everything is back to normal."
@@ -2650,7 +2884,7 @@
},
{
"cell_type": "markdown",
- "id": "241",
+ "id": "249",
"metadata": {},
"source": [
"### Decorators: examples"
@@ -2658,7 +2892,7 @@
},
{
"cell_type": "markdown",
- "id": "242",
+ "id": "250",
"metadata": {},
"source": [
"#### Example 1: timer"
@@ -2666,7 +2900,7 @@
},
{
"cell_type": "markdown",
- "id": "243",
+ "id": "251",
"metadata": {},
"source": [
"This is classic example of using decorators: creating a timer for a generic function."
@@ -2675,39 +2909,41 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "244",
- "metadata": {},
+ "id": "252",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
- "from time import perf_counter\n",
"from functools import wraps\n",
+ "from time import perf_counter\n",
"\n",
- "def timed(function):\n",
"\n",
+ "def timed(function):\n",
" @wraps(function)\n",
" def inner(*args, **kwargs):\n",
" start = perf_counter()\n",
- " \n",
+ "\n",
" result = function(*args, **kwargs)\n",
- " \n",
+ "\n",
" end = perf_counter()\n",
" elapsed = end - start\n",
- " \n",
+ "\n",
" args_ = [str(a) for a in args]\n",
- " kwargs_ = [f'{k}={v}' for (k, v) in kwargs.items()]\n",
+ " kwargs_ = [f\"{k}={v}\" for (k, v) in kwargs.items()]\n",
" all_args = args_ + kwargs_\n",
- " args_str = ','.join(all_args)\n",
- " \n",
- " print(f'{function.__name__}({args_str}) took {elapsed:.6f}s to run.')\n",
+ " args_str = \",\".join(all_args)\n",
+ "\n",
+ " print(f\"{function.__name__}({args_str}) took {elapsed:.6f}s to run.\")\n",
"\n",
" return result\n",
- " \n",
+ "\n",
" return inner"
]
},
{
"cell_type": "markdown",
- "id": "245",
+ "id": "253",
"metadata": {},
"source": [
"Let's test it with a function to calculate the n-th Fibonacci number: `1, 1, 2, 3, 5, 8, 11, ...`\n",
@@ -2723,8 +2959,10 @@
},
{
"cell_type": "markdown",
- "id": "246",
- "metadata": {},
+ "id": "254",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"##### Fibonacci with recursion"
]
@@ -2732,7 +2970,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "247",
+ "id": "255",
"metadata": {},
"outputs": [],
"source": [
@@ -2745,7 +2983,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "248",
+ "id": "256",
"metadata": {},
"outputs": [],
"source": [
@@ -2755,7 +2993,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "249",
+ "id": "257",
"metadata": {},
"outputs": [],
"source": [
@@ -2765,7 +3003,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "250",
+ "id": "258",
"metadata": {},
"outputs": [],
"source": [
@@ -2777,7 +3015,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "251",
+ "id": "259",
"metadata": {},
"outputs": [],
"source": [
@@ -2787,7 +3025,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "252",
+ "id": "260",
"metadata": {},
"outputs": [],
"source": [
@@ -2797,8 +3035,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "253",
- "metadata": {},
+ "id": "261",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fib_recursive(40)"
@@ -2806,7 +3046,7 @@
},
{
"cell_type": "markdown",
- "id": "254",
+ "id": "262",
"metadata": {},
"source": [
"Sounds a bit long, doesn't it?\n",
@@ -2816,8 +3056,10 @@
},
{
"cell_type": "markdown",
- "id": "255",
- "metadata": {},
+ "id": "263",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"##### Fibonacci with a simple loop"
]
@@ -2825,7 +3067,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "256",
+ "id": "264",
"metadata": {},
"outputs": [],
"source": [
@@ -2833,17 +3075,17 @@
"def fib_loop(n):\n",
" fib_1 = 1\n",
" fib_2 = 1\n",
- " \n",
+ "\n",
" for i in range(3, n + 1):\n",
" fib_1, fib_2 = fib_2, fib_1 + fib_2\n",
- " \n",
- " return fib_2\n"
+ "\n",
+ " return fib_2"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "257",
+ "id": "265",
"metadata": {},
"outputs": [],
"source": [
@@ -2853,7 +3095,7 @@
},
{
"cell_type": "markdown",
- "id": "258",
+ "id": "266",
"metadata": {},
"source": [
"Incredibly more efficient!\n",
@@ -2862,7 +3104,7 @@
},
{
"cell_type": "markdown",
- "id": "259",
+ "id": "267",
"metadata": {},
"source": [
"##### Fibonacci using `reduce`"
@@ -2870,7 +3112,7 @@
},
{
"cell_type": "markdown",
- "id": "260",
+ "id": "268",
"metadata": {},
"source": [
"First, a quick refresher:"
@@ -2879,7 +3121,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "261",
+ "id": "269",
"metadata": {},
"outputs": [],
"source": [
@@ -2889,8 +3131,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "262",
- "metadata": {},
+ "id": "270",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"reduce(lambda x, y: x + y, (1, 2, 3, 4, 5))"
@@ -2898,17 +3142,23 @@
},
{
"cell_type": "markdown",
- "id": "263",
+ "id": "271",
"metadata": {},
"source": [
"It's just the progressive sum of pairs of numbers.\n",
- "`reduce` applies an operation (1st argument) to pairs of element in an interable (2nd argument)."
+ "`reduce` applies an operation (1st argument) to pairs of element in an interable (2nd argument).\n",
+ "See also the relevant section in [Functional programming](./11_functional_programming.ipynb#Reducing).\n",
+ "Remember that `reduce` can also have a third optional argument, called `initial`.\n",
+ "If `initial` is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty.\n",
+ "If `initial` is not given and iterable contains only one item, the first item is returned."
]
},
{
"cell_type": "markdown",
- "id": "264",
- "metadata": {},
+ "id": "272",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"To calculate the Fibonacci sequence with `reduce`:\n",
"\n",
@@ -2917,7 +3167,7 @@
"(1, 0) --> (1, 1)\n",
"\n",
"n=2:\n",
- "(1, 0) --> (1, 1) --> (1 + 1, 1) = (2, 1) : result = 2 \n",
+ "(1, 0) --> (1, 1) --> (1 + 1, 1) = (2, 1) : result = 2\n",
"\n",
"n=3\n",
"(1, 0) --> (1, 1) --> (2, 1) --> (2+1, 2) = (3, 2) : result = 3\n",
@@ -2940,7 +3190,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "265",
+ "id": "273",
"metadata": {},
"outputs": [],
"source": [
@@ -2954,7 +3204,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "266",
+ "id": "274",
"metadata": {},
"outputs": [],
"source": [
@@ -2964,7 +3214,7 @@
},
{
"cell_type": "markdown",
- "id": "267",
+ "id": "275",
"metadata": {},
"source": [
"If we compare the three methods:"
@@ -2973,8 +3223,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "268",
- "metadata": {},
+ "id": "276",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fib_recursive(35)\n",
@@ -2984,7 +3236,7 @@
},
{
"cell_type": "markdown",
- "id": "269",
+ "id": "277",
"metadata": {},
"source": [
"Although the recursive method is the __easiest__ to understand, it's also the slowest because it's written inefficiently.\n",
@@ -2993,7 +3245,7 @@
},
{
"cell_type": "markdown",
- "id": "270",
+ "id": "278",
"metadata": {},
"source": [
"#### Example 2: memoization"
@@ -3001,7 +3253,7 @@
},
{
"cell_type": "markdown",
- "id": "271",
+ "id": "279",
"metadata": {},
"source": [
"The previous example showed one task that a decorator can accomplish pretty well: adding some feature to a predefined function.\n",
@@ -3010,8 +3262,10 @@
},
{
"cell_type": "markdown",
- "id": "272",
- "metadata": {},
+ "id": "280",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Remember the Fibonacci sequence example.\n",
"We discovered that the recursive approach is by far the most intuitive, yet it's tremendously inefficient because a number gets calculated multiple times."
@@ -3020,20 +3274,22 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "273",
+ "id": "281",
"metadata": {},
"outputs": [],
"source": [
"def fib(n):\n",
- " print (f'Calculating fib({n})')\n",
- " return 1 if n < 3 else fib(n-1) + fib(n-2)"
+ " print(f\"Calculating fib({n})\")\n",
+ " return 1 if n < 3 else fib(n - 1) + fib(n - 2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "274",
- "metadata": {},
+ "id": "282",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fib(5)"
@@ -3041,7 +3297,7 @@
},
{
"cell_type": "markdown",
- "id": "275",
+ "id": "283",
"metadata": {},
"source": [
"You can see that `fib(2)` is calculated **three times**.\n",
@@ -3051,7 +3307,7 @@
},
{
"cell_type": "markdown",
- "id": "276",
+ "id": "284",
"metadata": {},
"source": [
"We'll see how we can improve this approach using a decorator and a caching mechanism for previously calculated numbers.\n",
@@ -3060,8 +3316,10 @@
},
{
"cell_type": "markdown",
- "id": "277",
- "metadata": {},
+ "id": "285",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"For the sake of comparison, let's first approach this problem with a simple class:"
]
@@ -3069,25 +3327,25 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "278",
+ "id": "286",
"metadata": {},
"outputs": [],
"source": [
"class Fib:\n",
" def __init__(self):\n",
- " self.cache = {1: 1, 2: 1} # initial values already known\n",
- " \n",
+ " self.cache = {1: 1, 2: 1} # initial values already known\n",
+ "\n",
" def fib(self, n):\n",
" if n not in self.cache:\n",
- " print(f'Calculating fib({n})')\n",
- " self.cache[n] = self.fib(n-1) + self.fib(n-2)\n",
+ " print(f\"Calculating fib({n})\")\n",
+ " self.cache[n] = self.fib(n - 1) + self.fib(n - 2)\n",
" return self.cache[n]"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "279",
+ "id": "287",
"metadata": {},
"outputs": [],
"source": [
@@ -3098,8 +3356,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "280",
- "metadata": {},
+ "id": "288",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"f.fib(12)"
@@ -3107,8 +3367,10 @@
},
{
"cell_type": "markdown",
- "id": "281",
- "metadata": {},
+ "id": "289",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"You can see that numbers $\\leq 10$ are **not** recalculated, but are fetched from the cache.\n",
"\n",
@@ -3118,38 +3380,38 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "282",
+ "id": "290",
"metadata": {},
"outputs": [],
"source": [
"def fib():\n",
" # `cache` is our free variable\n",
" cache = {1: 1, 2: 2}\n",
- " \n",
+ "\n",
" def calc_fib(n):\n",
" if n not in cache:\n",
- " print(f'Calculating fib({n})')\n",
- " cache[n] = calc_fib(n-1) + calc_fib(n-2)\n",
+ " print(f\"Calculating fib({n})\")\n",
+ " cache[n] = calc_fib(n - 1) + calc_fib(n - 2)\n",
" return cache[n]\n",
- " \n",
+ "\n",
" return calc_fib"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "283",
+ "id": "291",
"metadata": {},
"outputs": [],
"source": [
- "f = fib() # create our closure\n",
- "f(10) # call it"
+ "f = fib() # create our closure\n",
+ "f(10) # call it"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "284",
+ "id": "292",
"metadata": {},
"outputs": [],
"source": [
@@ -3158,7 +3420,7 @@
},
{
"cell_type": "markdown",
- "id": "285",
+ "id": "293",
"metadata": {},
"source": [
"Once again, cached valued are just returned and not recalculated."
@@ -3166,7 +3428,7 @@
},
{
"cell_type": "markdown",
- "id": "286",
+ "id": "294",
"metadata": {},
"source": [
"How can we implement this as a decorator?"
@@ -3175,41 +3437,42 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "287",
+ "id": "295",
"metadata": {},
"outputs": [],
"source": [
"from functools import wraps\n",
"\n",
+ "\n",
"def memoize_fib(fn):\n",
" cache = {}\n",
- " \n",
+ "\n",
" @wraps(fn)\n",
" def inner(n):\n",
" if n not in cache:\n",
" cache[n] = fn(n)\n",
" return cache[n]\n",
- " \n",
+ "\n",
" return inner"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "288",
+ "id": "296",
"metadata": {},
"outputs": [],
"source": [
"@memoize_fib\n",
"def fib(n):\n",
- " print (f'Calculating fib({n})')\n",
- " return 1 if n < 3 else fib(n-1) + fib(n-2)"
+ " print(f\"Calculating fib({n})\")\n",
+ " return 1 if n < 3 else fib(n - 1) + fib(n - 2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "289",
+ "id": "297",
"metadata": {},
"outputs": [],
"source": [
@@ -3219,7 +3482,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "290",
+ "id": "298",
"metadata": {},
"outputs": [],
"source": [
@@ -3229,8 +3492,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "291",
- "metadata": {},
+ "id": "299",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fib(6)"
@@ -3238,7 +3503,7 @@
},
{
"cell_type": "markdown",
- "id": "292",
+ "id": "300",
"metadata": {},
"source": [
"`fib(6)` was literally instantaneous because we already had it in the cache."
@@ -3246,8 +3511,10 @@
},
{
"cell_type": "markdown",
- "id": "293",
- "metadata": {},
+ "id": "301",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"How to create a generic decorator that caches the return values of **any** function?\n",
"We know how to do it:"
@@ -3256,26 +3523,30 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "294",
- "metadata": {},
+ "id": "302",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"def memoize(fn):\n",
" cache = {}\n",
- " \n",
+ "\n",
" @wraps(fn)\n",
" def inner(*args):\n",
" if args not in cache:\n",
" cache[args] = fn(*args)\n",
" return cache[args]\n",
- " \n",
+ "\n",
" return inner"
]
},
{
"cell_type": "markdown",
- "id": "295",
- "metadata": {},
+ "id": "303",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"And we can now give any function a cache to store previously calculated results:"
]
@@ -3283,20 +3554,20 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "296",
+ "id": "304",
"metadata": {},
"outputs": [],
"source": [
"@memoize\n",
"def fact(n):\n",
- " print(f'Calculating {n}!')\n",
- " return 1 if n < 2 else n * fact(n-1)"
+ " print(f\"Calculating {n}!\")\n",
+ " return 1 if n < 2 else n * fact(n - 1)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "297",
+ "id": "305",
"metadata": {},
"outputs": [],
"source": [
@@ -3306,7 +3577,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "298",
+ "id": "306",
"metadata": {},
"outputs": [],
"source": [
@@ -3316,7 +3587,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "299",
+ "id": "307",
"metadata": {},
"outputs": [],
"source": [
@@ -3325,7 +3596,7 @@
},
{
"cell_type": "markdown",
- "id": "300",
+ "id": "308",
"metadata": {},
"source": [
"Caching and decorators play a crucial role in optimizing function performance.\n",
@@ -3338,7 +3609,7 @@
},
{
"cell_type": "markdown",
- "id": "301",
+ "id": "309",
"metadata": {},
"source": [
"Additionally, our current implementation does not handle keyword arguments (`**kwargs`), which can be a significant limitation in more complex scenarios.\n",
@@ -3352,7 +3623,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "302",
+ "id": "310",
"metadata": {},
"outputs": [],
"source": [
@@ -3362,20 +3633,20 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "303",
+ "id": "311",
"metadata": {},
"outputs": [],
"source": [
"@lru_cache()\n",
"def fact(n):\n",
" print(f\"Calculating fact({n})\")\n",
- " return 1 if n < 2 else n * fact(n-1)"
+ " return 1 if n < 2 else n * fact(n - 1)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "304",
+ "id": "312",
"metadata": {},
"outputs": [],
"source": [
@@ -3385,7 +3656,7 @@
},
{
"cell_type": "markdown",
- "id": "305",
+ "id": "313",
"metadata": {},
"source": [
"Once again, the last value `fact(8)` was simply fetched from the cache."
@@ -3393,7 +3664,7 @@
},
{
"cell_type": "markdown",
- "id": "306",
+ "id": "314",
"metadata": {},
"source": [
"Now let's see if we have improved on our recursive approach of calculating Fibonacci numbers.\n",
@@ -3403,7 +3674,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "307",
+ "id": "315",
"metadata": {},
"outputs": [],
"source": [
@@ -3413,18 +3684,18 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "308",
+ "id": "316",
"metadata": {},
"outputs": [],
"source": [
"def fib_no_memo(n):\n",
- " return 1 if n < 3 else fib_no_memo(n-1) + fib_no_memo(n-2)"
+ " return 1 if n < 3 else fib_no_memo(n - 1) + fib_no_memo(n - 2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "309",
+ "id": "317",
"metadata": {},
"outputs": [],
"source": [
@@ -3438,19 +3709,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "310",
+ "id": "318",
"metadata": {},
"outputs": [],
"source": [
"@lru_cache()\n",
"def fib_memo(n):\n",
- " return 1 if n < 3 else fib_memo(n-1) + fib_memo(n-2)"
+ " return 1 if n < 3 else fib_memo(n - 1) + fib_memo(n - 2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "311",
+ "id": "319",
"metadata": {},
"outputs": [],
"source": [
@@ -3463,7 +3734,7 @@
},
{
"cell_type": "markdown",
- "id": "312",
+ "id": "320",
"metadata": {},
"source": [
"It's about **4 orders of magnitude** faster than the naive approach! 🔥\n",
@@ -3473,7 +3744,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "313",
+ "id": "321",
"metadata": {},
"outputs": [],
"source": [
@@ -3487,8 +3758,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "314",
- "metadata": {},
+ "id": "322",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"start = perf_counter()\n",
@@ -3500,7 +3773,7 @@
},
{
"cell_type": "markdown",
- "id": "315",
+ "id": "323",
"metadata": {},
"source": [
"Not the same time, but about the same order of magnitude.\n",
@@ -3509,8 +3782,10 @@
},
{
"cell_type": "markdown",
- "id": "316",
- "metadata": {},
+ "id": "324",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"You may have noticed that `lru_cache` was called an **empty list of arguments**, but it supports some.\n",
"One of them is the **cache size**: by default, it can hold up to **128 items**.\n",
@@ -3520,20 +3795,20 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "317",
+ "id": "325",
"metadata": {},
"outputs": [],
"source": [
"@lru_cache(maxsize=8)\n",
"def fib(n):\n",
" print(f\"Calculating fib({n})\")\n",
- " return 1 if n < 3 else fib(n-1) + fib(n-2)"
+ " return 1 if n < 3 else fib(n - 1) + fib(n - 2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "318",
+ "id": "326",
"metadata": {},
"outputs": [],
"source": [
@@ -3543,7 +3818,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "319",
+ "id": "327",
"metadata": {},
"outputs": [],
"source": [
@@ -3553,7 +3828,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "320",
+ "id": "328",
"metadata": {},
"outputs": [],
"source": [
@@ -3562,7 +3837,7 @@
},
{
"cell_type": "markdown",
- "id": "321",
+ "id": "329",
"metadata": {},
"source": [
"We had to recalculate `fib(1)` because when we called `fib(9)` the least recent item in the cache (the result of `fib(1)`) was evicted from the cache."
@@ -3570,7 +3845,7 @@
},
{
"cell_type": "markdown",
- "id": "322",
+ "id": "330",
"metadata": {},
"source": [
"### Parametrized decorators"
@@ -3578,7 +3853,7 @@
},
{
"cell_type": "markdown",
- "id": "323",
+ "id": "331",
"metadata": {},
"source": [
"Here comes a natural question: what if I need to pass some argument to my decorator?\n",
@@ -3587,7 +3862,7 @@
},
{
"cell_type": "markdown",
- "id": "324",
+ "id": "332",
"metadata": {},
"source": [
"Let's bring back our `timed` decorator and make a small change.\n",
@@ -3597,40 +3872,42 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "325",
+ "id": "333",
"metadata": {},
"outputs": [],
"source": [
"from time import perf_counter\n",
"\n",
+ "\n",
"def timed(fn):\n",
" def inner(*args, **kwargs):\n",
" total_elapsed = 0\n",
- " \n",
+ "\n",
" for i in range(10):\n",
" start = perf_counter()\n",
" result = fn(*args, **kwargs)\n",
" end = perf_counter()\n",
- " total_elapsed += (perf_counter() - start)\n",
- " \n",
+ " total_elapsed += perf_counter() - start\n",
+ "\n",
" avg_elapsed = total_elapsed / 10\n",
- " \n",
- " print(f'Avg runtime: {avg_elapsed:.6f}s')\n",
- " \n",
+ "\n",
+ " print(f\"Avg runtime: {avg_elapsed:.6f}s\")\n",
+ "\n",
" return result\n",
- " \n",
+ "\n",
" return inner"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "326",
+ "id": "334",
"metadata": {},
"outputs": [],
"source": [
"def calc_fib_recurse(n):\n",
- " return 1 if n < 3 else calc_fib_recurse(n-1) + calc_fib_recurse(n-2)\n",
+ " return 1 if n < 3 else calc_fib_recurse(n - 1) + calc_fib_recurse(n - 2)\n",
+ "\n",
"\n",
"@timed\n",
"def fib(n):\n",
@@ -3640,8 +3917,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "327",
- "metadata": {},
+ "id": "335",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"fib(30)"
@@ -3649,8 +3928,10 @@
},
{
"cell_type": "markdown",
- "id": "328",
- "metadata": {},
+ "id": "336",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"But what if I wanted to time this function **100 times**?\n",
"Or say that I have different functions that should be timed with a different number of repetitions?\n",
@@ -3662,45 +3943,46 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "329",
+ "id": "337",
"metadata": {},
"outputs": [],
"source": [
- "def timed(fn, num_reps): \n",
+ "def timed(fn, num_reps):\n",
" def inner(*args, **kwargs):\n",
" total_elapsed = 0\n",
- " \n",
+ "\n",
" for i in range(num_reps):\n",
" start = perf_counter()\n",
" result = fn(*args, **kwargs)\n",
" end = perf_counter()\n",
- " total_elapsed += (perf_counter() - start)\n",
- " \n",
+ " total_elapsed += perf_counter() - start\n",
+ "\n",
" avg_elapsed = total_elapsed / num_reps\n",
- " \n",
- " print(f'Avg runtime: {avg_elapsed:.6f}s ({num_reps} reps)')\n",
+ "\n",
+ " print(f\"Avg runtime: {avg_elapsed:.6f}s ({num_reps} reps)\")\n",
" return result\n",
- " \n",
+ "\n",
" return inner"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "330",
+ "id": "338",
"metadata": {},
"outputs": [],
"source": [
"def fib(n):\n",
" return calc_fib_recurse(n)\n",
"\n",
+ "\n",
"fib = timed(fib, 5)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "331",
+ "id": "339",
"metadata": {},
"outputs": [],
"source": [
@@ -3709,7 +3991,7 @@
},
{
"cell_type": "markdown",
- "id": "332",
+ "id": "340",
"metadata": {},
"source": [
"But wait: why did we use the fancy `@-` syntax?\n",
@@ -3718,7 +4000,7 @@
},
{
"cell_type": "markdown",
- "id": "333",
+ "id": "341",
"metadata": {},
"source": [
"To fix this behavior we need to rethink of what `@` is doing.\n",
@@ -3742,7 +4024,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "334",
+ "id": "342",
"metadata": {},
"outputs": [],
"source": [
@@ -3752,7 +4034,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "335",
+ "id": "343",
"metadata": {},
"outputs": [],
"source": [
@@ -3761,7 +4043,7 @@
},
{
"cell_type": "markdown",
- "id": "336",
+ "id": "344",
"metadata": {},
"source": [
"So, for the syntax `@timed(10)` to work, where `10` is the number of repetition, `timed` should return **a decorator itself**, and not our closure.\n",
@@ -3771,46 +4053,46 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "337",
+ "id": "345",
"metadata": {},
"outputs": [],
"source": [
"from functools import wraps\n",
"from time import perf_counter\n",
"\n",
+ "\n",
"def timed(num_reps=10):\n",
- " \n",
" def decorator(fn):\n",
- "\n",
" @wraps(fn)\n",
" def inner(*args, **kwargs):\n",
" total_elapsed = 0\n",
- " \n",
+ "\n",
" for i in range(num_reps):\n",
" start = perf_counter()\n",
" result = fn(*args, **kwargs)\n",
" end = perf_counter()\n",
- " total_elapsed += (perf_counter() - start)\n",
- " \n",
+ " total_elapsed += perf_counter() - start\n",
+ "\n",
" avg_elapsed = total_elapsed / num_reps\n",
- " \n",
- " print(f'Avg Run time: {avg_elapsed:.6f}s ({num_reps} reps)')\n",
+ "\n",
+ " print(f\"Avg Run time: {avg_elapsed:.6f}s ({num_reps} reps)\")\n",
" return result\n",
- " \n",
+ "\n",
" return inner\n",
- " \n",
- " return decorator "
+ "\n",
+ " return decorator"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "338",
+ "id": "346",
"metadata": {},
"outputs": [],
"source": [
"def calc_fib_recurse(n):\n",
- " return 1 if n < 3 else calc_fib_recurse(n-1) + calc_fib_recurse(n-2)\n",
+ " return 1 if n < 3 else calc_fib_recurse(n - 1) + calc_fib_recurse(n - 2)\n",
+ "\n",
"\n",
"@timed(10)\n",
"def fib(n):\n",
@@ -3820,7 +4102,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "339",
+ "id": "347",
"metadata": {},
"outputs": [],
"source": [
@@ -3830,14 +4112,16 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "340",
+ "id": "348",
"metadata": {},
"outputs": [],
"source": [
"from functools import lru_cache\n",
"\n",
+ "\n",
"def calc_fact(n):\n",
- " return 1 if n < 2 else n * calc_fact(n-1)\n",
+ " return 1 if n < 2 else n * calc_fact(n - 1)\n",
+ "\n",
"\n",
"@timed(20)\n",
"@lru_cache()\n",
@@ -3848,7 +4132,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "341",
+ "id": "349",
"metadata": {},
"outputs": [],
"source": [
@@ -3857,7 +4141,7 @@
},
{
"cell_type": "markdown",
- "id": "342",
+ "id": "350",
"metadata": {},
"source": [
"And yes, you can **stack multiple decorators**! 😎"
@@ -3865,7 +4149,29 @@
},
{
"cell_type": "markdown",
- "id": "343",
+ "id": "351",
+ "metadata": {},
+ "source": [
+ "### Quiz: Decorators"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "352",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
+ "outputs": [],
+ "source": [
+ "import tutorial.quiz.functions_advanced as fp\n",
+ "\n",
+ "fp.FunctionsAdvancedQuizDecorators()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "353",
"metadata": {},
"source": [
"## Generators"
@@ -3873,7 +4179,7 @@
},
{
"cell_type": "markdown",
- "id": "344",
+ "id": "354",
"metadata": {},
"source": [
"The concept of generators is very much tied to that of \"looping over some kind of container\".\n",
@@ -3885,12 +4191,12 @@
" # do something\n",
"```\n",
"\n",
- "The object that Python builds for us with `range(10)` is something very close to a generator. "
+ "The object that Python builds for us with `range(10)` is something very close to a generator."
]
},
{
"cell_type": "markdown",
- "id": "345",
+ "id": "355",
"metadata": {},
"source": [
"To understand generators, we first need to review what it means to be **iterable** and, more importantly, what is an **iterator**.\n",
@@ -3901,8 +4207,10 @@
},
{
"cell_type": "markdown",
- "id": "346",
- "metadata": {},
+ "id": "356",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Delving deep into iterators is out of the scope of this section, so we are going to show you a practical example of a class that implements the \"iterator protocol\".\n",
"\n",
@@ -3912,7 +4220,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "347",
+ "id": "357",
"metadata": {},
"outputs": [],
"source": [
@@ -3929,14 +4237,16 @@
" raise StopIteration\n",
"\n",
" self.i += 1\n",
- " return self.i ** 2"
+ " return self.i**2"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "348",
- "metadata": {},
+ "id": "358",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"for sq in Squares(5):\n",
@@ -3945,7 +4255,7 @@
},
{
"cell_type": "markdown",
- "id": "349",
+ "id": "359",
"metadata": {},
"source": [
"We see that we can indeed loop over our custom `Squares` class.\n",
@@ -3958,10 +4268,10 @@
},
{
"cell_type": "markdown",
- "id": "350",
+ "id": "360",
"metadata": {},
"source": [
- "As you might have learned by now, we can implement some built-in behavior in our classes by using the so-called \"special methods\" or **dunder methods**: those with this naming schema `__method__`.\n",
+ "As you might have learned by now, we can implement some built-in behavior in our classes by using the so-called \"special methods\" or **dunder methods**: `__method__`.\n",
"\n",
"A few examples:\n",
"\n",
@@ -3972,12 +4282,12 @@
},
{
"cell_type": "markdown",
- "id": "351",
+ "id": "361",
"metadata": {},
"source": [
- "Python also has the built-in `next()` which does what you think it does: it takes an **iterator** object and returns the next element in the stream of data by calling the `__next__` method implemented by that object.\n",
+ "Python also has the built-in `next()` which does what you think: it takes an **iterator** object and returns the **next element** in the stream of data by calling the `__next__` method implemented by that object.\n",
"\n",
- "It the same way, we can call `iter()` on an object as the **only** argument and return an iterator.\n",
+ "It the same way, we can call `iter()` with a **single argument** and return an iterator.\n",
"Our class is doing that by implementing the `__iter__` method.\n",
"\n",
"But there's another way of calling `iter()` with **two arguments**: the first must be a **callable** (i.e., a function) and the second argument is a **sentinel**. As soon as the callable returns the sentinel value, then a `StopIteration` is raised."
@@ -3985,8 +4295,10 @@
},
{
"cell_type": "markdown",
- "id": "352",
- "metadata": {},
+ "id": "362",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We could've written our `Squares` class using a closure instead:"
]
@@ -3994,26 +4306,31 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "353",
+ "id": "363",
"metadata": {},
"outputs": [],
"source": [
"def square():\n",
" i = 0\n",
+ "\n",
" def inner():\n",
" nonlocal i\n",
" i += 1\n",
- " return i ** 2\n",
+ " return i**2\n",
+ "\n",
" return inner\n",
"\n",
+ "\n",
"square_iter = iter(square(), 5**2)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "354",
- "metadata": {},
+ "id": "364",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"for sq in square_iter:\n",
@@ -4022,7 +4339,7 @@
},
{
"cell_type": "markdown",
- "id": "355",
+ "id": "365",
"metadata": {},
"source": [
"If the value returned by `square()` is 25 (our sentinel), then a `StopIteration` is raised."
@@ -4030,7 +4347,7 @@
},
{
"cell_type": "markdown",
- "id": "356",
+ "id": "366",
"metadata": {},
"source": [
"These two ways are identical: in the first case (the class), we built the iterator ourselves. In the second case, Python built it for us.\n",
@@ -4041,36 +4358,42 @@
},
{
"cell_type": "markdown",
- "id": "357",
- "metadata": {},
+ "id": "367",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"The `yield` statement is used almost like a `return` statement in a function, but there is a huge difference.\n",
"When the `yield` statement is encountered, Python returns whatever value `yield` specifies, but it **pauses** execution of the function.\n",
"We can then _call_ the same function again and it will _resume_ from where the last `yield` was encountered.\n",
"\n",
- "We do **not** resume the function by calling it the standard way, but we have to use the built-in `next()`:"
+ "Functions that contain `yield` statements are called **generator functions**. When you call a generator function, it doesn't execute the function body immediately. Instead, it returns a **generator object** that implements the iterator protocol.\n",
+ "\n",
+ "We do **not** resume the function by calling it the standard way, but we have to use the built-in `next()`. Alternatively, we can use the generator in a `for` loop, which will automatically call `next()` for us and handle the `StopIteration` exception when the generator is exhausted:\n",
+ "\n",
+ "Generator objects maintain local variables and execution state between calls, making them memory-efficient for processing large datasets because they produce values on-demand (lazy evaluation) rather than storing all values in memory at once."
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "358",
+ "id": "368",
"metadata": {},
"outputs": [],
"source": [
"def my_func():\n",
- " print('line 1')\n",
- " yield 'Python'\n",
- " print('line 2')\n",
- " yield 'Is'\n",
- " print('line 3')\n",
- " yield 'Great'"
+ " print(\"line 1\")\n",
+ " yield \"Python\"\n",
+ " print(\"line 2\")\n",
+ " yield \"Is\"\n",
+ " print(\"line 3\")\n",
+ " yield \"Great\""
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "359",
+ "id": "369",
"metadata": {},
"outputs": [],
"source": [
@@ -4080,7 +4403,7 @@
},
{
"cell_type": "markdown",
- "id": "360",
+ "id": "370",
"metadata": {},
"source": [
"Here it is: our function returned _something_ different than the usual \"function\" object.\n",
@@ -4090,7 +4413,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "361",
+ "id": "371",
"metadata": {},
"outputs": [],
"source": [
@@ -4100,7 +4423,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "362",
+ "id": "372",
"metadata": {},
"outputs": [],
"source": [
@@ -4110,7 +4433,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "363",
+ "id": "373",
"metadata": {},
"outputs": [],
"source": [
@@ -4120,7 +4443,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "364",
+ "id": "374",
"metadata": {},
"outputs": [],
"source": [
@@ -4129,7 +4452,7 @@
},
{
"cell_type": "markdown",
- "id": "365",
+ "id": "375",
"metadata": {},
"source": [
"A `StopIteration` is raised if we are trying to go past the last `yield` statement.\n",
@@ -4142,16 +4465,16 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "366",
+ "id": "376",
"metadata": {},
"outputs": [],
"source": [
- "'__iter__' in dir(gen_my_func)"
+ "\"__iter__\" in dir(gen_my_func)"
]
},
{
"cell_type": "markdown",
- "id": "367",
+ "id": "377",
"metadata": {},
"source": [
"And also the `__next__` method"
@@ -4160,61 +4483,199 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "368",
+ "id": "378",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\"__next__\" in dir(gen_my_func)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "379",
+ "metadata": {},
+ "source": [
+ "We can also check that `iter()` applied on our object returns indeed the same thing.\n",
+ "That is, our object is itself an iterator."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "380",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gen_my_func"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "381",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "iter(gen_my_func)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "382",
"metadata": {},
+ "source": [
+ "Precisely the same object."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "383",
+ "metadata": {},
+ "source": [
+ "How does Python know when to stop the iteration?\n",
+ "When should it raise the `StopIteration`?\n",
+ "A generator will terminate (raise `StopIteration`) in any of these cases:\n",
+ "\n",
+ "1. When the function body reaches its end (which is an implicit `return None`)\n",
+ "2. When a `return` statement is encountered (with or without a value)\n",
+ "3. When an exception is raised inside the generator\n",
+ "\n",
+ "When a generator ends with a `return` statement that includes a value, that value becomes attached \n",
+ "to the `StopIteration` exception. Let's see this in action with a practical example:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "384",
+ "metadata": {},
+ "source": [
+ "Here's a generator that yields numbers and returns a message at the end:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "385",
+ "metadata": {
+ "lines_to_next_cell": 1
+ },
+ "outputs": [],
+ "source": [
+ "def gen_with_return():\n",
+ " yield 1\n",
+ " yield 2\n",
+ " yield 3\n",
+ " return \"Generator finished!\" # This value gets attached to StopIteration"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "386",
+ "metadata": {},
+ "source": [
+ "When we iterate through the generator in a for loop, we only see the yielded values:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "387",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for value in gen_with_return():\n",
+ " print(value)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "388",
+ "metadata": {},
+ "source": [
+ "But we can capture the return value by catching the StopIteration exception:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "389",
+ "metadata": {
+ "lines_to_next_cell": 1
+ },
"outputs": [],
"source": [
- "'__next__' in dir(gen_my_func)"
+ "gen = gen_with_return()\n",
+ "try:\n",
+ " while True:\n",
+ " value = next(gen)\n",
+ " print(f\"Got value: {value}\")\n",
+ "except StopIteration as e:\n",
+ " print(f\"Return value: {e.value}\")"
]
},
{
"cell_type": "markdown",
- "id": "369",
+ "id": "390",
"metadata": {},
"source": [
- "We can also check that `iter()` applied on our object returns indeed the same thing.\n",
- "That is, our object is itself an iterator."
+ "This ability to attach return values to generators is used internally by Python features like `yield from`.\n",
+ "\n",
+ "Let's see a practical example of how `yield from` uses return values:"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "370",
- "metadata": {},
+ "id": "391",
+ "metadata": {
+ "lines_to_next_cell": 1
+ },
"outputs": [],
"source": [
- "gen_my_func"
+ "def sub_generator():\n",
+ " yield 'A'\n",
+ " yield 'B'\n",
+ " yield 'C'\n",
+ " return \"Sub-generator finished!\" # Return value"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "371",
- "metadata": {},
+ "id": "392",
+ "metadata": {
+ "lines_to_next_cell": 1
+ },
"outputs": [],
"source": [
- "iter(gen_my_func)"
+ "def main_generator():\n",
+ " # yield from delegates to sub_generator and gets its return value\n",
+ " result = yield from sub_generator()\n",
+ " print(f\"Got return value: {result}\")\n",
+ " yield \"Main generator continues\""
]
},
{
- "cell_type": "markdown",
- "id": "372",
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "393",
"metadata": {},
+ "outputs": [],
"source": [
- "Precisely the same object."
+ "gen = main_generator()\n",
+ "for item in gen:\n",
+ " print(f\"Yielded item: {item}\")"
]
},
{
"cell_type": "markdown",
- "id": "373",
- "metadata": {},
+ "id": "394",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
- "How Python knows when to stop the iteration?\n",
- "When should it raise the `StopIteration`?\n",
- "In the simple example above, it's easy: when there's nothing else after the last `yield`.\n",
- "\n",
- "Well, not really \"nothing\". Remember that Python returns `None` for us if we don't specify any `return` statement.\n",
- "So, in general, the iteration will terminate **when we return something from the function** using the `return` statement.\n",
+ "Notice how `yield from` automatically propagates all yields from the sub-generator,\n",
+ "and also captures its return value which we can then use in the main generator.\n",
"\n",
"Let's go back to our `squares` example and refactor it to have a generator:"
]
@@ -4222,7 +4683,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "374",
+ "id": "395",
"metadata": {},
"outputs": [],
"source": [
@@ -4230,16 +4691,16 @@
" i = 0\n",
" while True:\n",
" if i < sentinel:\n",
- " yield i ** 2\n",
+ " yield i**2\n",
" i += 1\n",
" else:\n",
- " return 'Finished.'"
+ " return \"Finished.\""
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "375",
+ "id": "396",
"metadata": {},
"outputs": [],
"source": [
@@ -4250,7 +4711,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "376",
+ "id": "397",
"metadata": {},
"outputs": [],
"source": [
@@ -4260,26 +4721,28 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "377",
+ "id": "398",
"metadata": {},
"outputs": [],
"source": [
- "next(sq) # this is the last"
+ "next(sq) # this is the last"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "378",
- "metadata": {},
+ "id": "399",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
- "next(sq) # a StopIteration is raised"
+ "next(sq) # a StopIteration is raised"
]
},
{
"cell_type": "markdown",
- "id": "379",
+ "id": "400",
"metadata": {},
"source": [
"Note how in the generator function above we incremented the number `i` **after** the `yield` statement.\n",
@@ -4288,7 +4751,7 @@
},
{
"cell_type": "markdown",
- "id": "380",
+ "id": "401",
"metadata": {},
"source": [
"### Create an interable from a generator"
@@ -4296,7 +4759,7 @@
},
{
"cell_type": "markdown",
- "id": "381",
+ "id": "402",
"metadata": {},
"source": [
"As we know, generators are iterators.\n",
@@ -4308,8 +4771,10 @@
},
{
"cell_type": "markdown",
- "id": "382",
- "metadata": {},
+ "id": "403",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"Let's consider again the example of generating squares of integers:"
]
@@ -4317,19 +4782,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "383",
+ "id": "404",
"metadata": {},
"outputs": [],
"source": [
"def squares_gen(n):\n",
" for i in range(n):\n",
- " yield i ** 2"
+ " yield i**2"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "384",
+ "id": "405",
"metadata": {},
"outputs": [],
"source": [
@@ -4339,7 +4804,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "385",
+ "id": "406",
"metadata": {},
"outputs": [],
"source": [
@@ -4349,7 +4814,7 @@
},
{
"cell_type": "markdown",
- "id": "386",
+ "id": "407",
"metadata": {},
"source": [
"But our generator is now exhausted and it has nothing left to return:"
@@ -4358,8 +4823,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "387",
- "metadata": {},
+ "id": "408",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"next(sq)"
@@ -4367,8 +4834,10 @@
},
{
"cell_type": "markdown",
- "id": "388",
- "metadata": {},
+ "id": "409",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"To restart the iteration, we need to create another instance of the generator.\n",
"We can wrap this behavior in an **iterable class**:"
@@ -4377,7 +4846,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "389",
+ "id": "410",
"metadata": {},
"outputs": [],
"source": [
@@ -4392,7 +4861,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "390",
+ "id": "411",
"metadata": {},
"outputs": [],
"source": [
@@ -4402,7 +4871,7 @@
},
{
"cell_type": "markdown",
- "id": "391",
+ "id": "412",
"metadata": {},
"source": [
"And we can do it again:"
@@ -4411,8 +4880,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "392",
- "metadata": {},
+ "id": "413",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"[num for num in sq]"
@@ -4420,8 +4891,10 @@
},
{
"cell_type": "markdown",
- "id": "393",
- "metadata": {},
+ "id": "414",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We can put everything is a single class to make things easier to read:"
]
@@ -4429,19 +4902,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "394",
+ "id": "415",
"metadata": {},
"outputs": [],
"source": [
"class Squares:\n",
" def __init__(self, n):\n",
" self.n = n\n",
- " \n",
+ "\n",
" @staticmethod\n",
" def squares_gen(n):\n",
" for i in range(n):\n",
- " yield i ** 2\n",
- " \n",
+ " yield i**2\n",
+ "\n",
" def __iter__(self):\n",
" return Squares.squares_gen(self.n)"
]
@@ -4449,7 +4922,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "395",
+ "id": "416",
"metadata": {},
"outputs": [],
"source": [
@@ -4459,7 +4932,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "396",
+ "id": "417",
"metadata": {},
"outputs": [],
"source": [
@@ -4469,8 +4942,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "397",
- "metadata": {},
+ "id": "418",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"outputs": [],
"source": [
"[num for num in sq]"
@@ -4478,7 +4953,7 @@
},
{
"cell_type": "markdown",
- "id": "398",
+ "id": "419",
"metadata": {},
"source": [
"### Combining generators"
@@ -4486,8 +4961,10 @@
},
{
"cell_type": "markdown",
- "id": "399",
- "metadata": {},
+ "id": "420",
+ "metadata": {
+ "lines_to_next_cell": 2
+ },
"source": [
"We have to be careful when using a generator with one another.\n",
"For example, the `enumerate()` built-in returns a generator to iterate over an indexed container."
@@ -4496,19 +4973,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "400",
+ "id": "421",
"metadata": {},
"outputs": [],
"source": [
"def squares(n):\n",
" for i in range(n):\n",
- " yield i ** 2"
+ " yield i**2"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "401",
+ "id": "422",
"metadata": {},
"outputs": [],
"source": [
@@ -4518,7 +4995,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "402",
+ "id": "423",
"metadata": {},
"outputs": [],
"source": [
@@ -4527,7 +5004,7 @@
},
{
"cell_type": "markdown",
- "id": "403",
+ "id": "424",
"metadata": {},
"source": [
"Now, `enumerate` builds a generator itself, so `sq` had not been consumed yet at this point:"
@@ -4536,7 +5013,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "404",
+ "id": "425",
"metadata": {},
"outputs": [],
"source": [
@@ -4546,7 +5023,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "405",
+ "id": "426",
"metadata": {},
"outputs": [],
"source": [
@@ -4555,7 +5032,7 @@
},
{
"cell_type": "markdown",
- "id": "406",
+ "id": "427",
"metadata": {},
"source": [
"But since we now have consumed **2 elements** from `sq`, when we use `enumerate` it will also have two less items from `sq`:"
@@ -4564,8 +5041,10 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "407",
- "metadata": {},
+ "id": "428",
+ "metadata": {
+ "lines_to_next_cell": 1
+ },
"outputs": [],
"source": [
"next(enum_sq)"
@@ -4573,7 +5052,7 @@
},
{
"cell_type": "markdown",
- "id": "408",
+ "id": "429",
"metadata": {},
"source": [
"And this might not be what you expected: the value is the **third** element of `sq` ($2^2$), while the index is `0`, as if we were starting from the beginning.\n",
@@ -4584,7 +5063,148 @@
},
{
"cell_type": "markdown",
- "id": "409",
+ "id": "430",
+ "metadata": {},
+ "source": [
+ "### Beware: Generators can only be consumed once!\n",
+ "\n",
+ "One of the most common mistakes with generators is trying to use them multiple times. Unlike lists or tuples, generators are exhausted once they've been consumed:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "431",
+ "metadata": {
+ "lines_to_next_cell": 1
+ },
+ "outputs": [],
+ "source": [
+ "def numbers_gen():\n",
+ " for i in range(3):\n",
+ " yield i"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "432",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Create a generator\n",
+ "gen = numbers_gen()\n",
+ "print(\"Generator type:\", type(gen))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "433",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# First iteration works fine\n",
+ "print(\"First iteration:\")\n",
+ "for num in gen:\n",
+ " print(num)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "434",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Second iteration - nothing happens!\n",
+ "print(\"Second iteration:\")\n",
+ "for num in gen:\n",
+ " print(num) # This loop won't execute at all!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "435",
+ "metadata": {},
+ "source": [
+ "Compare this with a list, which can be iterated multiple times:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "436",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Create a list\n",
+ "numbers_list = list(range(3))\n",
+ "print(\"List type:\", type(numbers_list))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "437",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# First iteration\n",
+ "print(\"First iteration:\")\n",
+ "for num in numbers_list:\n",
+ " print(num)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "438",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Second iteration still works!\n",
+ "print(\"Second iteration:\")\n",
+ "for num in numbers_list:\n",
+ " print(num) # This works fine with lists"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "439",
+ "metadata": {},
+ "source": [
+ "If you need to iterate through generator values multiple times, convert it to a list first:\n",
+ "```python\n",
+ "gen = some_generator()\n",
+ "values = list(gen) # Exhaust the generator and store all values\n",
+ "```\n",
+ "But remember that this defeats the memory-saving purpose of generators for large datasets."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "440",
+ "metadata": {},
+ "source": [
+ "### Quiz: Generators"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "441",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import tutorial.quiz.functions_advanced as fp\n",
+ "\n",
+ "fp.FunctionsAdvancedQuizGenerators()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "442",
"metadata": {},
"source": [
"## Exercises"
@@ -4593,7 +5213,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "410",
+ "id": "443",
"metadata": {},
"outputs": [],
"source": [
@@ -4602,22 +5222,22 @@
},
{
"cell_type": "markdown",
- "id": "411",
+ "id": "444",
"metadata": {},
"source": [
- "### Password checker factory"
+ "### Exercise 1: Password checker factory"
]
},
{
"cell_type": "markdown",
- "id": "412",
+ "id": "445",
"metadata": {
"jp-MarkdownHeadingCollapsed": true
},
"source": [
- "Create a function called `password_checker_factory` that can be used to generate different password checkers.\n",
+ "Complete the function `solution_password_checker_factory` so that it can be used to generate different password checkers.\n",
"This function will take **four parameters**: `min_uppercase`, `min_lowercase`, `min_punctuation`, and `min_digits`.\n",
- "They represents the constraints on a given password:\n",
+ "They represent the constraints on a given password:\n",
"\n",
"1. The minimum number of uppercase letters.\n",
"2. The minimum number of lowercase letters.\n",
@@ -4625,7 +5245,7 @@
"4. The minimum number of digits.\n",
"\n",
"\n",
- "The `create_password_checker` function generates another function that assesses a given password (string).\n",
+ "The `solution_password_checker_factory` function generates another function that assesses a given password (string).\n",
"This resulting function returns a **tuple with two elements**:\n",
"\n",
"1. The first element is a **boolean** indicating if the password passed validation.\n",
@@ -4634,7 +5254,7 @@
},
{
"cell_type": "markdown",
- "id": "413",
+ "id": "446",
"metadata": {},
"source": [
"For example, to create a password checker that requires a password to have at least 2 uppercase letters, at least 3 lowercase letters, at least 1 punctuation mark, and at least 4 digits, we can write\n",
@@ -4664,31 +5284,49 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "414",
+ "id": "447",
"metadata": {},
"outputs": [],
"source": [
"%%ipytest\n",
"\n",
- "def solution_password_checker_factory(min_up: int, min_low: int, min_pun: int, min_dig: int):\n",
- " \"\"\"Password checker factory\"\"\"\n",
- " "
+ "def solution_password_checker_factory(\n",
+ " min_up: int, min_low: int, min_pun: int, min_dig: int\n",
+ "):\n",
+ " \"\"\"A function that returns a password checker function.\n",
+ " The password checker function checks if a password meets the specified criteria:\n",
+ " - At least `min_up` uppercase letters\n",
+ " - At least `min_low` lowercase letters\n",
+ " - At least `min_pun` punctuation characters\n",
+ " - At least `min_dig` digits\n",
+ " The password checker function takes a password as input and returns a tuple with two elements:\n",
+ " - A boolean indicating if the password meets the criteria\n",
+ " - A dictionary mapping the `uppercase`, `lowercase`, `punctuation`, and `digits` criteria to the difference between the actual count and the required count\n",
+ "\n",
+ " Args:\n",
+ " min_up: Minimum number of uppercase letters\n",
+ " min_low: Minimum number of lowercase letters\n",
+ " min_pun: Minimum number of punctuation characters\n",
+ " min_dig: Minimum number of digits\n",
+ " Returns:\n",
+ " - A function that checks if a password meets the criteria\n",
+ " \"\"\""
]
},
{
"cell_type": "markdown",
- "id": "415",
+ "id": "448",
"metadata": {},
"source": [
- "### String range"
+ "### Exercise 2: String range"
]
},
{
"cell_type": "markdown",
- "id": "416",
+ "id": "449",
"metadata": {},
"source": [
- "Create a function called `str_range` that emulates the the built-in `range`, but for characters.\n",
+ "Complete the function called `solution_str_range` so that it emulates the built-in `range`, but for characters.\n",
"That is, when you call `str_range('j', 'm')`, you will get back a generator that produces each of the letters in between.\n",
"\n",
"The function takes two **mandatory** parameters, `start` and `end`, plus an **optional** `step` value, with default value of `1`.\n",
@@ -4699,7 +5337,7 @@
},
{
"cell_type": "markdown",
- "id": "417",
+ "id": "450",
"metadata": {},
"source": [
"
\n",
@@ -4709,7 +5347,7 @@
},
{
"cell_type": "markdown",
- "id": "418",
+ "id": "451",
"metadata": {},
"source": [
"
\n",
@@ -4720,28 +5358,38 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "419",
+ "id": "452",
"metadata": {},
"outputs": [],
"source": [
"%%ipytest\n",
"\n",
"def solution_str_range(start: str, end: str, step: int):\n",
- " \"\"\"Return a generator from `start` to `end` strings (inclusive)\"\"\"\n",
- " "
+ " \"\"\"Return a generator from `start` to `end` strings (inclusive) with an optional step size of `step`.\n",
+ " Example:\n",
+ " >>> list(solution_str_range('a', 'z', 2))\n",
+ " ['a', 'c', 'e', 'g', 'i', 'k', 'm', 'o', 'q', 's', 'u', 'w', 'y']\n",
+ "\n",
+ " Args:\n",
+ " start: The starting string\n",
+ " end: The ending string\n",
+ " step: The step size (number of characters to skip)\n",
+ " Returns:\n",
+ " - A generator that yields strings from `start` to `end` with the specified step size\n",
+ " \"\"\""
]
},
{
"cell_type": "markdown",
- "id": "420",
+ "id": "453",
"metadata": {},
"source": [
- "### Read `n` lines"
+ "### Exercise 3: Read `n` lines"
]
},
{
"cell_type": "markdown",
- "id": "421",
+ "id": "454",
"metadata": {},
"source": [
"Create a function called `read_n_lines` that takes two arguments: the filename from which to read, and the **maximum number of lines** that should be returned with each iteration.\n",
@@ -4785,7 +5433,7 @@
},
{
"cell_type": "markdown",
- "id": "422",
+ "id": "455",
"metadata": {},
"source": [
"We could also do:\n",
@@ -4811,42 +5459,48 @@
},
{
"cell_type": "markdown",
- "id": "423",
+ "id": "456",
"metadata": {},
"source": [
"
\n",
- "
Note
With each iteration, read_n_lines shoul return a string (not a list) containing up to the number of lines specified by the parameter lines.\n",
+ " Note
With each iteration, read_n_lines should return a string (not a list) containing up to the number of lines specified by the parameter lines.\n",
""
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "424",
+ "id": "457",
"metadata": {},
"outputs": [],
"source": [
"%%ipytest\n",
"\n",
"def solution_read_n_lines(filename: str, lines: int):\n",
- " \"\"\"Read multiple lines from a file\"\"\"\n",
- " "
+ " \"\"\"Read multiple lines from a file and return them as one string.\n",
+ "\n",
+ " Args:\n",
+ " filename: The name of the file to read from\n",
+ " lines: The number of lines to read each time\n",
+ " Returns:\n",
+ " - A string containing the lines read from the file\n",
+ " \"\"\""
]
},
{
"cell_type": "markdown",
- "id": "425",
+ "id": "458",
"metadata": {},
"source": [
- "### Only run once"
+ "### Exercise 4: Only run once"
]
},
{
"cell_type": "markdown",
- "id": "426",
+ "id": "459",
"metadata": {},
"source": [
- "Create a decorator called `once` that restricts a function to run at most **once every `allowed_time` seconds**, where `allowed_time` is a parameter with a default value of `15`.\n",
+ "Create a decorator called `solution_once` that restricts a function to run at most **once every `allowed_time` seconds**, where `allowed_time` is a parameter with a default value of `15`.\n",
"\n",
"If you try to invoke the function too soon, the decorator should raise an exception called `RuntimeError` which tells you how long you need to wait before running your function again.\n",
"The error message should be `Wait another {remaining_time} seconds`, where `remaining_time` is the time left to wait before running the function again."
@@ -4854,7 +5508,7 @@
},
{
"cell_type": "markdown",
- "id": "427",
+ "id": "460",
"metadata": {},
"source": [
"For example, the following code:\n",
@@ -4871,14 +5525,14 @@
" try:\n",
" time.sleep(3)\n",
" print(hello(f\"attempt #{i}\"))\n",
- " except TooSoonError as err:\n",
+ " except RuntimeError as err:\n",
" print(f\"Too soon: {err}\")\n",
"```"
]
},
{
"cell_type": "markdown",
- "id": "428",
+ "id": "461",
"metadata": {},
"source": [
"Should print something like:\n",
@@ -4903,7 +5557,7 @@
},
{
"cell_type": "markdown",
- "id": "429",
+ "id": "462",
"metadata": {},
"source": [
"
\n",
@@ -4922,20 +5576,35 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "430",
+ "id": "463",
"metadata": {},
"outputs": [],
"source": [
"%%ipytest\n",
+ "\n",
"import time\n",
+ "from typing import Callable\n",
+ "\n",
"\n",
- "def solution_once(allowed_time: int = 15) -> t.Callable:\n",
- " \"\"\"Decorator to run a function at most once per given seconds\"\"\"\n",
- " "
+ "def solution_once(allowed_time: int = 15) -> Callable:\n",
+ " \"\"\"\n",
+ " Decorator to run a function at most once per given seconds.\n",
+ " The function will be allowed to run again after the specified time has passed.\n",
+ " If the function is called again before the time has passed,\n",
+ " the decorator will raise a `RuntimeError` with a message indicating the time remaining before the function can be called again.\n",
+ "\n",
+ " Args:\n",
+ " allowed_time: The time in seconds to wait before allowing the function to run again. Default is 15 seconds.\n",
+ " Returns:\n",
+ " - A decorator that runs the function at most once per given seconds\n",
+ " \"\"\""
]
}
],
"metadata": {
+ "jupytext": {
+ "formats": "ipynb,py:percent"
+ },
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
@@ -4951,7 +5620,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.10"
+ "version": "3.12.10"
}
},
"nbformat": 4,
diff --git a/tutorial/quiz/functions_advanced.py b/tutorial/quiz/functions_advanced.py
index ac026f7e..c4e76872 100644
--- a/tutorial/quiz/functions_advanced.py
+++ b/tutorial/quiz/functions_advanced.py
@@ -1,6 +1,6 @@
from markdown import markdown
-from .common import Spoiler
+from .common import Question, Quiz, Spoiler
tricky_closures = Spoiler(
"Answer",
@@ -9,3 +9,196 @@
"Hence, the variable `n` is **not** a free variable, and the function `op()` is **not** a closure, just a plain function."
),
)
+
+
+class FunctionsAdvancedQuiz(Quiz):
+ def __init__(self, title=""):
+ q1 = Question(
+ question="""Are functions in Python objects?
+
def example():\n return "Hello, World!"\n\n# Can we do this?\nfunc = example\nprint(func())
""",
+ options={
+ "Yes": "Correct! Functions in Python are first-class objects. They can be assigned to variables, passed as arguments, and returned from other functions.",
+ "No": "Incorrect. Functions in Python are indeed objects and can be manipulated like any other object.",
+ },
+ correct_answer="Yes",
+ hint="Think about whether you can assign a function to a variable or pass it as an argument.",
+ shuffle=True,
+ )
+
+ q2 = Question(
+ question="""What is the correct order of scope resolution in Python?
+
def outer():\n x = 'enclosing'\n def inner():\n x = 'local'\n print(x)\n inner()\nouter()
""",
+ options={
+ "Local → Enclosing → Global → Built-in": "Correct! Python resolves variable names in this order.",
+ "Global → Local → Enclosing → Built-in": "Incorrect. The correct order is Local → Enclosing → Global → Built-in.",
+ "Built-in → Global → Enclosing → Local": "Incorrect. The correct order is Local → Enclosing → Global → Built-in.",
+ },
+ correct_answer="Local → Enclosing → Global → Built-in",
+ hint="Remember the acronym LEGB.",
+ shuffle=True,
+ )
+
+ q3 = Question(
+ question="""Why is using mutable default arguments in Python considered a bad practice?
+
def add_item(item, items=[]):\n items.append(item)\n return items\n\nprint(add_item(1))\nprint(add_item(2))
""",
+ options={
+ "Because the default value is shared across all calls to the function.": "Correct! Mutable default arguments retain their state across function calls.",
+ "Because Python does not allow mutable default arguments.": "Incorrect. Python allows mutable default arguments, but they can lead to unexpected behavior.",
+ "Because it causes a syntax error.": "Incorrect. Mutable default arguments do not cause syntax errors.",
+ },
+ correct_answer="Because the default value is shared across all calls to the function.",
+ hint="Think about what happens to the `items` list after multiple calls.",
+ shuffle=True,
+ )
+
+ super().__init__(questions=[q1, q2, q3])
+
+
+class FunctionsAdvancedQuizLambdas(Quiz):
+ def __init__(self, title=""):
+ q1 = Question(
+ question="""What is the correct syntax for a lambda function that adds two numbers?
+
# Example usage:\nadd = ?\nprint(add(2, 3)) # Output: 5
""",
+ options={
+ "lambda x, y: x + y": "Correct! This is the correct syntax for a lambda function.",
+ "def lambda(x, y): x + y": "Incorrect. Lambda functions do not use the `def` keyword.",
+ "lambda (x, y): x + y": "Incorrect. Parentheses are not used around the arguments in lambda functions.",
+ },
+ correct_answer="lambda x, y: x + y",
+ hint="Lambda functions use the syntax `lambda arguments: expression`.",
+ shuffle=True,
+ )
+
+ q2 = Question(
+ question="""What is the limitation of lambda functions in Python?
+
# Example:\nadd = lambda x, y: x + y\nprint(add(2, 3))
""",
+ options={
+ "They can only contain a single expression.": "Correct! Lambda functions are limited to a single expression.",
+ "They cannot take arguments.": "Incorrect. Lambda functions can take arguments.",
+ "They are slower than regular functions.": "Incorrect. Lambda functions are not inherently slower than regular functions.",
+ },
+ correct_answer="They can only contain a single expression.",
+ hint="Think about the syntax of lambda functions.",
+ shuffle=True,
+ )
+
+ super().__init__(questions=[q1, q2])
+
+
+class FunctionsAdvancedQuizClosures(Quiz):
+ def __init__(self, title=""):
+ q1 = Question(
+ question="""What is a closure in Python?
+
def outer():\n x = 'free variable'\n def inner():\n return x\n return inner\n\nclosure = outer()\nprint(closure())
""",
+ options={
+ "A function that retains access to its enclosing scope's variables.": "Correct! Closures allow inner functions to remember variables from their enclosing scope.",
+ "A function that does not take any arguments.": "Incorrect. Closures are not defined by the number of arguments.",
+ "A function that is defined inside another function.": "Incorrect. While closures are often defined inside other functions, this is not their defining characteristic.",
+ },
+ correct_answer="A function that retains access to its enclosing scope's variables.",
+ hint="Think about how the `inner` function accesses `x` even after `outer` has finished executing.",
+ shuffle=True,
+ )
+
+ q2 = Question(
+ question="""Why is the variable `x` in the following example considered a free variable?
+
def outer():\n x = 10\n def inner():\n return x\n return inner\n\nclosure = outer()\nprint(closure())
""",
+ options={
+ "Because it is defined in the enclosing scope and used in the inner function.": "Correct! A free variable is one that is not defined in the local scope but is used in the function.",
+ "Because it is defined in the global scope.": "Incorrect. The variable `x` is defined in the enclosing scope, not the global scope.",
+ "Because it is a local variable.": "Incorrect. The variable `x` is not local to the inner function.",
+ },
+ correct_answer="Because it is defined in the enclosing scope and used in the inner function.",
+ hint="Think about where `x` is defined and how it is accessed.",
+ shuffle=True,
+ )
+
+ q3 = Question(
+ question="""What happens if you modify a free variable in a closure?
+
def outer():\n x = 10\n def inner():\n x += 1\n return x\n return inner\n\nclosure = outer()\nclosure()
""",
+ options={
+ "It raises an UnboundLocalError.": "Correct! You cannot modify a free variable directly in a closure without declaring it as nonlocal.",
+ "It modifies the variable in the enclosing scope.": "Incorrect. Free variables cannot be modified directly without using the `nonlocal` keyword.",
+ "It creates a new local variable.": "Incorrect. The `x` in the inner function is not treated as a new local variable in this case.",
+ },
+ correct_answer="It raises an UnboundLocalError.",
+ hint="Think about whether the `x` in the inner function is treated as local or nonlocal.",
+ shuffle=True,
+ )
+
+ super().__init__(questions=[q1, q2, q3])
+
+
+class FunctionsAdvancedQuizDecorators(Quiz):
+ def __init__(self, title=""):
+ q1 = Question(
+ question="""What is the purpose of a decorator in Python?
+
@decorator\ndef example():\n pass
""",
+ options={
+ "To modify or extend the behavior of a function or method.": "Correct! Decorators are used to enhance or modify functions.",
+ "To define a new function.": "Incorrect. Decorators do not define new functions.",
+ "To execute a function immediately.": "Incorrect. Decorators wrap a function but do not execute it immediately.",
+ },
+ correct_answer="To modify or extend the behavior of a function or method.",
+ hint="Think about how decorators are used to add functionality to existing functions.",
+ shuffle=True,
+ )
+
+ q2 = Question(
+ question="""What does the following decorator do?
+
def decorator(func):\n def wrapper():\n print("Before the function call")\n func()\n print("After the function call")\n return wrapper\n\n@decorator\ndef example():\n print("Inside the function")\n\nexample()""",
+ options={
+ "It adds behavior before and after the function call.": "Correct! The decorator adds behavior before and after the wrapped function is executed.",
+ "It modifies the function to return a different value.": "Incorrect. The decorator does not modify the return value of the function.",
+ "It executes the function twice.": "Incorrect. The function is executed only once.",
+ },
+ correct_answer="It adds behavior before and after the function call.",
+ hint="Look at the `wrapper` function and what it does before and after calling `func()`.",
+ shuffle=True,
+ )
+
+ q3 = Question(
+ question="""Can a decorator accept arguments? If yes, how?
+
def decorator_with_args(arg):\n def decorator(func):\n def wrapper():\n print(f"Decorator argument: {arg}")\n func()\n return wrapper\n return decorator\n\n@decorator_with_args("Hello")\ndef example():\n print("Inside the function")\n\nexample()""",
+ options={
+ "Yes, by nesting the decorator inside another function.": "Correct! A decorator can accept arguments by using an additional outer function.",
+ "No, decorators cannot accept arguments.": "Incorrect. Decorators can accept arguments by using an additional outer function.",
+ "Yes, but only if the arguments are strings.": "Incorrect. Decorators can accept arguments of any type.",
+ },
+ correct_answer="Yes, by nesting the decorator inside another function.",
+ hint="Think about how the `decorator_with_args` function works.",
+ shuffle=True,
+ )
+
+ super().__init__(questions=[q1, q2, q3])
+
+
+class FunctionsAdvancedQuizGenerators(Quiz):
+ def __init__(self, title=""):
+ q1 = Question(
+ question="""What is the purpose of the `yield` keyword in Python?
+
def generator():\n yield 1\n yield 2\n yield 3\n\nfor value in generator():\n print(value)
""",
+ options={
+ "To produce a value and pause the function's execution.": "Correct! The `yield` keyword allows a function to produce values one at a time.",
+ "To return a value and terminate the function.": "Incorrect. `yield` pauses the function, unlike `return` which terminates it.",
+ "To define a function.": "Incorrect. `yield` is used inside a function, not to define it.",
+ },
+ correct_answer="To produce a value and pause the function's execution.",
+ hint="Think about how `yield` differs from `return`.",
+ shuffle=True,
+ )
+
+ q2 = Question(
+ question="""What happens when you call `next()` on a generator that has no more values to yield?
+
def generator():\n yield 1\n yield 2\n\ngen = generator()\nprint(next(gen))\nprint(next(gen))\nprint(next(gen))
""",
+ options={
+ "A StopIteration exception is raised.": "Correct! When a generator is exhausted, calling `next()` raises a StopIteration exception.",
+ "The generator restarts from the beginning.": "Incorrect. Generators do not restart automatically.",
+ "It returns `None`.": "Incorrect. Generators do not return `None` when exhausted; they raise an exception.",
+ },
+ correct_answer="A StopIteration exception is raised.",
+ hint="Think about what happens when a generator is exhausted.",
+ shuffle=True,
+ )
+
+ super().__init__(questions=[q1, q2])
diff --git a/tutorial/tests/test_12_functions_advanced.py b/tutorial/tests/test_12_functions_advanced.py
index 371b26e1..4e7e11b5 100644
--- a/tutorial/tests/test_12_functions_advanced.py
+++ b/tutorial/tests/test_12_functions_advanced.py
@@ -1,4 +1,5 @@
import pathlib
+import random
import re
import time
import typing as t
@@ -10,6 +11,27 @@
import pytest
+#
+# Example: Randomize list
+#
+def reference_randomize_list(my_list: list[int]) -> list[int]:
+ return sorted(my_list, key=lambda x: random.random())
+
+
+@pytest.mark.parametrize(
+ "my_list",
+ [
+ ([1, 2, 3, 4]),
+ (list(range(100))),
+ ],
+)
+def test_randomize_list(
+ function_to_test: t.Callable,
+ my_list: list[int],
+):
+ assert function_to_test(my_list) == reference_randomize_list(my_list)
+
+
#
# Exercise: Password checker factory
#
@@ -211,6 +233,21 @@ def test_once_waiting_not_enough_time(function_to_test: t.Callable) -> None:
assert wait_time and isclose(float(wait_time.group()), 1.0, abs_tol=1e-2)
+def test_once_waiting_enough_time(function_to_test: t.Callable) -> None:
+ # Test that waiting the allowed time lets the function run again
+ allowed_time = 2
+ _hello = function_to_test(allowed_time)(hello)
+
+ # First call should work
+ assert _hello("world") == "Hello world!"
+
+ # Wait for the full allowed time
+ time.sleep(allowed_time + 0.1) # Add small buffer to avoid timing issues
+
+ # Second call should work
+ assert _hello("world 2") == "Hello world 2!"
+
+
#
# Exercise: String range
#