From a6a77f7feb69d52b5cf8f41e5a1a70a486b55afa Mon Sep 17 00:00:00 2001 From: Edoardo Baldi Date: Thu, 28 Nov 2024 09:20:26 +0100 Subject: [PATCH] Updates to `functions` notebook (#251) --- functions.ipynb | 440 ++++++++++++++++++++----------- tutorial/tests/test_functions.py | 302 +++++++++++++-------- 2 files changed, 475 insertions(+), 267 deletions(-) diff --git a/functions.ipynb b/functions.ipynb index d442a36e..b186477c 100644 --- a/functions.ipynb +++ b/functions.ipynb @@ -13,7 +13,7 @@ "id": "1", "metadata": {}, "source": [ - "# Table of Contents\n", + "## Table of Contents\n", "\n", "- [References](#References)\n", "- [Functions are building blocks](#Functions-are-building-blocks)\n", @@ -31,20 +31,20 @@ " - [Default values](#Default-values)\n", "- [Exercise 1](#Exercise-1)\n", "- [Exercise 2](#Exercise-2)\n", + " - [Part 1](#Part-1)\n", + " - [Part 2](#Part-2)\n", + " - [Part 3](#Part-3)\n", "- [How Python executes a function](#How-Python-executes-a-function)\n", " - [Calling](#Calling)\n", " - [Executing](#Executing)\n", " - [Returning](#Returning)\n", "- [The scope of a function](#The-scope-of-a-function)\n", " - [Different types of scope](#Different-types-of-scope)\n", - "- [Global scope](#Global-scope)\n", "- [`*args` and `**kwargs`](#*args-and-**kwargs)\n", "- [Exercise 3](#Exercise-3)\n", "- [Quiz on functions](#Quiz-on-functions)\n", "- [Bonus exercises](#Bonus-exercises)\n", " - [Longest consecutive sequence](#Longest-consecutive-sequence)\n", - " - [Example 1](#Example-1)\n", - " - [Example 2](#Example-2)\n", " - [Part 2](#Part-2)\n", " - [Password validator](#Password-validator)\n", " - [Part 1](#Part-1)\n", @@ -156,8 +156,6 @@ "- The Python keyword to indicate that a name is a function's name is `def`, and it's a **reserved keyword**\n", "- The signature is what allows you to call the function and pass it arguments\n", "\n", - "---\n", - "\n", "For example:\n", "\n", "```python\n", @@ -172,8 +170,6 @@ "id": "11", "metadata": {}, "source": [ - "---\n", - "\n", "### Type hints\n", "\n", "Python doesn't require you to explicitly declare a variable's type. However, when defining functions, you can add **type hints** in the function's signature.\n", @@ -184,8 +180,6 @@ "

Warning

The Python interpreter does not enforce type hints and will not check them during runtime. Type hints are primarily intended for improving code readability, serving as documentation for developers, and making IDEs much more helpful when writing code.\n", "\n", "\n", - "---\n", - "\n", "They are specified using the `typing` module and are added in the function definition using colons followed by the expected type, right after the parameter name.\n", "\n", "For example:\n", @@ -252,8 +246,6 @@ " return product\n", "```\n", "\n", - "---\n", - "\n", "A few more examples of simple functions:\n", "\n", "```python\n", @@ -421,21 +413,21 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "26", "metadata": {}, - "outputs": [], "source": [ - "%reload_ext tutorial.tests.testsuite" + "## Exercise 1" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "27", "metadata": {}, + "outputs": [], "source": [ - "## Exercise 1" + "%reload_ext tutorial.tests.testsuite" ] }, { @@ -443,12 +435,13 @@ "id": "28", "metadata": {}, "source": [ - "Write a Python function called `greet` that takes two parameters: `name` (a string) and `age` (an integer).\n", + "Complete the function below `solution_greet` that should accept two parameters: `name` (a string) and `age` (an integer).\n", + "\n", "The function should return a greeting message in the following format: `\"Hello, ! You are years old.\"`\n", "\n", "
\n", "

Note

\n", - " Do not forget to write a proper docstring and add the correct type hints to the parameters and the return value.\n", + " Do not forget to add a docstring and the correct type hints to the parameters.\n", "
" ] }, @@ -461,7 +454,7 @@ "source": [ "%%ipytest\n", "\n", - "def solution_greet():\n", + "def solution_greet() -> str:\n", " return" ] }, @@ -470,59 +463,163 @@ "id": "30", "metadata": {}, "source": [ - "## Exercise 2\n", + "## Exercise 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "### Part 1\n", "\n", - "Write a Python function called `calculate_area` that takes three parameters: `length` (a float), `width` (a float), and `unit` (a string with a **default** value of `\"cm\"`).\n", - "The function should calculate the area of a rectangle based on the given length and width, and return the result **as a string** including the correct, default unit (i.e., `cm^2`).\n", - "If the unit parameter is \"m\", the function should convert the length and width from meters to centimeters before calculating the area.\n", + "Write a function called `solution_calculate_basic_area` that calculates the area of a rectangle given its length and width (in centimeters).\n", "\n", - "Your solution function **must** handle the following input units (the output unit is **always** `cm^2`):\n", + "The result should be returned as a string with **two** decimal digits, followed by \"cm^2\".\n", "\n", - "- centimeters (`cm`)\n", - "- meters (`m`)\n", - "- millimeters (`mm`)\n", - "- yards (`yd`)\n", - "- feet (`ft`)\n", + "*Hint:* you can use the [built-in function](https://docs.python.org/3/library/functions.html#round) `round(number, ndigits)` function to round the floating point `number` to a given number expressed by `ndigits`.\n", "\n", - "If you pass an unsupported unit, the function should **return** a string with the error message: `Invalid unit: `, where `` is the unsupported unit.\n", + "Example: `length=2`, `width=3` should return `\"6.00 cm^2\"`\n", "\n", "
\n", "

Note

\n", - " Do not forget to write a proper docstring and add the correct type hints to the parameters and the return value.\n", - "
\n", - "
\n", - "

Hints

\n", - "
    \n", - "
  • 1 yd = 91.44 cm
  • \n", - "
  • 1 ft = 30.48 cm
  • \n", - "
\n", + " Do not forget to add the correct type hints to the parameters and the return value.\n", "
" ] }, { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "33", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", - "def solution_calculate_area():\n", - " pass" + "def solution_calculate_basic_area(length, width):\n", + " \"\"\"Calculates the area of a rectangle in square centimeters.\n", + "\n", + " Args:\n", + " length: The length of the rectangle in centimeters\n", + " width: The width of the rectangle in centimeters\n", + "\n", + " Returns:\n", + " - A string representing the area with 2 decimal places,\n", + " followed by \"cm^2\" (e.g., \"6.00 cm^2\")\n", + " \"\"\"\n", + " return" ] }, { "cell_type": "markdown", - "id": "32", + "id": "34", "metadata": {}, "source": [ - "---" + "### Part 2\n", + "\n", + "Extend the previous function, now called `solution_calculate_metric_area`, by adding a `unit` parameter that can be either \"cm\" or \"m\".\n", + "If the unit is \"m\", convert the measurements to centimeters before calculating the area.\n", + "The result should still be in cm^2.\n", + "\n", + "*Hint:* remember the [built-in function](https://docs.python.org/3/library/functions.html#round) `round(number, ndigits)`.\n", + "\n", + "Example: `length=2`, `width=3`, `unit=\"m\"` should return `\"60000.00 cm^2\"` (because 2m × 3m = 6m² = 60000cm²)\n", + "\n", + "
\n", + "

Note

\n", + " Do not forget to add the correct type hints to the parameters and the return value.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_calculate_metric_area(length, width, unit):\n", + " \"\"\"Calculates the area of a rectangle, converting from meters if necessary.\n", + "\n", + " Args:\n", + " length: The length of the rectangle\n", + " width: The width of the rectangle\n", + " unit: The unit of measurement (\"cm\" or \"m\", defaults to \"cm\")\n", + "\n", + " Returns:\n", + " - A string representing the area in cm^2 with 2 decimal places.\n", + " If unit is invalid, returns \"Invalid unit: \"\n", + " \"\"\"\n", + " return" ] }, { "cell_type": "markdown", - "id": "33", + "id": "36", + "metadata": {}, + "source": [ + "### Part 3\n", + "\n", + "Extend the previous function, now called `solution_calculate_area`, to support the following units: cm, m, mm, yd, and ft.\n", + "The calculated area should always be in centimeters, so you need to take care of the appropriate conversions (when needed).\n", + "Keep the same string format for the result.\n", + "\n", + "Use the following conversion factors:\n", + "- $\\text{yd} / \\text{cm}= 91.44$\n", + "- $\\text{ft} / \\text{cm}= 30.48$\n", + "\n", + "*Hint:* remember the [built-in function](https://docs.python.org/3/library/functions.html#round) `round(number, ndigits)`.\n", + "\n", + "Examples:\n", + "- `length=2`, `width=3`, `unit=\"mm\"` should return `\"0.06 cm^2\"`\n", + "- `length=2`, `width=3`, `unit=\"yd\"` should return `\"50167.64 cm^2\"`\n", + "\n", + "
\n", + "

Note

\n", + " Do not forget to add the correct type hints to the parameters and the return value.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_calculate_area():\n", + " \"\"\"Calculates the area of a rectangle with support for multiple units.\n", + "\n", + " Args:\n", + " length: The length of the rectangle\n", + " width: The width of the rectangle\n", + " unit: The unit of measurement, one of:\n", + " \"cm\" (default), \"m\", \"mm\", \"yd\", \"ft\"\n", + "\n", + " Returns:\n", + " - A string representing the area in cm^2 with 2 decimal places.\n", + " If unit is invalid, returns \"Invalid unit: \"\n", + " \"\"\"\n", + " return" + ] + }, + { + "cell_type": "markdown", + "id": "38", "metadata": { "tags": [] }, @@ -532,7 +629,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "39", "metadata": { "tags": [] }, @@ -545,7 +642,6 @@ "2. **Executing**\n", "3. **Returning**\n", "\n", - "---\n", "\n", "### Calling\n", "\n", @@ -553,7 +649,6 @@ "- A function call frame contains information about the function call, such as the function name, arguments, local variables, and the return address (i.e., where to return control when the function returns)\n", "- The function call frame is pushed onto the top of the call stack\n", "\n", - "---\n", "\n", "### Executing\n", "\n", @@ -563,7 +658,6 @@ "- The function can access its parameters, declare local variables, and perform any other operations specified in the body\n", "- The function can call other functions, which will create their own function call frames and push them onto the call stack\n", "\n", - "---\n", "\n", "### Returning\n", "\n", @@ -577,7 +671,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "40", "metadata": { "tags": [] }, @@ -587,22 +681,16 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "41", "metadata": {}, "source": [ - "The following might seem trivial: could we assign the same variable name to two different values?\n", + "The following question might seem a trivial one: *could we assign the **same** variable name to **two** different values?*\n", "\n", - "The obvious (and right) answer is **no**. If `x` is `3` and `2` at the same time, how should Python evaluate an expression containing `x`?\n", + "The obvious (and right) answer is **no**. If `x=3` and we can also have `x=2` at the same time, how should Python evaluate an expression like `x + 1`?\n", "\n", - "There is, however, a workaround to this and it involves the concept of **scope**.\n", + "There is, however, a workaround and it involves the concept of **scope**.\n", "\n", - "
\n", - "

Important

You should not the scope to have a variable with two values at the same time, but it's an important concept to understand unexpected behaviours.\n", - "
\n", - "\n", - "---\n", - "\n", - "Look at the following lines of valid Python code:\n", + "Let's look at the following lines of valid Python code:\n", "\n", "```python\n", "x = \"Hello World\"\n", @@ -615,23 +703,54 @@ "print(f\"Outside 'func', x has the value {x}\")\n", "```\n", "\n", - "What output do you expect?\n", + "What output do you expect? Try it out below:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "x = \"Hello World\"\n", "\n", + "def func():\n", + " x = 2\n", + " return f\"Inside 'func', x has the value {x}\"\n", + "\n", + "print(func())\n", + "print(f\"Outside 'func', x has the value {x}\")" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ "Does `x` really have two simultaneous values?\n", "\n", "**Not really:** the reason is that `x` **within the function's body** and `x` **in the outside code** live in two separates **scopes**. The function body has a **local scope**, while all the code outside belongs to the **global scope**.\n", "\n", - "---\n", - "\n", + "
\n", + "

Important

You should not use scopes to have a variable with two values at the same time, but it's an important concept to understand and prevent unexpected behaviours (or bugs).\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ "### Different types of scope\n", "\n", "We can define the scope as **the region of a program where a variable can be accessed**.\n", "\n", - "1. **Global scope**: Variables declared at the top level of a program or module are in the global scope. They are accessible from anywhere in the program or module.\n", + "1. **Global scope**: Variables declared at the top level of a program or module are in the **global** scope. They are accessible from anywhere in the program or module.\n", "\n", - "2. **Local scope**: Variables declared inside a function are in the local scope. They are only accessible from within the function and are discarded when the function returns.\n", + "2. **Local scope**: Variables declared inside a function are in the **local** scope. They are only accessible from within the function and are discarded when the function returns.\n", "\n", - "3. **Non-local scope**: Variables defined in an outer function and declared as `nonlocal` in the inner nested function are in the non-local scope. They are accessible both from the outer function and from within the nested function.\n", + "3. **Non-local scope**: Variables defined in an outer function and declared as `nonlocal` in the *innermost* function are in the **non-local** scope. They are accessible both from the outer function and from within the nested function.\n", "\n", "\n", "Here's an example to illustrate the different scopes:" @@ -640,7 +759,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "45", "metadata": { "tags": [] }, @@ -660,17 +779,15 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "46", "metadata": {}, "source": [ - "In this example, `x` is a **global** variable and is accessible from anywhere in the program. `x` is also a **local** variable to the `function`, and is accessible from within the function's body.\n", - "\n", - "---" + "In this example, `x` is a **global** variable and is accessible from anywhere in the program. `x` is also a **local** variable to the `function`, and is accessible from within the function's body.\n" ] }, { "cell_type": "markdown", - "id": "39", + "id": "47", "metadata": {}, "source": [ "The `nonlocal` keyword is used to access a variable in the **nearest enclosing scope** that is **not** global.\n", @@ -682,7 +799,7 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "48", "metadata": {}, "source": [ "Look at the following example:" @@ -690,7 +807,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "49", "metadata": {}, "source": [ "```python\n", @@ -718,7 +835,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "50", "metadata": {}, "source": [ "The `global` keyword is used to access a global variable and modify its value from within a function.\n", @@ -729,7 +846,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "51", "metadata": { "tags": [] }, @@ -751,17 +868,17 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "52", "metadata": {}, "source": [ - "Even though you can access and modify variables from different scope, it's not considered a good practice. When a function makes use of `global` or `nonlocal`, or when modifying a mutable type in-place, it's like when a function modifies its own arguments. It's a **side-effect** that should be generally avoided.\n", + "Even though you can access and modify variables from a different scope, it's not considered a good practice. When a function makes use of `global` or `nonlocal`, or when modifying a mutable type in-place, it's like when a function modifies its own arguments. It's a **side-effect** that should be generally avoided.\n", "\n", "It's is considered a much better programming practice to make use of a function's return values instead of resorting to the scoping keywords." ] }, { "cell_type": "markdown", - "id": "45", + "id": "53", "metadata": {}, "source": [ "# `*args` and `**kwargs`" @@ -769,10 +886,10 @@ }, { "cell_type": "markdown", - "id": "46", + "id": "54", "metadata": {}, "source": [ - "In the [Basic datatypes](./basic_datatypes.ipynb#Unpacking) section we saw that iterables (tuples and lists) support **unpacking**. You can exploit unpacking to make a **parallel assignment**.\n", + "In the [Basic datatypes](./basic_datatypes.ipynb#Unpacking) section we saw that iterables (e.g., tuples, lists, strings) support **unpacking**. You can exploit unpacking to make a **parallel assignment**.\n", "\n", "A reminder:\n", "\n", @@ -789,7 +906,7 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -804,7 +921,7 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "56", "metadata": {}, "source": [ "The only difference with unpacking is: all the optional positional arguments are collected by `*args` **in a tuple** and not in a list.\n", @@ -817,7 +934,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "57", "metadata": {}, "source": [ "One importat rule:\n", @@ -845,7 +962,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -857,7 +974,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "59", "metadata": {}, "source": [ "The following **cannot** work\n", @@ -875,7 +992,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "60", "metadata": {}, "source": [ "Remember that functions' arguments can be either **positional** or **keyword** arguments:" @@ -884,7 +1001,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "61", "metadata": { "tags": [] }, @@ -897,7 +1014,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "62", "metadata": {}, "source": [ "We can call the function with" @@ -906,7 +1023,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "63", "metadata": { "tags": [] }, @@ -921,7 +1038,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "64", "metadata": {}, "source": [ "After `*args` there can be **no additional positional arguments** but we might have some (or no) keyword arguments.\n", @@ -932,7 +1049,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "65", "metadata": { "tags": [] }, @@ -952,7 +1069,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "66", "metadata": {}, "source": [ "Remember: `d` is a **required** keyword argument because we didn't supply a default value in the function definition.\n", @@ -963,7 +1080,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "67", "metadata": { "tags": [] }, @@ -975,7 +1092,7 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "68", "metadata": { "tags": [] }, @@ -987,7 +1104,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "69", "metadata": { "tags": [] }, @@ -998,7 +1115,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "70", "metadata": {}, "source": [ "We can even **omit** mandatory positional arguments\n", @@ -1015,7 +1132,7 @@ }, { "cell_type": "markdown", - "id": "63", + "id": "71", "metadata": {}, "source": [ "Or we can force **no positional arguments at all**:\n", @@ -1030,20 +1147,26 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "72", "metadata": {}, "source": [ "You can see that with iterables unpacking and the two `*`/`**` operators, Python is showing all its versatility when it comes to writing your own function.\n", "\n", - "If all this seems confusing, **try to experiment with these concepts** here in the notebook to better understand the behaviour. Create and call all the functions you want and check if their outputs is what you expect.\n", - "\n", - "---" + "If all this seems confusing, **try to experiment with these concepts** here in the notebook to better understand the behaviour. Create and call all the functions you want and check if their outputs is what you expect.\n" + ] + }, + { + "cell_type": "markdown", + "id": "73", + "metadata": {}, + "source": [ + "## Exercise 3" ] }, { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "74", "metadata": {}, "outputs": [], "source": [ @@ -1052,19 +1175,23 @@ }, { "cell_type": "markdown", - "id": "66", + "id": "75", "metadata": {}, "source": [ - "## Exercise 3\n", + "Complete function below `solution_combine_anything` that combines multiple arguments of the same type.\n", + "It must accept a *variable* number of arguments.\n", + "\n", + "Make sure to consider the case where you have **no arguments** at all.\n", "\n", - "Write a Python function called `summing_anything` that is able to sum **any** kind of arguments.\n", - "It therefore must accept a *variable* number of arguments.\n", - "You must **not** use the built-in function `sum()` to solve this exercise: you should write your own (generalized) implementation.\n", + "You must **not** use the built-in function `sum()`.\n", + "\n", + "*Hint:* read the function's docstring since it contains all the details.\n", "\n", "A few examples of how the function should work:\n", "\n", "| Arguments | Expected result |\n", "| --- | --- |\n", + "| `()` | `()` |\n", "| `('abc', 'def')` | `'abcdef'` |\n", "| `([1,2,3], [4,5,6])` | `[1,2,3,4,5,6]` |\n" ] @@ -1072,36 +1199,43 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "76", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", - "def solution_summing_anything():\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "id": "68", - "metadata": {}, - "source": [ - "---" + "def solution_combine_anything(*args):\n", + " \"\"\"Combines any number of arguments using the appropriate addition operation.\n", + "\n", + " The function determines the appropriate way to combine arguments based on their type:\n", + " - Strings are concatenated\n", + " - Lists are extended\n", + " - Numbers are added\n", + "\n", + " Args:\n", + " *args: Variable number of arguments to combine. Can be of any type\n", + " that supports the + operator (like str, list, int, etc.)\n", + "\n", + " Returns:\n", + " - The combined result using the appropriate operation for the input types.\n", + " If no arguments are provided, you should return an empty tuple.\n", + " \"\"\"\n", + " return" ] }, { "cell_type": "markdown", - "id": "69", + "id": "77", "metadata": {}, "source": [ - "## Quiz on functions" + "# Quiz on functions" ] }, { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "78", "metadata": { "tags": [] }, @@ -1113,12 +1247,12 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "79", "metadata": { "tags": [] }, "source": [ - "## Bonus exercises\n", + "# Bonus exercises\n", "\n", "
\n", "

Note

\n", @@ -1129,7 +1263,7 @@ }, { "cell_type": "markdown", - "id": "72", + "id": "80", "metadata": { "jp-MarkdownHeadingCollapsed": true }, @@ -1139,24 +1273,26 @@ }, { "cell_type": "markdown", - "id": "73", + "id": "81", "metadata": { "tags": [] }, "source": [ "Given an **unsorted** set of $N$ random integers, write a function that returns the length of the longest consecutive sequence of integers.\n", "\n", - "#### Example 1\n", + "**Example 1**\n", + "\n", "Given the list `numbers = [100, 4, 200, 1, 3, 2]`, the longest sequence is `[1, 2, 3, 4]` of length *4*.\n", "\n", - "#### Example 2\n", + "**Example 2**\n", + "\n", "Given the list `numbers = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1]`, the longest sequence contains all the numbers from 0 to 8, so its length is **9**.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "82", "metadata": { "tags": [] }, @@ -1173,7 +1309,7 @@ }, { "cell_type": "markdown", - "id": "75", + "id": "83", "metadata": { "jp-MarkdownHeadingCollapsed": true }, @@ -1191,7 +1327,7 @@ { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "84", "metadata": { "tags": [] }, @@ -1208,7 +1344,7 @@ }, { "cell_type": "markdown", - "id": "77", + "id": "85", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] @@ -1219,7 +1355,7 @@ }, { "cell_type": "markdown", - "id": "78", + "id": "86", "metadata": { "tags": [] }, @@ -1229,7 +1365,7 @@ }, { "cell_type": "markdown", - "id": "79", + "id": "87", "metadata": {}, "source": [ "You have a range of numbers `136760-595730` and need to count how many valid password it contains. A valid password must meet **all** the following criteria:\n", @@ -1250,7 +1386,7 @@ }, { "cell_type": "markdown", - "id": "80", + "id": "88", "metadata": {}, "source": [ "\n", @@ -1261,7 +1397,7 @@ }, { "cell_type": "markdown", - "id": "81", + "id": "89", "metadata": {}, "source": [ "\n", @@ -1273,7 +1409,7 @@ { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -1288,7 +1424,7 @@ }, { "cell_type": "markdown", - "id": "83", + "id": "91", "metadata": { "tags": [] }, @@ -1298,7 +1434,7 @@ }, { "cell_type": "markdown", - "id": "84", + "id": "92", "metadata": {}, "source": [ "You have a new rule: **at least** two adjacent matching digits **must not be part of a larger group of matching digits**.\n", @@ -1320,7 +1456,7 @@ { "cell_type": "code", "execution_count": null, - "id": "85", + "id": "93", "metadata": {}, "outputs": [], "source": [ @@ -1335,7 +1471,7 @@ }, { "cell_type": "markdown", - "id": "86", + "id": "94", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] @@ -1346,7 +1482,7 @@ }, { "cell_type": "markdown", - "id": "87", + "id": "95", "metadata": {}, "source": [ "#### Part 1" @@ -1354,7 +1490,7 @@ }, { "cell_type": "markdown", - "id": "88", + "id": "96", "metadata": {}, "source": [ "You have a list of buckets, each containing some items labeled from `a` to `z`, or from `A` to `Z`. Each bucket is split into **two** equally sized compartments.\n", @@ -1385,7 +1521,7 @@ }, { "cell_type": "markdown", - "id": "89", + "id": "97", "metadata": {}, "source": [ "
\n", @@ -1395,7 +1531,7 @@ }, { "cell_type": "markdown", - "id": "90", + "id": "98", "metadata": {}, "source": [ "
\n", @@ -1406,7 +1542,7 @@ { "cell_type": "code", "execution_count": null, - "id": "91", + "id": "99", "metadata": { "tags": [] }, @@ -1422,7 +1558,7 @@ }, { "cell_type": "markdown", - "id": "92", + "id": "100", "metadata": { "jp-MarkdownHeadingCollapsed": true }, @@ -1432,7 +1568,7 @@ }, { "cell_type": "markdown", - "id": "93", + "id": "101", "metadata": {}, "source": [ "You are told that you should not care about the priority of **every item**, but only of a \"special item\" that is common to groups of **three buckets**.\n", @@ -1458,7 +1594,7 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "102", "metadata": {}, "outputs": [], "source": [ @@ -1487,7 +1623,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/tutorial/tests/test_functions.py b/tutorial/tests/test_functions.py index c0ad1b9a..b8a08399 100644 --- a/tutorial/tests/test_functions.py +++ b/tutorial/tests/test_functions.py @@ -12,55 +12,52 @@ def read_data(name: str, data_dir: str = "data") -> pathlib.Path: return (pathlib.Path(__file__).parent / f"{data_dir}/{name}").resolve() -def errors_to_list(errors): - result = "
    " - for error in errors: - result += "
  • " + error + "
  • " - result += "
" - return result - - # # Exercise 1: a `greet` function # def reference_greet(name: str, age: int) -> str: - """Reference solution for the greet exercise""" + """Creates a personalized greeting message using name and age. + + Args: + name: The person's name to include in the greeting + age: The person's age in years + + Returns: + - A string in the format "Hello, ! You are years old." + """ return f"Hello, {name}! You are {age} years old." -@pytest.mark.parametrize( - "name,age", - [ - ("John", 30), - ], -) def test_greet( - name: str, - age: int, function_to_test, ) -> None: - errors = [] + name, age = "Alice", 30 - signature = inspect.signature(function_to_test) - params = signature.parameters - return_annotation = signature.return_annotation + params = inspect.signature(function_to_test).parameters + return_annotation = inspect.signature(function_to_test).return_annotation + + # Check docstring + assert function_to_test.__doc__ is not None, "The function is missing a docstring." - if function_to_test.__doc__ is None: - errors.append("The function is missing a docstring.") - if len(params) != 2: - errors.append("The function should take two arguments.") - if "name" not in params.keys() or "age" not in params.keys(): - errors.append("The function's parameters should be 'name' and 'age'.") - if any(p.annotation == inspect.Parameter.empty for p in params.values()): - errors.append("The function's parameters should have type hints.") - if return_annotation == inspect.Signature.empty: - errors.append("The function's return value is missing the type hint.") - - # test signature - assert not errors, errors_to_list(errors) - # test result + # Check number and names of parameters + assert len(params) == 2, "The function should take two arguments." + assert ( + "name" in params and "age" in params + ), "The function's parameters should be 'name' and 'age'." + + # Check type hints for parameters + assert all( + p.annotation != inspect.Parameter.empty for p in params.values() + ), "The function's parameters should have type hints." + + # Check return type hint + assert ( + return_annotation != inspect.Signature.empty + ), "The function's return value is missing the type hint." + + # Test the return value assert function_to_test(name, age) == reference_greet(name, age) @@ -69,97 +66,172 @@ def test_greet( # +# Part 1 +def reference_calculate_basic_area(length: float, width: float) -> str: + """Reference solution for Part 1: basic area calculation.""" + area = round(length * width, 2) + return f"{area} cm^2" + + +def validate_basic_area_signature(function_to_test) -> None: + """Validate signature of the basic area calculation function.""" + signature = inspect.signature(function_to_test) + params = signature.parameters + return_annotation = signature.return_annotation + + assert function_to_test.__doc__ is not None, "The function is missing a docstring." + assert ( + len(params) == 2 + ), "The function should take exactly two arguments (length and width)." + assert all( + p in params.keys() for p in ["length", "width"] + ), "The function's parameters should be 'length' and 'width'." + assert all( + p.annotation is float for p in params.values() + ), "Both parameters should be annotated as float." + assert return_annotation is str, "The return type should be annotated as str." + + +@pytest.mark.parametrize( + "length,width", + [ + (2.0, 3.0), + (5.0, 4.0), + (1.5, 2.5), + (0.1, 0.1), + ], +) +def test_calculate_basic_area(length: float, width: float, function_to_test): + validate_basic_area_signature(function_to_test) + expected = reference_calculate_basic_area(length, width) + result = function_to_test(length, width) + assert isinstance(result, str), "Result should be a string" + assert expected == result, "Incorrect area calculation or formatting" + + +# Part 2 + + +def reference_calculate_metric_area( + length: float, width: float, unit: str = "cm" +) -> str: + """Reference solution for Part 2: metric units only.""" + if unit not in ("cm", "m"): + return f"Invalid unit: {unit}" + + if unit == "m": + length *= 100 + width *= 100 + + area = round(length * width, 2) + return f"{area} cm^2" + + +def validate_metric_area_signature(function_to_test) -> None: + """Validate signature of the metric area calculation function.""" + signature = inspect.signature(function_to_test) + params = signature.parameters + return_annotation = signature.return_annotation + + assert function_to_test.__doc__ is not None, "The function is missing a docstring." + assert ( + len(params) == 3 + ), "The function should take three arguments (length, width, and unit)." + assert all( + p in params.keys() for p in ["length", "width", "unit"] + ), "The function's parameters should be 'length', 'width' and 'unit'." + assert ( + params["length"].annotation is float + ), "Parameter 'length' should be annotated as float." + assert ( + params["width"].annotation is float + ), "Parameter 'width' should be annotated as float." + assert ( + params["unit"].annotation is str + ), "Parameter 'unit' should be annotated as str." + assert ( + params["unit"].default == "cm" + ), "Parameter 'unit' should have a default value of 'cm'." + assert return_annotation is str, "The return type should be annotated as str." + + +@pytest.mark.parametrize( + "length,width,unit", + [ + (2.0, 3.0, "cm"), + (2.0, 3.0, "m"), + (1.5, 2.0, "cm"), + (1.5, 2.0, "m"), + ], +) +def test_calculate_metric_area(length, width, unit, function_to_test): + validate_metric_area_signature(function_to_test) + expected = reference_calculate_metric_area(length, width, unit) + result = function_to_test(length, width, unit) + assert isinstance(result, str), "Result should be a string" + assert expected == result, "Incorrect area calculation or formatting" + + +# Part 3 + + def reference_calculate_area(length: float, width: float, unit: str = "cm") -> str: - """Reference solution for the calculate_area exercise""" - # Conversion factors from supported units to centimeters - units = { - "cm": 1.0, - "m": 100.0, - "mm": 10.0, - "yd": 91.44, - "ft": 30.48, - } + """Reference solution for Part 3: all units.""" + conversions = {"cm": 1, "m": 100, "mm": 0.1, "yd": 91.44, "ft": 30.48} try: - area = length * width * units[unit] ** 2 + factor = conversions[unit] except KeyError: return f"Invalid unit: {unit}" - else: - return f"{area} cm^2" + area = round(length * width * factor**2, 2) + return f"{area} cm^2" -def test_calculate_area_signature(function_to_test) -> None: - errors = [] +def validate_area_signature(function_to_test) -> None: + """Validate signature of the full area calculation function.""" signature = inspect.signature(function_to_test) params = signature.parameters return_annotation = signature.return_annotation - if function_to_test.__doc__ is None: - errors.append("The function is missing a docstring.") - if len(params) != 3: - errors.append("The function should take three arguments.") - if ( - "length" not in params.keys() - or "width" not in params.keys() - or "unit" not in params.keys() - ): - errors.append( - "The function's parameters should be 'length', 'width' and 'unit'." - ) - if "unit" in params.keys() and not ( - params["unit"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD - and params["unit"].default == "cm" - ): - errors.append("Argument 'unit' should have a default value 'cm'.") - if any(p.annotation == inspect.Parameter.empty for p in params.values()): - errors.append("The function's parameters should have type hints.") - if return_annotation == inspect.Signature.empty: - errors.append("The function's return value is missing the type hint.") - - assert not errors, errors_to_list(errors) + assert function_to_test.__doc__ is not None, "The function is missing a docstring." + assert ( + len(params) == 3 + ), "The function should take three arguments (length, width, and unit)." + assert all( + p in params.keys() for p in ["length", "width", "unit"] + ), "The function's parameters should be 'length', 'width' and 'unit'." + assert ( + params["length"].annotation is float + ), "Parameter 'length' should be annotated as float." + assert ( + params["width"].annotation is float + ), "Parameter 'width' should be annotated as float." + assert ( + params["unit"].annotation is str + ), "Parameter 'unit' should be annotated as str." + assert ( + params["unit"].default == "cm" + ), "Parameter 'unit' should have a default value of 'cm'." + assert return_annotation is str, "The return type should be annotated as str." @pytest.mark.parametrize( "length,width,unit", [ (2.0, 3.0, "cm"), - (4.0, 5.0, "m"), - (10.0, 2.0, "mm"), - (2.0, 8.0, "yd"), - (5.0, 4.0, "ft"), - (3.0, 5.0, "in"), + (2.0, 3.0, "m"), + (2.0, 3.0, "mm"), + (2.0, 3.0, "yd"), + (2.0, 3.0, "ft"), ], ) -def test_calculate_area_result( - length: float, - width: float, - unit: str, - function_to_test, -) -> None: - errors = [] - - if unit in ("cm", "m", "mm", "yd", "ft"): - result = function_to_test(length, width, unit) - - if not isinstance(result, str): - errors.append("The function should return a string.") - if "cm^2" not in result: - errors.append("The result should be in squared centimeters (cm^2).") - if result != reference_calculate_area(length, width, unit): - errors.append("The solution is incorrect.") - else: - try: - result = function_to_test(length, width, unit) - except KeyError: - errors.append( - "The function should return an error string for unsupported units." - ) - else: - if result != f"Invalid unit: {unit}": - errors.append("The error message is incorrectly formatted.") - - assert not errors +def test_calculate_area(length, width, unit, function_to_test): + validate_area_signature(function_to_test) + result = function_to_test(length, width, unit) + expected = reference_calculate_area(length, width, unit) + assert isinstance(result, str), "Result should be a string" + assert expected == result, "Incorrect area calculation or formatting" # @@ -167,8 +239,8 @@ def test_calculate_area_result( # -def reference_summing_anything(*args: Any) -> Any: - """Reference solution for the summing_anything exercise""" +def reference_combine_anything(*args: Any) -> Any: + """Reference solution for the combine_anything exercise""" if not args: return args @@ -181,16 +253,16 @@ def reference_summing_anything(*args: Any) -> Any: @pytest.mark.parametrize( - "args,expected", + "args", [ - ((), ()), - ((1, 2, 3), 6), - (([1, 2, 3], [4, 5, 6]), [1, 2, 3, 4, 5, 6]), - (("hello", "world"), "helloworld"), + (()), + ((1, 2, 3)), + (([1, 2, 3], [4, 5, 6])), + (("hello", "world")), ], ) -def test_summing_anything(args: Any, expected: Any, function_to_test) -> None: - assert function_to_test(*args) == expected +def test_combine_anything(args: Any, function_to_test) -> None: + assert function_to_test(*args) == reference_combine_anything(*args) #