From 9240a6d9aab945fb931e09bffaad84ec15b8d428 Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou <16343312+despadam@users.noreply.github.com> Date: Fri, 15 Nov 2024 18:14:25 +0100 Subject: [PATCH] `Object-oriented`: Add simpler examples, update quiz and exercises (#249) * add 2 new examples * finish new examples * finish new examples pt2 * update new examples with docstrings and improved tests * add docstrings * finish docstrings * make sure that class methods are only using the class attributes to produce their results * test that the return value is an instance of a custom class * use closure to check for usage of class attributes * update property section and quiz * Expand docstring description of Exercise 4 * finish updating the exercises * Minor changes * Stupid metadata fix of the ipython notebook * Fixing * Replacing variable l: E741 Ambiguous variable name: `l` * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: edoardob90 Co-authored-by: Snowwpanda Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- object_oriented_programming.ipynb | 802 ++++++++++++------ tutorial/object_oriented_programming.py | 26 +- tutorial/tests/test_functions.py | 4 +- tutorial/tests/test_functions_advanced.py | 8 +- tutorial/tests/test_library_scipy.py | 4 +- .../tests/test_object_oriented_programming.py | 413 ++++++++- tutorial/tests/testsuite/testsuite.py | 2 +- tutorial/toc.py | 2 +- 8 files changed, 978 insertions(+), 283 deletions(-) diff --git a/object_oriented_programming.ipynb b/object_oriented_programming.ipynb index cd1441a1..80b3798c 100644 --- a/object_oriented_programming.ipynb +++ b/object_oriented_programming.ipynb @@ -14,20 +14,25 @@ "metadata": {}, "source": [ "# Table of Contents\n", - " - [Object-oriented Programming](#Object-oriented-Programming)\n", - " - [References](#References)\n", - " - [A `class` as a blueprint of objects](#A-class-as-a-blueprint-of-objects)\n", - " - [Properties and methods](#Properties-and-methods)\n", - " - [Python's *special methods*](#Python's-*special-methods*)\n", - " - [`__str__` and `__repr__`](#__str__-and-__repr__)\n", - " - [Exercise 1: Ice cream scoop 🌶️](#Exercise-1:-Ice-cream-scoop-🌶️)\n", - " - [Exercise 2: Ice cream bowl 🌶️🌶️](#Exercise-2:-Ice-cream-bowl-🌶️🌶️)\n", - " - [Comparison methods](#Comparison-methods)\n", - " - [Exercise 3: Ice cream shop 🌶️🌶️🌶️](#Exercise-3:-Ice-cream-shop-🌶️🌶️🌶️)\n", - " - [The `@property` keyword](#The-@property-keyword)\n", - " - [Quick glossary](#Quick-glossary)\n", - " - [Quiz](#Quiz)\n", - " - [Exercise 4: Intcode computer 🌶️🌶️🌶️🌶️](#Exercise-4:-Intcode-computer-🌶️🌶️🌶️🌶️)" + " - [References](#References)\n", + " - [A `class` as a blueprint of objects](#A-class-as-a-blueprint-of-objects)\n", + " - [Example 1](#Example-1)\n", + " - [Properties and methods](#Properties-and-methods)\n", + " - [Example 2](#Example-2)\n", + " - [Python's *special methods*](#Python's-*special-methods*)\n", + " - [`__str__` and `__repr__`](#__str__-and-__repr__)\n", + " - [Example 3](#Example-3)\n", + " - [Comparison methods](#Comparison-methods)\n", + " - [Example 4](#Example-4)\n", + " - [More comparison methods](#More-comparison-methods)\n", + " - [The `@property` keyword](#The-@property-keyword)\n", + " - [Quick glossary](#Quick-glossary)\n", + " - [Quiz](#Quiz)\n", + " - [Exercises](#Exercises)\n", + " - [Exercise 1: Ice cream scoop](#Exercise-1:-Ice-cream-scoop)\n", + " - [Exercise 2: Ice cream bowl](#Exercise-2:-Ice-cream-bowl)\n", + " - [Exercise 3: Ice cream shop](#Exercise-3:-Ice-cream-shop)\n", + " - [Exercise 4: Intcode computer 🌶️](#Exercise-4:-Intcode-computer-🌶️)" ] }, { @@ -35,7 +40,7 @@ "id": "2", "metadata": {}, "source": [ - "## References" + "# References" ] }, { @@ -55,7 +60,7 @@ "tags": [] }, "source": [ - "## A `class` as a blueprint of objects" + "# A `class` as a blueprint of objects" ] }, { @@ -63,7 +68,7 @@ "id": "5", "metadata": {}, "source": [ - "Object-oriented programming (OOP) is probably the most well-know approach to programming. Almost every programming language in some way or another supports this paradigm.\n", + "Object-oriented programming (OOP) is probably the most well-known approach to programming. Almost every programming language in some way or another supports this paradigm.\n", "\n", "The idea behind OOP is simple: instead of defining our functions in one part of the code, and the data on which those functions operate in a separate part of the code, we define them together.\n", "\n", @@ -167,16 +172,70 @@ { "cell_type": "markdown", "id": "13", + "metadata": {}, + "source": [ + "## Example 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "
\n", + "

Question

\n", + " Inside the solution function, write a class called Person which should have two attributes called first_name and last_name. Create an instance of this class, representing a person, which is being initialized by using the arguments passed in the solution function. Lastly, return the instance.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_oop_person(first_name: str, last_name: str):\n", + " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", + "\n", + " Person is a class with two attributes called 'first_name' and 'last_name'.\n", + "\n", + " Args:\n", + " first_name: a string used to initialize the Person instance\n", + " last_name: a string used to initialize the Person instance\n", + " Returns:\n", + " - an instance of Person\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "17", "metadata": { "tags": [] }, "source": [ - "## Properties and methods" + "# Properties and methods" ] }, { "cell_type": "markdown", - "id": "14", + "id": "18", "metadata": {}, "source": [ "`width` and `height` are attributes of the `Rectangle` class. But since they are just values (they are **not** functions), we call them **properties**.\n", @@ -188,7 +247,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "19", "metadata": {}, "source": [ "You'll note that we were able to retrieve the `width` and `height` attributes (properties) using a dot notation, where we specify the object we are interested in, then a dot, then the attribute we are interested in." @@ -196,7 +255,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "20", "metadata": {}, "source": [ "We can add callable attributes to our class (methods), that will also be referenced using the dot notation.\n", @@ -207,7 +266,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "21", "metadata": { "tags": [] }, @@ -228,7 +287,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "22", "metadata": { "tags": [] }, @@ -240,7 +299,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "23", "metadata": { "tags": [] }, @@ -251,7 +310,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "24", "metadata": {}, "source": [ "When we ran the above line of code, our object was `r1`, so when `area` was called, Python called the method `area` in the `Rectangle` class automatically, passing `r1` as the argument to the `self` parameter." @@ -259,17 +318,61 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "25", + "metadata": {}, + "source": [ + "## Example 2\n", + "\n", + "
\n", + "

Question

\n", + " Modify your code from Example 1 so that class Person will now have a method called full_name() that returns the string: My name is {first_name} {last_name}. Create an instance of this class, representing a person, which is being initialized by using the arguments passed in the solution function. Lastly, return the instance.\n", + "
\n", + "\n", + "
\n", + "

Hint

\n", + " Make sure to use the self. notation when you wish to access the class attributes.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_oop_fullname(first_name: str, last_name: str):\n", + " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", + "\n", + " Person is a class with two attributes called 'first_name' and 'last_name',\n", + " and a method called 'full_name' which should return the string 'My name is {first_name} {last_name}'.\n", + "\n", + " Args:\n", + " first_name: a string used to initialize the Person instance\n", + " last_name: a string used to initialize the Person instance\n", + " Returns:\n", + " - an instance of Person\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "27", "metadata": { "tags": [] }, "source": [ - "## Python's *special methods*" + "# Python's *special methods*" ] }, { "cell_type": "markdown", - "id": "22", + "id": "28", "metadata": {}, "source": [ "Special methods are methods that Python defines automatically. If you define a custom class, then you are responsible of defining the **expected behavior** of these methods. Otherwise, Python will fallback to the default, built-in definition, or it will raise an error if it doesn't know what to do.\n", @@ -279,12 +382,12 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "29", "metadata": { "tags": [] }, "source": [ - "### `__str__` and `__repr__`\n", + "## `__str__` and `__repr__`\n", "\n", "For example, we can obtain a string representation of an integer using the built-in `str` function:" ] @@ -292,7 +395,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -301,7 +404,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "31", "metadata": {}, "source": [ "What happens if we try this with our `Rectangle` object?" @@ -310,7 +413,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "32", "metadata": { "tags": [] }, @@ -321,7 +424,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "33", "metadata": {}, "source": [ "Not exactly what we might have expected. On the other hand, how is Python supposed to know how to display our rectangle as a string?\n", @@ -332,7 +435,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "34", "metadata": { "tags": [] }, @@ -355,7 +458,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "35", "metadata": {}, "source": [ "So now we could get a string from our object as follows:" @@ -364,7 +467,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "36", "metadata": { "tags": [] }, @@ -376,7 +479,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "37", "metadata": {}, "source": [ "However, using the built-in `str` function still does not work 🤔" @@ -385,7 +488,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "38", "metadata": { "tags": [] }, @@ -396,7 +499,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "39", "metadata": {}, "source": [ "This is where these special methods come in. When we call `str(r1)`, Python will first look to see if our class (`Rectangle`) has a special method called `__str__`.\n", @@ -409,7 +512,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "40", "metadata": { "tags": [] }, @@ -434,7 +537,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "41", "metadata": { "tags": [] }, @@ -446,7 +549,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "42", "metadata": { "tags": [] }, @@ -457,7 +560,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "43", "metadata": {}, "source": [ "However, in Jupyter, look what happens here:" @@ -466,7 +569,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "44", "metadata": { "tags": [] }, @@ -477,7 +580,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "45", "metadata": {}, "source": [ "As you can see we still get the default. That's because here Python is **not** converting `r1` to a string, but instead looking for a string *representation* of the object. It is looking for the [`__repr__` method](https://docs.python.org/3/reference/datamodel.html#object.__repr__), which is defined as\n", @@ -490,7 +593,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "46", "metadata": { "tags": [] }, @@ -516,112 +619,69 @@ }, { "cell_type": "markdown", - "id": "41", - "metadata": { - "tags": [] - }, + "id": "47", + "metadata": {}, "source": [ - "### Exercise 1: Ice cream scoop 🌶️" + "So, let's try to define and print `r1` again, to see what happens." ] }, { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "48", "metadata": {}, "outputs": [], "source": [ - "%reload_ext tutorial.tests.testsuite" + "r1 = Rectangle(10, 20)\n", + "r1" ] }, { "cell_type": "markdown", - "id": "43", + "id": "49", "metadata": {}, "source": [ - "Define a class `Scoop` that represents a single scoop of ice cream. Each scoop should have a **single** attribute, `flavor`, a string that you can initialize when you create the instance of `Scoop`.\n", - "\n", - "Define also a `__str__` method to return a string reprensentation of a scoop. The output should be `Ice cream scoop with flavor ''`, where `\n", "

Question

\n", - " Complete the solution function such that it creates an instance of the Scoop class for every flavor contained in the argument flavors. This function should return a list that collects the string representations of the ice cream scoops.\n", + " Modify your code from Example 2 so that class Person will now have a __str__() method instead of full_name(), with the same functionality. Then, add a __repr__() method that returns the string: Person({first_name}, {last_name}). Create an instance of this class, representing a person, which is being initialized by using the arguments passed in the solution function. Lastly, return the instance.\n", "" ] }, { "cell_type": "code", "execution_count": null, - "id": "44", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "%%ipytest\n", - "\n", - "def solution_ice_cream_scoop(flavors: tuple[str]) -> list[str]:\n", - "\n", - " class Scoop:\n", - " \"\"\"A class representing a single scoop of ice cream\"\"\"\n", - " # Write your class implementation here\n", - "\n", - " # Write your solution here\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "id": "45", - "metadata": { - "tags": [] - }, - "source": [ - "### Exercise 2: Ice cream bowl 🌶️🌶️" - ] - }, - { - "cell_type": "markdown", - "id": "46", + "id": "50", "metadata": {}, + "outputs": [], "source": [ - "Create a class `Bowl` that can hold many ice cream scoops, as many as you like. You *should use* the custom class you created in the previous exercise.\n", - "\n", - "The `Bowl` class should have a method called `add_scoops()` that accepts **variable number** of scoops.\n", - "\n", - "
\n", - "

Hint

\n", - " In the __init__ method of the Bowl class, you should define an attribute that acts as a container to hold the scoops you might want to add.\n", - "
\n", - "\n", - "
\n", - "

Question

\n", - " Complete the solution function such that it creates a bowl of scoops with every flavor contained in the argument flavors. The output of this function should be a string that reports the content of the bowl you just created.\n", - "
\n", - "\n", - "For example:\n", - "\n", - "```\n", - "Ice cream bowl with chocolate, vanilla, stracciatella scoops\n", - "```" + "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "code", "execution_count": null, - "id": "47", - "metadata": { - "tags": [] - }, + "id": "51", + "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", - " \n", - "def solution_ice_cream_bowl(flavors: tuple[str]) -> str:\n", "\n", - " class Bowl:\n", - " \"\"\"A class representing a bowl of ice cream scoops\"\"\"\n", - " # Write your class implementation here\n", + "def solution_oop_str_and_repr(first_name: str, last_name: str):\n", + " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", + "\n", + " Person is a class with two attributes called 'first_name' and 'last_name'.\n", + " Person also implements:\n", + " - The __str__() method which should return the string 'My name is {first_name} {last_name}',\n", + " - The __repr__() method which should return the string 'Person({first_name}, {last_name})'.\n", + "\n", + " Args:\n", + " first_name: a string used to initialize the Person instance\n", + " last_name: a string used to initialize the Person instance\n", + " Returns:\n", + " - an instance of Person\n", + " \"\"\"\n", "\n", " # Write your solution here\n", " pass" @@ -629,17 +689,17 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "52", "metadata": { "tags": [] }, "source": [ - "### Comparison methods" + "## Comparison methods" ] }, { "cell_type": "markdown", - "id": "49", + "id": "53", "metadata": {}, "source": [ "How about the comparison operator, such as `==`? How can we tell Python how it should compare two different rectangles?" @@ -648,7 +708,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "54", "metadata": { "tags": [] }, @@ -661,7 +721,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -670,7 +730,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "56", "metadata": {}, "source": [ "As you can see, Python does not consider `r1` and `r2` as equal (using the `==` operator). Again, how is Python supposed to know that two rectangle objects with the same height and width should be considered equal?" @@ -678,7 +738,7 @@ }, { "cell_type": "markdown", - "id": "53", + "id": "57", "metadata": {}, "source": [ "We just need to tell Python how to do it, using the special method `__eq__`. Let's see how:" @@ -687,7 +747,7 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "58", "metadata": { "tags": [] }, @@ -725,7 +785,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "59", "metadata": { "tags": [] }, @@ -737,7 +797,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "60", "metadata": {}, "source": [ "We now have two **different** objects, two instances of our `Rectangle` class. In fact, we can check that the two objects are different with the `is` operator" @@ -746,7 +806,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "61", "metadata": { "tags": [] }, @@ -757,7 +817,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "62", "metadata": {}, "source": [ "However, they are **equal** according to our `__eq__` function: rectangles are considered equal if their widths and heights are equal" @@ -766,7 +826,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "63", "metadata": { "tags": [] }, @@ -778,7 +838,7 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "64", "metadata": { "tags": [] }, @@ -790,7 +850,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "65", "metadata": { "tags": [] }, @@ -801,7 +861,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "66", "metadata": {}, "source": [ "And if we try to compare our Rectangle to a different type:" @@ -810,7 +870,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "67", "metadata": { "tags": [] }, @@ -821,7 +881,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "68", "metadata": {}, "source": [ "That's because our `Rectangle` class automatically returns `False` if we try to compare for equality an instance of `Rectangle` and any other Python object." @@ -829,7 +889,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "69", "metadata": {}, "source": [ "Here's our final class, without any `print` statement – remember, we should try to avoid side-effects when they are **not** necessary:" @@ -838,7 +898,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -868,32 +928,91 @@ }, { "cell_type": "markdown", - "id": "67", + "id": "71", "metadata": {}, "source": [ - "What about `<`, `>`, `<=`, etc.?\n", - "\n", - "Again, Python has special methods we can use to provide that functionality.\n", + "### Example 4\n", "\n", - "These are `__lt__`, `__gt__`, `__le__` methods. There are [many more](https://docs.python.org/3/reference/datamodel.html)!" + "
\n", + "

Question

\n", + " Modify your code from Example 1 so that class Person will now have an additional attribute called age. Then, add a __eq__() comparison method that makes sure that two persons are the same when they have the same first name, last name and age. Create an instance of this class, representing a person, which is being initialized by using the arguments passed in the solution function. Lastly, return the instance.\n", + "
" ] }, { - "cell_type": "markdown", - "id": "68", + "cell_type": "code", + "execution_count": null, + "id": "72", "metadata": {}, + "outputs": [], "source": [ - "
\n", - "

Note

\n", - " While we can define custom methods for the comparison operators, it doesn't mean it makes sense. In this example with the Rectangle class, the meaning of the greater/less than operations is not formally defined. We chose to compare the rectangles areas but that's completely arbitrary.\n", - "
" + "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "code", "execution_count": null, - "id": "69", - "metadata": { + "id": "73", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_oop_compare_persons(first_name: str, last_name: str, age: int):\n", + " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", + "\n", + " Person is a class with three attributes called 'first_name', 'last_name' and 'age'.\n", + " Person implements the __eq__() comparison method,\n", + " which should return True when two persons have the same first name, last name and age.\n", + "\n", + " Args:\n", + " first_name: a string used to initialize the Person instance\n", + " last_name: a string used to initialize the Person instance\n", + " age: an integer used to initialize the Person instance\n", + " Returns:\n", + " - an instance of Person\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "74", + "metadata": {}, + "source": [ + "## More comparison methods" + ] + }, + { + "cell_type": "markdown", + "id": "75", + "metadata": {}, + "source": [ + "What about `<`, `>`, `<=`, etc.?\n", + "\n", + "Again, Python has special methods we can use to provide that functionality.\n", + "\n", + "These are `__lt__`, `__gt__`, `__le__` methods. There are [many more](https://docs.python.org/3/reference/datamodel.html)!" + ] + }, + { + "cell_type": "markdown", + "id": "76", + "metadata": {}, + "source": [ + "
\n", + "

Note

\n", + " While we can define custom methods for the comparison operators, it doesn't mean it makes sense. In this example with the Rectangle class, the meaning of the greater/less than operations is not formally defined. We chose to compare the rectangles areas but that's completely arbitrary.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77", + "metadata": { "tags": [] }, "outputs": [], @@ -931,7 +1050,7 @@ { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "78", "metadata": { "tags": [] }, @@ -944,7 +1063,7 @@ { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "79", "metadata": {}, "outputs": [], "source": [ @@ -954,7 +1073,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "80", "metadata": {}, "outputs": [], "source": [ @@ -963,7 +1082,7 @@ }, { "cell_type": "markdown", - "id": "73", + "id": "81", "metadata": {}, "source": [ "What about `>`?" @@ -972,7 +1091,7 @@ { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "82", "metadata": {}, "outputs": [], "source": [ @@ -981,7 +1100,7 @@ }, { "cell_type": "markdown", - "id": "75", + "id": "83", "metadata": {}, "source": [ "How did that work? We did not define a `__gt__` method.\n", @@ -991,7 +1110,7 @@ }, { "cell_type": "markdown", - "id": "76", + "id": "84", "metadata": {}, "source": [ "Of course, `<=` is not going to magically work!" @@ -1000,7 +1119,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "85", "metadata": {}, "outputs": [], "source": [ @@ -1009,109 +1128,85 @@ }, { "cell_type": "markdown", - "id": "78", - "metadata": {}, + "id": "86", + "metadata": { + "tags": [] + }, "source": [ - "### Exercise 3: Ice cream shop 🌶️🌶️🌶️" + "# The `@property` keyword" ] }, { "cell_type": "markdown", - "id": "79", + "id": "87", "metadata": {}, "source": [ - "Create a class `Shop` that sells many ice cream flavours. \n", - "\n", - "The `Shop` class should implement the comparison methods `__eq__`, `__lt__`, `__le__`.\n", - "\n", - "
\n", - "

Hints

\n", - "
    \n", - "
  • In the __init__ method of the Shop class, you should define an attribute that acts as a container to hold the available flavours.
  • \n", - "
  • You can use __eq__ and __lt__ to define __le__.
  • \n", - "
\n", - "
\n", + "A question you might have about our `Rectangle` class is the following: why should we **call** a function to return its area? Isn't area a **property** of a rectangle?\n", "\n", - "
\n", - "

Question

\n", - " Complete the solution function so that it creates two ice cream shops. Shop 1 should sell the flavors provided by the parameter flavor_1 and Shop 2 should sell the flavors provided by the parameter flavor_2. The output of this function should be a boolean value: Does Shop 1 sell fewer or the same number of flavors as Shop 2?\n", - "
" + "Unfortunately, Python doesn't think the same way. If we try to do" ] }, { "cell_type": "code", "execution_count": null, - "id": "80", - "metadata": {}, - "outputs": [], - "source": [ - "%%ipytest\n", - "\n", - "def solution_ice_cream_shop(flavors_1: list[str], flavors_2: list[str]) -> bool:\n", - "\n", - " class Shop:\n", - " \"\"\"A class representing an ice cream shop\"\"\"\n", - " # Write your class implementation here\n", - "\n", - " # Write your solution here\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "id": "81", + "id": "88", "metadata": { "tags": [] }, + "outputs": [], "source": [ - "## The `@property` keyword" + "r1.area" ] }, { "cell_type": "markdown", - "id": "82", + "id": "89", "metadata": {}, "source": [ - "A question you might have about our `Rectangle` class is the following: why should we **call** a function to return its area? Isn't area a **property** of a rectangle?\n", - "\n", - "Unfortunately, Python doesn't think the same way. If we try to do" + "It would tell us that `r1.area` is in fact an object. It's actually a **function**" ] }, { "cell_type": "code", "execution_count": null, - "id": "83", + "id": "90", "metadata": { "tags": [] }, "outputs": [], "source": [ - "r1.area" + "print(f\"r1.area is a {type(r1.area)}. Is is a function? {callable(r1.area)}\")" ] }, { "cell_type": "markdown", - "id": "84", + "id": "91", "metadata": {}, "source": [ - "It would tell us that `r1.area` is in fact an object. It's actually a **function**" + "Which means that if you want to return the Rectangle's area, you should do the following: " ] }, { "cell_type": "code", "execution_count": null, - "id": "85", - "metadata": { - "tags": [] - }, + "id": "92", + "metadata": {}, "outputs": [], "source": [ - "print(f\"r1.area is a {type(r1.area)}. Is is a function? {callable(r1.area)}\")" + "r1.area()" ] }, { "cell_type": "markdown", - "id": "86", + "id": "93", + "metadata": {}, + "source": [ + "But what if you want to define area as one of Rectangle's properties? The only difference with the other ones, it that this property requires some additional logic in the background, in order for its value to be calculated." + ] + }, + { + "cell_type": "markdown", + "id": "94", "metadata": {}, "source": [ "Python provides you the special keyword `@property`. We are not going into the details of what this keyword does, but here's how you can use it in your classes to make them more \"user-friendly\":" @@ -1120,7 +1215,7 @@ { "cell_type": "code", "execution_count": null, - "id": "87", + "id": "95", "metadata": { "tags": [] }, @@ -1148,16 +1243,24 @@ }, { "cell_type": "markdown", - "id": "88", + "id": "96", "metadata": {}, "source": [ "You can simply add `@property` **just above** the line that defines your property. For example, the `def area()` or `def perimeter()` functions above." ] }, + { + "cell_type": "markdown", + "id": "97", + "metadata": {}, + "source": [ + "By doing so, you can now access the values of area and perimeter in the same way that you would for any other property of the class:" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "89", + "id": "98", "metadata": { "tags": [] }, @@ -1172,7 +1275,33 @@ }, { "cell_type": "markdown", - "id": "90", + "id": "99", + "metadata": {}, + "source": [ + "The only difference is that in this way we are unable to directly set the value of these properties. Their values are always internally calculated based on their defined logic, and we are only able to access them." + ] + }, + { + "cell_type": "markdown", + "id": "100", + "metadata": {}, + "source": [ + "See what happens when you try to execute the following line:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "101", + "metadata": {}, + "outputs": [], + "source": [ + "r1.area = 5" + ] + }, + { + "cell_type": "markdown", + "id": "102", "metadata": {}, "source": [ "If you want to learn more about the `@property` keyword, you can check out [this link](https://docs.python.org/3/library/functions.html#property). **Beware**, it's rather advanced stuff! If it's your first time with Python, just skip it for the time being." @@ -1180,17 +1309,17 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "103", "metadata": { "tags": [] }, "source": [ - "## Quick glossary\n" + "# Quick glossary\n" ] }, { "cell_type": "markdown", - "id": "92", + "id": "104", "metadata": {}, "source": [ "\n", @@ -1202,12 +1331,12 @@ "| Attribute | A variable that is bound to a class or instance and holds some value or reference. |\n", "| `__init__` | A special method that is automatically called when an instance of a class is created, used for initializing the instance's attributes. |\n", "| `__str__/__repr__` | Special methods used for defining a string representation of a class or instance, used for debugging or displaying information about the object. `__str__` is used for human-readable output, while `__repr__` is used for machine-readable output. |\n", - "| `@property` | A special keyword used to define a method that can be accessed like an attribute, allowing for dynamic attribute behavior. |" + "| `@property` | A special keyword used to define a method that can be accessed like an attribute, when additional logic or validation is required. |" ] }, { "cell_type": "markdown", - "id": "93", + "id": "105", "metadata": { "tags": [] }, @@ -1217,10 +1346,10 @@ }, { "cell_type": "markdown", - "id": "94", + "id": "106", "metadata": {}, "source": [ - "## Quiz\n", + "# Quiz\n", "\n", "Run the following cell to test your knowledge with a small quiz." ] @@ -1228,7 +1357,7 @@ { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "107", "metadata": {}, "outputs": [], "source": [ @@ -1239,12 +1368,187 @@ }, { "cell_type": "markdown", - "id": "96", + "id": "108", + "metadata": {}, + "source": [ + "# Exercises" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "109", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "markdown", + "id": "110", + "metadata": {}, + "source": [ + "## Exercise 1: Ice cream scoop" + ] + }, + { + "cell_type": "markdown", + "id": "111", + "metadata": {}, + "source": [ + "Define a class `Scoop` that represents a single scoop of ice cream. Each scoop should have a **single** attribute, `flavor`, a string that you can initialize when you create the instance of `Scoop`.\n", + "\n", + "Define also a `__str__` method to return a string reprensentation of a scoop. The output should be `Ice cream scoop with flavor ''`, where `\n", + "

Question

\n", + " Complete the solution function such that it creates an instance of the Scoop class for every flavor contained in the function parameter flavors. This function should return a list that collects the Scoop instances of the ice cream flavors.\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "112", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_ice_cream_scoop(flavors: tuple[str]) -> list:\n", + " \"\"\"A function that contains the definition of a class Scoop, and returns a list of Scoop instances.\n", + "\n", + " Scoop is a class with one attribute called 'flavor'.\n", + " Scoop implements the __str__() method which should return the string 'Ice cream scoop with flavor '{flavor}'\n", + "\n", + " Args:\n", + " flavors: all available ice cream flavors\n", + " Returns:\n", + " - a list containing one Scoop instance per flavor\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "113", + "metadata": {}, + "source": [ + "## Exercise 2: Ice cream bowl\n", + "\n", + "Create a class `Bowl` that can hold many ice cream scoops, as many as you like. You *should use* the custom class `Scoop` that you created in the previous exercise.\n", + "\n", + "The `Bowl` class should have a method called `add_scoops()` that accepts **variable number** of scoops.\n", + "\n", + "

Define also the __str__ method to return the reprensentation of a bowl, which should report the content of the bowl you just created. For example: Ice cream bowl with chocolate, vanilla, stracciatella scoops.

\n", + "\n", + "
\n", + "

Hint

\n", + " In the __init__ method of the Bowl class, you should define an attribute called scoops, that acts as a container to hold the scoops you might want to add.\n", + "
\n", + "\n", + "
\n", + "

Question

\n", + " Complete the solution function such that it creates a bowl of scoops with every flavor provided by the function parameter flavors. The output of this function should be the instance of the bowl you just created.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "114", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + " \n", + "def solution_ice_cream_bowl(flavors: tuple[str]):\n", + " \"\"\"A function that contains the definitions of classes Scoop and Bowl, and returns an instance of Bowl.\n", + "\n", + " Scoop is a class with one attribute called 'flavor'.\n", + "\n", + " Bowl is a class with one attribute called 'scoops' and a method called 'add_scoops' which fills the bowl's container of scoops.\n", + " Bowl also implements the __str__() method which should return the string 'Ice cream bowl with ... scoops'.\n", + "\n", + " Args:\n", + " flavors: all available ice cream flavors\n", + " Returns:\n", + " - a bowl\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "115", + "metadata": {}, + "source": [ + "## Exercise 3: Ice cream shop" + ] + }, + { + "cell_type": "markdown", + "id": "116", + "metadata": {}, + "source": [ + "Create a class `Shop` that sells many ice cream flavours. \n", + "\n", + "The `Shop` class should implement the comparison methods `__eq__`, `__lt__`, `__le__`.\n", + "\n", + "
\n", + "

Hints

\n", + "
    \n", + "
  • In the __init__ method of the Shop class, you should define an attribute called flavors, that acts as a container to hold the available flavours.
  • \n", + "
  • You can use __eq__ and __lt__ to define __le__.
  • \n", + "
  • You should just compare the amount of flavors each shop has.
  • \n", + "
\n", + "
\n", + "\n", + "
\n", + "

Question

\n", + " Complete the solution function so that it creates an ice cream shop, which should sell the flavors provided by the function parameter flavors. The output of this function should be the instance of the shop you just created.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "117", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_ice_cream_shop(flavors: list[str]):\n", + " \"\"\"A function that contains the definition of a class Shop and returns an instance of it.\n", + "\n", + " Shop is a class with one attribute called 'flavors', which acts as a container for the available ice cream flavors.\n", + " Shop also implements the __eq__() and __lt__() methods to compare the flavor containers.\n", + " It also defines the __le__() method by using the two other comparison methods.\n", + "\n", + " Args:\n", + " flavors: all available ice cream flavors of the Shop\n", + " Returns:\n", + " - a Shop instance\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "118", "metadata": { "tags": [] }, "source": [ - "## Exercise 4: Intcode computer 🌶️🌶️🌶️🌶️\n", + "## Exercise 4: Intcode computer 🌶️\n", "\n", "
\n", "

Note

\n", @@ -1255,7 +1559,7 @@ }, { "cell_type": "markdown", - "id": "97", + "id": "119", "metadata": {}, "source": [ "An **Intcode program** is a list of integers separated by commas (e.g. `1,0,0,3,99`). The first number is called \"position `0`\". Each number represents either an **opcode** or a **position**.\n", @@ -1306,7 +1610,7 @@ }, { "cell_type": "markdown", - "id": "98", + "id": "120", "metadata": {}, "source": [ "
\n", @@ -1317,7 +1621,7 @@ }, { "cell_type": "markdown", - "id": "99", + "id": "121", "metadata": {}, "source": [ "Here are the initial and final states of a few small programs:\n", @@ -1329,7 +1633,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "122", "metadata": {}, "source": [ "
\n", @@ -1345,7 +1649,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "123", "metadata": { "tags": [] }, @@ -1353,21 +1657,31 @@ "source": [ "%%ipytest \n", "def solution_intcode_computer(intcode: str) -> int:\n", - " class Computer:\n", - " \"\"\"An Intcode computer class\"\"\"\n", - " # Write your class implementation here\n", + " \"\"\"A function that contains the definition of a class Computer, and returns a single integer value.\n", + "\n", + " Computer is a class with an attribute called 'program', which is where you store the processed input as a list of integers.\n", + " Computer also has a method called 'run' which executes an intcode program.\n", + "\n", + " Intcode programs values are integers that can be a 'position' or an 'opcode' (operation code). Operation codes can be:\n", + " - 99: Immediately terminates the program\n", + " - 1: Adds values from two positions and stores result in third position\n", + " - 2: Multiplies values from two positions and stores result in third position\n", "\n", - " # Write your solution function here\n", + " For opcodes 1 and 2:\n", + " - The three integers after the opcode specify positions (not values)\n", + " - First two positions are for input values\n", + " - Third position is where the result is stored\n", + " - After processing, move forward 4 positions to next opcode\n", + "\n", + " Args:\n", + " intcode: a string of integers separated by commas\n", + " Returns:\n", + " - the value left at position 0 after executing the intcode program\n", + " \"\"\"\n", + "\n", + " # Write your solution here\n", " pass" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "102", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1386,7 +1700,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.15" } }, "nbformat": 4, diff --git a/tutorial/object_oriented_programming.py b/tutorial/object_oriented_programming.py index 8b9f1d2e..b38a31db 100644 --- a/tutorial/object_oriented_programming.py +++ b/tutorial/object_oriented_programming.py @@ -4,13 +4,13 @@ class OopQuiz(Quiz): def __init__(self, title=""): q1 = Question( - question="Based on what you learned about Python's special methods, which of the following statements is true?", + question="Based on what you learned about Python's special methods, which statement best describes the relationship between __str__ and __repr__?", options={ - "__repr__ is also used for __str__, but not vice versa.": "Correct! This statement is true.", - "__str__ is also used for __repr__, but not vice versa.": "The opposite is true.", - "__repr__ and __str__ are completely independent.": "__repr__ is also used for __str__, but not vice versa.", + "__repr__ is used as a fallback when __str__ is missing.": "Correct! When __str__ is not defined, Python will use __repr__ instead.", + "__str__ is used as a fallback when __repr__ is missing.": "Think again based on the example we saw earlier.", + "__repr__ and __str__ are independent methods with no relationship to each other.": "There is a relationship between the two methods. Which one could it be?", }, - correct_answer="__repr__ is also used for __str__, but not vice versa.", + correct_answer="__repr__ is used as a fallback when __str__ is missing.", hint="", shuffle=True, ) @@ -18,9 +18,9 @@ def __init__(self, title=""): q2 = Question( question="Based on what you learned about Python's comparison methods, which of the following statements is false?", options={ - "If we implement __gt__, Python will also use it for __lt__": "This statement is true.", - "If we implement __lt__, Python will also use it for __le__": "Correct! This statement is false.", - "If we implement __eq__, Python will also use it for __ne__": "This statement is true.", + "If we implement __gt__, Python will also use it for __lt__": "Wrong! This statement is true because Python is able to cleverly swap the comparison terms.", + "If we implement __lt__, Python will also use it for __le__": "Correct! This statement is false because Python has no knowledge of what equality could mean based just on a comparison.", + "If we implement __eq__, Python will also use it for __ne__": "Wrong! This statement is true because Python is able to cleverly negate the equality comparison.", }, correct_answer="If we implement __lt__, Python will also use it for __le__", hint="", @@ -28,13 +28,13 @@ def __init__(self, title=""): ) q3 = Question( - question="Based on what you learned about the @property keyword, which of the following statements is false?", + question="Based on what you learned about the @property keyword, which of the following statements is true?", options={ - "@property creates attributes that act like methods but can be accessed and assigned as regular attributes.": "This statement is true.", - "@property helps implement attributes that require additional logic or validation when getting or setting their values.": "This statement is true.", - "@property makes code more readable but restricts dynamic attibute behaviour.": "Correct! This statement is false.", + "@property creates attributes that act like methods, which means that they need to be called as regular methods.": "Wrong! This statement is false beacuse we access these attributes as regular ones.", + "@property helps implement attributes that require additional logic or validation when calculating their values.": "Correct! This is how you can make your classes more readable and user-friendly.", + "@property allows to get and set the values of attributes, while applying additional logic in the background.": "Wrong! This statement is false beacuse we are not allowed to directly set the values of these attributes.", }, - correct_answer="@property makes code more readable but restricts dynamic attibute behaviour.", + correct_answer="@property helps implement attributes that require additional logic or validation when calculating their values.", hint="", shuffle=True, ) diff --git a/tutorial/tests/test_functions.py b/tutorial/tests/test_functions.py index 1f49708a..c0ad1b9a 100644 --- a/tutorial/tests/test_functions.py +++ b/tutorial/tests/test_functions.py @@ -297,7 +297,9 @@ def test_password_validator2(start: int, end: int, function_to_test) -> None: # Exercise: Buckets reorganization # -prio = {l: i for i, l in enumerate(ascii_lowercase + ascii_uppercase, start=1)} +prio = { + letter: i for i, letter in enumerate(ascii_lowercase + ascii_uppercase, start=1) +} buckets_1, buckets_2 = (read_data(f"buckets_{num}.txt") for num in (1, 2)) diff --git a/tutorial/tests/test_functions_advanced.py b/tutorial/tests/test_functions_advanced.py index dd6bc4dd..371b26e1 100644 --- a/tutorial/tests/test_functions_advanced.py +++ b/tutorial/tests/test_functions_advanced.py @@ -144,6 +144,10 @@ def hello(name): def reference_once(allowed_time: int = 15) -> t.Callable: """Decorator to run a function at most once""" + class TooSoonError(RuntimeError): + def __init__(self, wait: float): + super().__init__(f"Wait another {wait:.2f} seconds") + def decorator(func: t.Callable) -> t.Callable: timer = 0.0 @@ -156,9 +160,7 @@ def wrapper(*args, **kwargs) -> t.Any: return func(*args, **kwargs) if (stop := time.perf_counter()) - timer < allowed_time: - raise RuntimeError( - f"Wait another {allowed_time - (stop - timer):.2f} seconds" - ) + raise TooSoonError(allowed_time - (stop - timer)) timer = time.perf_counter() diff --git a/tutorial/tests/test_library_scipy.py b/tutorial/tests/test_library_scipy.py index 8c34f3ac..c2c3e74f 100644 --- a/tutorial/tests/test_library_scipy.py +++ b/tutorial/tests/test_library_scipy.py @@ -39,10 +39,10 @@ def reference_lu(): # 1. TODO: define the matrix a_ref here: a_ref = np.array([[9, 3, 3], [3, 2, 2], [3, 4, 2]]) # 2. TODO: call the lu function here: - p, l, u = lu(a_ref) + p_matrix, l_matrix, u_matrix = lu(a_ref) # 3. TODO: return p, l, u matrices in this order here: - return p, l, u + return p_matrix, l_matrix, u_matrix def test_lu(function_to_test): diff --git a/tutorial/tests/test_object_oriented_programming.py b/tutorial/tests/test_object_oriented_programming.py index 4b8c461c..b30aadc9 100644 --- a/tutorial/tests/test_object_oriented_programming.py +++ b/tutorial/tests/test_object_oriented_programming.py @@ -1,13 +1,257 @@ import pathlib +import re import pytest + +class SubAssertionError(AssertionError): + def __init__(self): + super().__init__("Solution must be a proper class instance with attributes.") + + +# +# Example 1: Person +# + + +def reference_oop_person(first_name: str, last_name: str): + class Person: + """A class representing a person with first name and last name""" + + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + + return Person(first_name, last_name) + + +def validate_oop_person(solution_result): + assert not isinstance( + solution_result, (str, int, float, bool, list, dict, tuple, set) + ), "Solution must return a class instance, not a datatype." + assert ( + type(solution_result).__module__ != "builtins" + ), "Solution must return an instance of a custom class, not a built-in type." + assert ( + type(solution_result).__name__ == "Person" + ), "The class should be named 'Person'." + # Check the class attributes + try: + attrs = list(vars(solution_result)) + except TypeError: + raise SubAssertionError + assert len(attrs) == 2, "The class should have 2 attributes." + assert ( + "first_name" in attrs and "last_name" in attrs + ), "The class attributes should be 'first_name' and 'last_name'." + + +@pytest.mark.parametrize( + "first_name, last_name", + [ + ("John", "Doe"), + ], +) +def test_oop_person(first_name, last_name, function_to_test): + solution_result = function_to_test(first_name, last_name) + reference_result = reference_oop_person(first_name, last_name) + + validate_oop_person(solution_result) + assert ( + solution_result.first_name == reference_result.first_name + and solution_result.last_name == reference_result.last_name + ) + + +# +# Example 2: Person's full name +# + + +def reference_oop_fullname(first_name: str, last_name: str): + class Person: + """A class representing a person with first name and last name""" + + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + + def full_name(self) -> str: + return f"My name is {self.first_name} {self.last_name}" + + return Person(first_name, last_name) + + +def validate_oop_fullname(solution_result): + methods = [ + attr + for attr in dir(solution_result) + if callable(getattr(solution_result, attr)) and not attr.startswith("__") + ] + assert len(methods) == 1, "The class should have 1 method." + assert "full_name" in methods, "The class method should be called 'full_name'." + assert ( + solution_result.full_name.__closure__ is None + ), "The full_name() method should be using the class attributes." + + +@pytest.mark.parametrize( + "first_name, last_name", + [ + ("John", "Doe"), + ], +) +def test_oop_fullname(first_name, last_name, function_to_test): + solution_result = function_to_test(first_name, last_name) + reference_result = reference_oop_fullname(first_name, last_name) + + validate_oop_person(solution_result) + validate_oop_fullname(solution_result) + assert ( + solution_result.full_name() == reference_result.full_name() + ), "The full_name() result does not match the template 'My name is {first_name} {last_name}'." + + +# +# Example 3: Person class with __str__ and __repr__ +# + + +def reference_oop_str_and_repr(first_name: str, last_name: str): + class Person: + """A class representing a person with first name and last name""" + + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + + def __str__(self): + return f"My name is {self.first_name} {self.last_name}" + + def __repr__(self): + return f"Person({self.first_name}, {self.last_name})" + + return Person(first_name, last_name) + + +def validate_oop_str_method(solution_result): + assert hasattr( + solution_result.__str__, "__closure__" + ), "Make sure that the class is properly implementing the __str__() method." + assert ( + solution_result.__str__.__closure__ is None + ), "The __str__() method should be using the class attributes." + + +def validate_oop_repr_method(solution_result): + assert hasattr( + solution_result.__repr__, "__closure__" + ), "Make sure that the class is properly implementing the __repr__() method." + assert ( + solution_result.__repr__.__closure__ is None + ), "The __repr__() method should be using the class attributes." + + +@pytest.mark.parametrize( + "first_name, last_name", + [ + ("John", "Doe"), + ], +) +def test_oop_str_and_repr(first_name, last_name, function_to_test): + solution_result = function_to_test(first_name, last_name) + reference_result = reference_oop_str_and_repr(first_name, last_name) + + validate_oop_person(solution_result) + validate_oop_str_method(solution_result) + validate_oop_repr_method(solution_result) + + assert str(solution_result) == str( + reference_result + ), "The __str__() result does not match the template 'My name is {first_name} {last_name}'." + assert ( + solution_result.__repr__() == reference_result.__repr__() + ), "The __repr__() result does not match the template 'Person({first_name}, {last_name})'." + + +# +# Example 4: Person class with equality comparison +# + + +def reference_oop_compare_persons(first_name: str, last_name: str, age: int): + class Person: + """A class representing a person with first name, last name and age""" + + def __init__(self, first_name: str, last_name: str, age: int): + self.first_name = first_name + self.last_name = last_name + self.age = age + + def __eq__(self, other): + if isinstance(other, Person): + return (self.first_name, self.last_name, self.age) == ( + other.first_name, + other.last_name, + other.age, + ) + else: + return False + + return Person(first_name, last_name, age) + + +def validate_oop_compare_persons(solution_result): + assert not isinstance( + solution_result, (str, int, float, bool, list, dict, tuple, set) + ), "Solution must return a class instance, not a datatype." + assert ( + type(solution_result).__module__ != "builtins" + ), "Solution must return an instance of a custom class, not a built-in type." + assert ( + type(solution_result).__name__ == "Person" + ), "The class should be named 'Person'." + # Check the class attributes + try: + attrs = list(vars(solution_result)) + except TypeError: + raise SubAssertionError + assert len(attrs) == 3, "The class should have 3 attributes." + assert ( + "first_name" in attrs and "last_name" in attrs and "age" in attrs + ), "The class attributes should be 'first_name', 'last_name' and 'age'." + + +@pytest.mark.parametrize( + "first_name_a, last_name_a, age_a, first_name_b, last_name_b, age_b", + [ + ("Jane", "Doe", 30, "John", "Doe", 25), + ("John", "Smith", 25, "John", "Doe", 25), + ("John", "Doe", 20, "John", "Doe", 25), + ("John", "Doe", 25, "John", "Doe", 25), + ], +) +def test_oop_compare_persons( + first_name_a, last_name_a, age_a, first_name_b, last_name_b, age_b, function_to_test +): + solution_result_a = function_to_test(first_name_a, last_name_a, age_a) + reference_result_a = reference_oop_compare_persons(first_name_a, last_name_a, age_a) + + solution_result_b = function_to_test(first_name_b, last_name_b, age_b) + reference_result_b = reference_oop_compare_persons(first_name_b, last_name_b, age_b) + + validate_oop_compare_persons(solution_result_a) + assert (solution_result_a == solution_result_b) == ( + reference_result_a == reference_result_b + ), "Comparison failed." + + # # Exercise 1: Ice cream scoop # -def reference_ice_cream_scoop(flavors: tuple[str]) -> list[str]: +def reference_ice_cream_scoop(flavors: tuple[str]) -> list: class Scoop: """A class representing a single scoop of ice cream""" @@ -17,7 +261,37 @@ def __init__(self, flavor: str): def __str__(self): return f"Ice cream scoop with flavor '{self.flavor}'" - return [str(Scoop(flavor)) for flavor in flavors] + return [Scoop(flavor) for flavor in flavors] + + +def validate_ice_cream_scoop(solution_result): + assert not isinstance( + solution_result, (str, int, float, bool, list, dict, tuple, set) + ), "The returned list must contain class instances, not datatypes." + assert ( + type(solution_result).__module__ != "builtins" + ), "The returned list must contain instances of a custom class, not a built-in type." + assert ( + type(solution_result).__name__ == "Scoop" + ), "The class should be named 'Scoop'." + # Check the class attributes + try: + attrs = list(vars(solution_result)) + except TypeError: + raise SubAssertionError + assert len(attrs) == 1, "The class should have 1 attribute." + assert "flavor" in attrs, "The class attribute should be 'flavor'." + assert hasattr( + solution_result.__str__, "__closure__" + ), "Make sure that the class is properly implementing the __str__() method." + assert ( + solution_result.__str__.__closure__ is None + ), "The __str__() method should be using the class attributes." + # check the __str__ result + pattern = r"^Ice cream scoop with flavor '(.+)'$" + assert re.match( + pattern, str(solution_result) + ), "The __str__() result does not match the template: Ice cream scoop with flavor '{flavor}'" @pytest.mark.parametrize( @@ -27,7 +301,20 @@ def __str__(self): ], ) def test_ice_cream_scoop(flavors, function_to_test) -> None: - assert function_to_test(flavors) == reference_ice_cream_scoop(flavors) + solution_result = function_to_test(flavors) + reference_result = reference_ice_cream_scoop(flavors) + + assert isinstance(solution_result, list), "Solution must return a list." + assert len(solution_result) == len( + flavors + ), "The returned list must contain as many scoops as the provided flavors." + + for res in solution_result: + validate_ice_cream_scoop(res) + + solution_str_repr = [str(scoop) for scoop in solution_result] + reference_str_repr = [str(scoop) for scoop in reference_result] + assert solution_str_repr == reference_str_repr # @@ -35,7 +322,7 @@ def test_ice_cream_scoop(flavors, function_to_test) -> None: # -def reference_ice_cream_bowl(flavors: tuple[str]) -> str: +def reference_ice_cream_bowl(flavors: tuple[str]): class Scoop: """A class representing a single scoop of ice cream""" @@ -63,7 +350,56 @@ def __str__(self): bowl = Bowl() scoops = [Scoop(flavor) for flavor in flavors] bowl.add_scoops(*scoops) - return str(bowl) + return bowl + + +def validate_ice_cream_bowl(solution_result): + assert not isinstance( + solution_result, (str, int, float, bool, list, dict, tuple, set) + ), "Solution must return a class instance, not a datatype." + assert ( + type(solution_result).__module__ != "builtins" + ), "Solution must return an instance of a custom class, not a built-in type." + assert type(solution_result).__name__ == "Bowl", "The class should be named 'Bowl'." + # Check the class methods + assert hasattr( + solution_result.__str__, "__closure__" + ), "Make sure that the Bowl class is properly implementing the __str__() method." + assert ( + solution_result.__str__.__closure__ is None + ), "The __str__() method should be using the class attributes." + methods = [ + attr + for attr in dir(solution_result) + if callable(getattr(solution_result, attr)) and not attr.startswith("__") + ] + assert len(methods) == 1, "The class should have 1 custom method." + assert "add_scoops" in methods, "The class method should be called 'add_scoops'." + # Check the class attributes + try: + attrs = list(vars(solution_result)) + except TypeError: + raise SubAssertionError + assert len(attrs) == 1, "The class should have 1 attribute." + assert "scoops" in attrs, "The class attribute should be 'scoops'." + assert isinstance( + solution_result.scoops, (list, set, tuple) + ), "The class attribute 'scoops' should be a datatype that acts as a container." + for scoop in solution_result.scoops: + assert not isinstance( + scoop, (str, int, float, bool, list, dict, tuple, set) + ), "The 'scoops' container must contain class instances, not datatypes." + assert ( + type(scoop).__module__ != "builtins" + ), "The 'scoops' container must contain instances of a custom class, not a built-in type." + assert ( + type(scoop).__name__ == "Scoop" + ), "The 'scoops' container must contain instances of 'Scoop'." + # check the __str__ result + pattern = r"^Ice cream bowl with ([a-zA-Z\s]+)(?:, ([a-zA-Z\s]+))* scoops$" + assert re.match( + pattern, str(solution_result) + ), "The __str__() result does not match the template: Ice cream bowl with ... scoops" @pytest.mark.parametrize( @@ -73,7 +409,11 @@ def __str__(self): ], ) def test_ice_cream_bowl(flavors, function_to_test) -> None: - assert function_to_test(flavors) == reference_ice_cream_bowl(flavors) + solution_result = function_to_test(flavors) + reference_result = reference_ice_cream_bowl(flavors) + + validate_ice_cream_bowl(solution_result) + assert str(solution_result) == str(reference_result) # @@ -81,7 +421,7 @@ def test_ice_cream_bowl(flavors, function_to_test) -> None: # -def reference_ice_cream_shop(flavors_1: list[str], flavors_2: list[str]) -> bool: +def reference_ice_cream_shop(flavors: list[str]): class Shop: """A class representing an ice cream shop""" @@ -108,23 +448,58 @@ def __le__(self, other): return self < other or self == other return False - shop_1 = Shop(flavors_1) - shop_2 = Shop(flavors_2) - return shop_1 <= shop_2 + return Shop(flavors) + + +def validate_ice_cream_shop(solution_result): + assert not isinstance( + solution_result, (str, int, float, bool, list, dict, tuple, set) + ), "Solution must return a class instance, not a datatype." + assert ( + type(solution_result).__module__ != "builtins" + ), "Solution must return an instance of a custom class, not a built-in type." + assert type(solution_result).__name__ == "Shop", "The class should be named 'Shop'." + # Check the class methods + assert hasattr( + solution_result.__eq__, "__closure__" + ), "Make sure that the class is properly implementing the __eq__() method." + assert hasattr( + solution_result.__lt__, "__closure__" + ), "Make sure that the class is properly implementing the __lt__() method." + assert hasattr( + solution_result.__le__, "__closure__" + ), "Make sure that the class is properly implementing the __le__() method." + # Check the class attributes + try: + attrs = list(vars(solution_result)) + except TypeError: + raise SubAssertionError + assert len(attrs) == 1, "The class should have 1 attribute." + assert "flavors" in attrs, "The class attribute should be 'flavors'." + assert isinstance( + solution_result.flavors, (list, set, tuple) + ), "The class attribute 'flavors' should be a datatype that acts as a container." @pytest.mark.parametrize( - "flavors_1, flavors_2", + "flavors_a, flavors_b", [ (["chocolate", "vanilla", "stracciatella"], ["caramel", "strawberry", "mango"]), (["vanilla", "stracciatella"], ["chocolate", "vanilla", "mango"]), (["vanilla", "mango"], ["chocolate"]), ], ) -def test_ice_cream_shop(flavors_1, flavors_2, function_to_test) -> None: - assert function_to_test(flavors_1, flavors_2) == reference_ice_cream_shop( - flavors_1, flavors_2 - ) +def test_ice_cream_shop(flavors_a, flavors_b, function_to_test) -> None: + solution_result_a = function_to_test(flavors_a) + reference_result_a = reference_ice_cream_shop(flavors_a) + + solution_result_b = function_to_test(flavors_b) + reference_result_b = reference_ice_cream_shop(flavors_b) + + validate_ice_cream_shop(solution_result_a) + assert (solution_result_a <= solution_result_b) == ( + reference_result_a <= reference_result_b + ), "Comparison failed." # @@ -149,8 +524,7 @@ class Computer: """An Intcode computer class""" def __init__(self, program: str): - self.program = [int(c.strip()) for c in program.split(",")] - self._backup = self.program[:] + self.program = [int(code.strip()) for code in program.split(",")] def run(self, pos=0): while True: @@ -176,4 +550,7 @@ def run(self, pos=0): prepare_params(), ) def test_intcode_computer(intcode: str, function_to_test) -> None: - assert function_to_test(intcode) == reference_intcode_computer(intcode) + solution_result = function_to_test(intcode) + reference_result = reference_intcode_computer(intcode) + + assert solution_result == reference_result diff --git a/tutorial/tests/testsuite/testsuite.py b/tutorial/tests/testsuite/testsuite.py index 4cccc262..30c12b57 100644 --- a/tutorial/tests/testsuite/testsuite.py +++ b/tutorial/tests/testsuite/testsuite.py @@ -273,7 +273,7 @@ def ipytest(self, line: str, cell: str): if not ( module_file := pathlib.Path(f"tutorial/tests/test_{self.module_name}.py") ).exists(): - raise FileNotFoundError(f"Module file '{module_file}' does not exist") + raise FileNotFoundError(module_file) self.module_file = module_file diff --git a/tutorial/toc.py b/tutorial/toc.py index 2a8814a0..627fe7a2 100755 --- a/tutorial/toc.py +++ b/tutorial/toc.py @@ -82,7 +82,7 @@ def main(): args = parser.parse_args() if not (input_nb := pathlib.Path(args.notebook)).exists(): - raise FileNotFoundError(f"Notebook '{input_nb}' does not exist.") + raise FileNotFoundError(input_nb) if args.output is None: output_nb = input_nb.with_suffix(".toc.ipynb")