From d52cbe3510b0f1aedd721d9521402c03c8b4946b Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou Date: Wed, 7 May 2025 13:59:54 +0200 Subject: [PATCH 1/7] add recap, one quiz per section, reorg exercises, update toc --- 13_object_oriented_programming_advanced.ipynb | 608 +++++++++++------- .../object_oriented_programming_advanced.py | 223 ++++++- 2 files changed, 581 insertions(+), 250 deletions(-) diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index e60022bf..826f73db 100644 --- a/13_object_oriented_programming_advanced.ipynb +++ b/13_object_oriented_programming_advanced.ipynb @@ -14,33 +14,37 @@ "metadata": {}, "source": [ "## Table of Contents\n", - "\n", - "- [References](#References)\n", - "- [Inheritance](#Inheritance)\n", - " - [Single Inheritance](#Single-Inheritance)\n", - " - [Multiple Inheritance](#Multiple-Inheritance)\n", - " - [Composition](#Composition)\n", - " - [super()](#super())\n", - "- [Abstract Classes](#Abstract-Classes)\n", - "- [Decorators](#Decorators)\n", - " - [@classmethod](#@classmethod)\n", - " - [@staticmethod](#@staticmethod)\n", - " - [@property](#@property)\n", - " - [Setters & Getters](#Setters-&-Getters)\n", - "- [Encapsulation](#Encapsulation)\n", - " - [Public](#Public)\n", - " - [Private](#Private)\n", - " - [Protected](#Protected)\n", - "- [How to write better classes](#How-to-write-better-classes)\n", - " - [Using dataclasses](#Using-dataclasses)\n", - " - [Using attrs](#Using-attrs)\n", - "- [Quiz](#Quiz)\n", - "- [Exercises](#Exercises)\n", - " - [Child Eye Color](#Child-Eye-Color)\n", - " - [Store Inventory](#Store-Inventory)\n", - " - [Music Streaming Service](#Music-Streaming-Service)\n", - " - [Banking System](#Banking-System)\n", - " - [The N-Body Problem](#The-N-body-problem)" + " - [References](#References)\n", + " - [Recap](#Recap)\n", + " - [Inheritance](#Inheritance)\n", + " - [Single Inheritance](#Single-Inheritance)\n", + " - [Multiple Inheritance](#Multiple-Inheritance)\n", + " - [Composition](#Composition)\n", + " - [`super()`](#super())\n", + " - [Quiz on Inheritance](#Quiz-on-Inheritance)\n", + " - [Exercise: Child Eye Color](#Exercise:-Child-Eye-Color)\n", + " - [Abstract Classes](#Abstract-Classes)\n", + " - [Quiz on Abstraction](#Quiz-on-Abstraction)\n", + " - [Exercise: Banking System](#Exercise:-Banking-System)\n", + " - [Decorators](#Decorators)\n", + " - [@classmethod](#@classmethod)\n", + " - [@staticmethod](#@staticmethod)\n", + " - [@property](#@property)\n", + " - [Setters & Getters](#Setters-&-Getters)\n", + " - [Quiz on Decorators](#Quiz-on-Decorators)\n", + " - [Encapsulation](#Encapsulation)\n", + " - [Public](#Public)\n", + " - [Private](#Private)\n", + " - [Protected](#Protected)\n", + " - [Quiz on Encapsulation](#Quiz-on-Encapsulation)\n", + " - [How to write better classes](#How-to-write-better-classes)\n", + " - [Using dataclasses](#Using-dataclasses)\n", + " - [Using attrs](#Using-attrs)\n", + " - [Quiz on `attrs` and `dataclasses`](#Quiz-on-attrs-and-dataclasses)\n", + " - [Exercises](#Exercises)\n", + " - [Store Inventory](#Store-Inventory)\n", + " - [Music Streaming Service](#Music-Streaming-Service)\n", + " - [The N-body problem](#The-N-body-problem)" ] }, { @@ -65,6 +69,21 @@ "cell_type": "markdown", "id": "4", "metadata": {}, + "source": [ + "## Recap\n", + "\n", + "In the [Object-oriented Programming](./05_object_oriented_programming.ipynb) notebook, we explored the foundational concepts of OOP, including how to define classes, create instances, and work with attributes and methods.\n", + "We covered Python's special methods like `__init__` for initialization, `__str__` and `__repr__` for string representation, and `__eq__` for equality checks.\n", + "Additionally, we introduced the `@property` decorator to define computed attributes, enabling cleaner access to derived values, and discussed encapsulation principles using public, private, and protected access modifiers.\n", + "These concepts laid the groundwork for understanding how to structure and organize code, emphasizing the importance of modularity in software design.\n", + "\n", + "In the following notebook, we are going to explore the more advanced concepts of OOP, like inheritance, composition, abstraction, and much more." + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, "source": [ "## Inheritance\n", "\n", @@ -74,7 +93,7 @@ }, { "cell_type": "markdown", - "id": "5", + "id": "6", "metadata": {}, "source": [ "### Single Inheritance\n", @@ -88,7 +107,7 @@ }, { "cell_type": "markdown", - "id": "6", + "id": "7", "metadata": {}, "source": [ "Let's create a simple example in Python.\n", @@ -98,7 +117,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -112,7 +131,7 @@ }, { "cell_type": "markdown", - "id": "8", + "id": "9", "metadata": {}, "source": [ "From your base class, you can define as many derived classes as you'd like.\n", @@ -128,7 +147,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -150,7 +169,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "11", "metadata": {}, "source": [ "Now we can create instances of `Dog` and `Cat`: " @@ -159,7 +178,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -169,7 +188,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "13", "metadata": {}, "source": [ "Let's see what happens when you call each instance's methods:" @@ -178,7 +197,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -189,7 +208,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -199,7 +218,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "Of course, you can always use the base class `Animal` **as is**.\n", @@ -209,7 +228,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -219,7 +238,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "### Multiple Inheritance\n", @@ -232,7 +251,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "19", "metadata": {}, "source": [ "To do that, we first define a new class." @@ -241,7 +260,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -255,7 +274,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "21", "metadata": {}, "source": [ "Then, we create a derived class that inherits from two base classes. Notice that:\n", @@ -266,7 +285,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -281,7 +300,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "23", "metadata": {}, "source": [ "We can also call all methods inherited from each parent." @@ -290,7 +309,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -303,7 +322,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "While multiple inheritance can be powerful, it can also lead to complexities and potential conflicts, so it should only be used when really needed.\n", @@ -313,7 +332,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "### Composition\n", @@ -328,7 +347,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "27", "metadata": {}, "source": [ "Let's first create the classes for the `Engine` and the `Wheels` of a vehicle:" @@ -337,7 +356,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -362,7 +381,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "29", "metadata": {}, "source": [ "Then we can create a car, which has one engine and four wheels.\n", @@ -374,7 +393,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -397,7 +416,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -409,7 +428,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "### `super()`\n", @@ -422,7 +441,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "33", "metadata": {}, "source": [ "Let's re-write class `Dog`, which is a subclass of `Animal`.\n", @@ -432,7 +451,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -450,7 +469,79 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "35", + "metadata": {}, + "source": [ + "### Quiz on Inheritance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "oopa.OopAdvancedInheritance()" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "### Exercise: Child Eye Color\n", + "\n", + "In this exercise, we will implement the following simplified theory on how to predict a child's eye color, based on the eye color of its parents.\n", + "\n", + "We assume that the only existing eye colors are blue and brown. We also assume the following rules:\n", + "- If both parents have brown eyes, their child will also have brown eyes.\n", + "- If both parents have blue eyes, their child will also have blue eyes.\n", + "- If one parent has brown eyes and the other one has blue eyes, the dominant color will be brown.\n", + "\n", + "
\n", + "

Question

\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "import random\n", + "\n", + "colors = [\"blue\", \"brown\"]\n", + "mother_eye_color = random.choice(colors)\n", + "father_eye_color = random.choice(colors)\n", + "\n", + "def solution_child_eye_color(mother_eye_color: str, father_eye_color: str) -> str:\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "40", "metadata": {}, "source": [ "## Abstract Classes\n", @@ -467,7 +558,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "41", "metadata": {}, "source": [ "We first create an abstract class which inherits from `ABC`:" @@ -476,7 +567,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -494,7 +585,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "43", "metadata": {}, "source": [ "Careful, you **cannot** create an instance of an abstract class!\n", @@ -504,7 +595,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -513,7 +604,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "45", "metadata": {}, "source": [ "Let's create two concrete subclasses of `Shape`:" @@ -522,7 +613,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -551,7 +642,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "47", "metadata": {}, "source": [ "Now we are allowed to create instances of the subclasses and also call their methods:" @@ -560,7 +651,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -575,7 +666,108 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "49", + "metadata": {}, + "source": [ + "### Quiz on Abstraction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "oopa.OopAdvancedAbstractClasses()" + ] + }, + { + "cell_type": "markdown", + "id": "51", + "metadata": {}, + "source": [ + "### Exercise: Banking System\n", + "\n", + "In this exercise, we will implement a very simple banking system where there are two different types of accounts: **Salary Accounts** and **Savings Accounts**.\n", + "\n", + "We assume the following Classes:\n", + "\n", + "**Account**:\n", + "\n", + "An abstract base class representing a generic bank account with attributes `account_number` and `balance` and abstract methods `credit()` and `get_balance()`.\n", + "It should also contain the method `debit()`, which, if funds are sufficient, should subtract a given amount (parameter) from the account balance.\n", + "Method `debit()` should be common for all derived classes.\n", + "\n", + "**SalaryAccount**:\n", + "\n", + "A derived class representing a salary account that contains an additional attribute for `tax_rate` and **overrides** methods `credit()` and `get_balance()`.\n", + "Method `credit()` should set the balance as the `gross_salary` after applying the `tax_rate` to it.\n", + "Method `get_balance()` should simply return the account balance.\n", + "\n", + "**SavingsAccount**:\n", + "\n", + "A derived class representing a savings account that contains additional attributes for `interest_rate` and the account's `creation_year`, plus **overrides** methods `credit()` and `get_balance()`.\n", + "Method `credit()` should simply add the given amount to the current balance.\n", + "Method `get_balance()` should return the account balance plus interest, based on the `interest_rate` and the `years_passed` from the account's creation.\n", + "\n", + "
\n", + "

Question

\n", + " Using abstraction in Python, create a banking system based on the entities mentioned above.\n", + " \n", + "\n", + "The solution function should return the balance of the Savings Account after the given number of years.\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "from abc import ABC, abstractmethod\n", + "from datetime import datetime\n", + "\n", + "def solution_banking_system(tax_rate: float, interest_rate: float, gross_salary: int, savings_precentage: float, years_passed: int) -> float:\n", + " # Write your solution here\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "54", "metadata": {}, "source": [ "## Decorators\n", @@ -588,7 +780,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "55", "metadata": {}, "source": [ "### @classmethod\n", @@ -602,7 +794,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "56", "metadata": {}, "outputs": [], "source": [ @@ -641,7 +833,7 @@ }, { "cell_type": "markdown", - "id": "46", + "id": "57", "metadata": {}, "source": [ "### @staticmethod\n", @@ -654,7 +846,7 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "58", "metadata": {}, "source": [ "As seen in the example below, static methods have limited use, because they don't have access neither to the class attributes nor to any instance of the class.\n", @@ -667,7 +859,7 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -687,7 +879,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "60", "metadata": {}, "source": [ "### @property\n", @@ -701,7 +893,7 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "61", "metadata": {}, "source": [ "In this simple example, we create a class `Circle`, which has a radius and an area.\n", @@ -711,7 +903,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "62", "metadata": {}, "outputs": [], "source": [ @@ -733,7 +925,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "63", "metadata": {}, "source": [ "We can access the `area` property, just like any other class attribute.\n", @@ -743,7 +935,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -752,7 +944,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "65", "metadata": {}, "source": [ "### Setters & Getters\n", @@ -770,7 +962,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -796,7 +988,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "67", "metadata": {}, "source": [ "Create an instance and use the getter:" @@ -805,7 +997,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -815,7 +1007,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "69", "metadata": {}, "source": [ "Update the radius using the setter:" @@ -824,7 +1016,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -835,7 +1027,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "71", "metadata": {}, "source": [ "What happens when we enter an invalid value?" @@ -844,7 +1036,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "72", "metadata": {}, "outputs": [], "source": [ @@ -853,7 +1045,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "73", "metadata": {}, "source": [ "Finally let's use the deleter:" @@ -862,7 +1054,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "74", "metadata": {}, "outputs": [], "source": [ @@ -871,7 +1063,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "75", "metadata": {}, "source": [ "We are no longer able to access the deleted attribute:" @@ -880,7 +1072,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "76", "metadata": {}, "outputs": [], "source": [ @@ -889,7 +1081,26 @@ }, { "cell_type": "markdown", - "id": "66", + "id": "77", + "metadata": {}, + "source": [ + "### Quiz on Decorators" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78", + "metadata": {}, + "outputs": [], + "source": [ + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "oopa.OopAdvancedDecorators()" + ] + }, + { + "cell_type": "markdown", + "id": "79", "metadata": {}, "source": [ "## Encapsulation\n", @@ -923,7 +1134,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "80", "metadata": {}, "outputs": [], "source": [ @@ -941,7 +1152,7 @@ }, { "cell_type": "markdown", - "id": "68", + "id": "81", "metadata": {}, "source": [ "### Protected\n", @@ -954,7 +1165,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69", + "id": "82", "metadata": {}, "outputs": [], "source": [ @@ -972,7 +1183,26 @@ }, { "cell_type": "markdown", - "id": "70", + "id": "83", + "metadata": {}, + "source": [ + "### Quiz on Encapsulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84", + "metadata": {}, + "outputs": [], + "source": [ + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "oopa.OopAdvancedEncapsulation()" + ] + }, + { + "cell_type": "markdown", + "id": "85", "metadata": {}, "source": [ "## How to write better classes" @@ -980,7 +1210,7 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "86", "metadata": {}, "source": [ "Lastly, we would like to offer some tips & tricks that will help you write your code in a cleaner and easier-to-maintain way." @@ -988,7 +1218,7 @@ }, { "cell_type": "markdown", - "id": "72", + "id": "87", "metadata": {}, "source": [ "### Using dataclasses\n", @@ -999,7 +1229,7 @@ { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "88", "metadata": {}, "outputs": [], "source": [ @@ -1012,7 +1242,7 @@ }, { "cell_type": "markdown", - "id": "74", + "id": "89", "metadata": {}, "source": [ "A simpler way, however, would be to import `dataclass` from the `dataclasses` module.\n", @@ -1025,7 +1255,7 @@ { "cell_type": "code", "execution_count": null, - "id": "75", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -1040,7 +1270,7 @@ }, { "cell_type": "markdown", - "id": "76", + "id": "91", "metadata": {}, "source": [ "Now, with the use of these auto generated methods, we can create an instance of the class and print a representation of the object, without any additional code.\n", @@ -1050,7 +1280,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "92", "metadata": {}, "outputs": [], "source": [ @@ -1064,7 +1294,7 @@ }, { "cell_type": "markdown", - "id": "78", + "id": "93", "metadata": {}, "source": [ "### Using attrs\n", @@ -1077,7 +1307,7 @@ }, { "cell_type": "markdown", - "id": "79", + "id": "94", "metadata": {}, "source": [ "Let's rewrite the previous example, this time using `attrs`.\n", @@ -1087,7 +1317,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80", + "id": "95", "metadata": {}, "outputs": [], "source": [ @@ -1109,7 +1339,7 @@ }, { "cell_type": "markdown", - "id": "81", + "id": "96", "metadata": {}, "source": [ "However, `attrs` also provides **validators**.\n", @@ -1121,7 +1351,7 @@ { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "97", "metadata": {}, "outputs": [], "source": [ @@ -1143,29 +1373,26 @@ }, { "cell_type": "markdown", - "id": "83", + "id": "98", "metadata": {}, "source": [ - "## Quiz\n", - "\n", - "Run the following cell to test your knowledge with a small quiz." + "### Quiz on `attrs` and `dataclasses`" ] }, { "cell_type": "code", "execution_count": null, - "id": "84", + "id": "99", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", - "\n", - "oopa.OopAdvanced()" + "oopa.OopAdvancedAttrsDataclasses()" ] }, { "cell_type": "markdown", - "id": "85", + "id": "100", "metadata": {}, "source": [ "## Exercises" @@ -1174,7 +1401,7 @@ { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1183,50 +1410,7 @@ }, { "cell_type": "markdown", - "id": "87", - "metadata": {}, - "source": [ - "### Child Eye Color\n", - "\n", - "In this exercise, we will implement the following simplified theory on how to predict a child's eye color, based on the eye color of its parents.\n", - "\n", - "We assume that the only existing eye colors are blue and brown. We also assume the following rules:\n", - "- If both parents have brown eyes, their child will also have brown eyes.\n", - "- If both parents have blue eyes, their child will also have blue eyes.\n", - "- If one parent has brown eyes and the other one has blue eyes, the dominant color will be brown.\n", - "\n", - "
\n", - "

Question

\n", - " \n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "88", - "metadata": {}, - "outputs": [], - "source": [ - "%%ipytest\n", - "import random\n", - "\n", - "colors = [\"blue\", \"brown\"]\n", - "mother_eye_color = random.choice(colors)\n", - "father_eye_color = random.choice(colors)\n", - "\n", - "def solution_child_eye_color(mother_eye_color: str, father_eye_color: str) -> str:\n", - " # Write your solution here\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "id": "89", + "id": "102", "metadata": {}, "source": [ "### Store Inventory\n", @@ -1265,7 +1449,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1296,7 +1480,7 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "104", "metadata": {}, "source": [ "### Music Streaming Service\n", @@ -1324,7 +1508,7 @@ { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "105", "metadata": {}, "outputs": [], "source": [ @@ -1355,79 +1539,7 @@ }, { "cell_type": "markdown", - "id": "93", - "metadata": {}, - "source": [ - "### Banking System\n", - "\n", - "In this exercise, we will implement a very simple banking system where there are two different types of accounts: **Salary Accounts** and **Savings Accounts**.\n", - "\n", - "We assume the following Classes:\n", - "\n", - "**Account**:\n", - "\n", - "An abstract base class representing a generic bank account with attributes `account_number` and `balance` and abstract methods `credit()` and `get_balance()`.\n", - "It should also contain the method `debit()`, which, if funds are sufficient, should subtract a given amount (parameter) from the account balance.\n", - "Method `debit()` should be common for all derived classes.\n", - "\n", - "**SalaryAccount**:\n", - "\n", - "A derived class representing a salary account that contains an additional attribute for `tax_rate` and **overrides** methods `credit()` and `get_balance()`.\n", - "Method `credit()` should set the balance as the `gross_salary` after applying the `tax_rate` to it.\n", - "Method `get_balance()` should simply return the account balance.\n", - "\n", - "**SavingsAccount**:\n", - "\n", - "A derived class representing a savings account that contains additional attributes for `interest_rate` and the account's `creation_year`, plus **overrides** methods `credit()` and `get_balance()`.\n", - "Method `credit()` should simply add the given amount to the current balance.\n", - "Method `get_balance()` should return the account balance plus interest, based on the `interest_rate` and the `years_passed` from the account's creation.\n", - "\n", - "
\n", - "

Question

\n", - " Using abstraction in Python, create a banking system based on the entities mentioned above.\n", - " \n", - "\n", - "The solution function should return the balance of the Savings Account after the given number of years.\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "94", - "metadata": {}, - "outputs": [], - "source": [ - "%%ipytest\n", - "from abc import ABC, abstractmethod\n", - "from datetime import datetime\n", - "\n", - "def solution_banking_system(tax_rate: float, interest_rate: float, gross_salary: int, savings_precentage: float, years_passed: int) -> float:\n", - " # Write your solution here\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "id": "95", + "id": "106", "metadata": {}, "source": [ "### The N-body problem" @@ -1435,7 +1547,7 @@ }, { "cell_type": "markdown", - "id": "96", + "id": "107", "metadata": {}, "source": [ "On a boring and rainy Sunday afternoon, you decide that you want to attempt writing a Python program that simulates the orbits of Jupiter's moons. To start with, you decide to focus your efforts on tracking just **four** of the largest moons: Io, Europa, Ganymede, and Callisto.\n", @@ -1456,7 +1568,7 @@ }, { "cell_type": "markdown", - "id": "97", + "id": "108", "metadata": {}, "source": [ "
\n", @@ -1468,7 +1580,7 @@ { "cell_type": "code", "execution_count": null, - "id": "98", + "id": "109", "metadata": { "tags": [] }, @@ -1481,7 +1593,7 @@ }, { "cell_type": "markdown", - "id": "99", + "id": "110", "metadata": {}, "source": [ "
\n", @@ -1492,7 +1604,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "111", "metadata": {}, "source": [ "Each of the strings output by your solution function below should be something like\n", @@ -1506,7 +1618,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "112", "metadata": { "tags": [] }, @@ -1520,7 +1632,7 @@ }, { "cell_type": "markdown", - "id": "102", + "id": "113", "metadata": {}, "source": [ "---\n", @@ -1544,7 +1656,7 @@ }, { "cell_type": "markdown", - "id": "103", + "id": "114", "metadata": {}, "source": [ "To have a complete account of the moons' orbits, you need to compute the **total energy of the system**. The total energy for a single moon is its **potential energy** multiplied by its **kinetic energy**.\n", @@ -1586,7 +1698,7 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "115", "metadata": {}, "source": [ "
\n", @@ -1598,7 +1710,7 @@ { "cell_type": "code", "execution_count": null, - "id": "105", + "id": "116", "metadata": { "tags": [] }, @@ -1611,7 +1723,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "117", "metadata": {}, "source": [ "
\n", @@ -1627,7 +1739,7 @@ }, { "cell_type": "markdown", - "id": "107", + "id": "118", "metadata": {}, "source": [ "
\n", @@ -1639,7 +1751,7 @@ { "cell_type": "code", "execution_count": null, - "id": "108", + "id": "119", "metadata": { "tags": [] }, @@ -1669,7 +1781,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/tutorial/quiz/object_oriented_programming_advanced.py b/tutorial/quiz/object_oriented_programming_advanced.py index 3aa0d478..1f77d9d6 100644 --- a/tutorial/quiz/object_oriented_programming_advanced.py +++ b/tutorial/quiz/object_oriented_programming_advanced.py @@ -1,9 +1,142 @@ from .common import Question, Quiz -class OopAdvanced(Quiz): +class OopAdvancedInheritance(Quiz): def __init__(self, title=""): q1 = Question( + question="Which special method is used for object initialization in Python?", + options={ + "__init__": "Correct! The `__init__` method is called when an object is created and is used to initialize the object.", + "__repr__": "The `__repr__` method is used to provide an unambiguous string representation of an object.", + "__eq__": "The `__eq__` method is used to define equality comparison between objects.", + }, + correct_answer="__init__", + hint="This method is automatically called when an object is instantiated.", + shuffle=True, + ) + + q2 = Question( + question="What is the term for a class that inherits from another class?", + options={ + "Base class": "A base class is the class being inherited from, not the one inheriting.", + "Derived class": "Correct! A derived class is a class that inherits from another class.", + }, + correct_answer="Derived class", + hint="This class extends the functionality of another class.", + shuffle=True, + ) + + q3 = Question( + question="What is the purpose of the `super()` function in Python?", + options={ + "To call a method from the parent class": "Correct! `super()` is used to call a method from the parent class.", + "To create a derived class": "Incorrect. TODO", + "To initialize an object": "Incorrect. Object initialization is done using the `__init__` method.", + }, + correct_answer="To call a method from the parent class", + hint="This function is used to access inherited methods.", + shuffle=True, + ) + + q4 = Question( + question="What is composition in OOP?", + options={ + "A way to build complex objects by combining simpler ones": "Correct! Composition involves including instances of other classes as attributes.", + "A way to inherit methods from a base class": "Incorrect. This describes inheritance, not composition.", + "A way to define abstract methods": "Incorrect. Abstract methods are unrelated to composition.", + "A way to restrict access to class attributes": "Incorrect. This describes encapsulation, not composition.", + }, + correct_answer="A way to build complex objects by combining simpler ones", + hint="Think about combining objects rather than inheriting from them.", + shuffle=True, + ) + + super().__init__(questions=[q1, q2, q3, q4]) + + +class OopAdvancedAbstractClasses(Quiz): + def __init__(self, title=""): + q1 = Question( + question="Which module in Python is used to create abstract classes?", + options={ + "abc": "Correct! The `abc` module provides the infrastructure for defining abstract base classes.", + "abstract": "There is no module named `abstract` in Python.", + "abstractmodule": "There is no module named `abstractmodule` in Python.", + }, + correct_answer="abc", + hint="This module's name is an abbreviation for 'Abstract Base Classes'.", + shuffle=True, + ) + + q2 = Question( + question="What is the purpose of an abstract class?", + options={ + "To define methods that must be implemented by subclasses": "Correct! Abstract classes define methods that must be implemented by concrete subclasses.", + "To create a class that cannot have methods": "Incorrect. Abstract classes can have methods.", + "To create a class that cannot have attributes": "Incorrect. Abstract classes can have attributes.", + "To create a class that cannot be inherited": "Incorrect. Abstract classes are designed to be inherited.", + }, + correct_answer="To define methods that must be implemented by subclasses", + hint="Abstract classes act as blueprints for other classes.", + shuffle=True, + ) + + q3 = Question( + question="True or False: You can instantiate an abstract class directly.", + options={ + "True": "Incorrect. Abstract classes cannot be instantiated directly.", + "False": "Correct! Abstract classes are meant to be subclassed and cannot be instantiated directly.", + }, + correct_answer="False", + hint="Abstract classes are designed to be extended by concrete subclasses.", + shuffle=True, + ) + + super().__init__(questions=[q1, q2, q3]) + + +class OopAdvancedDecorators(Quiz): + def __init__(self, title=""): + q1 = Question( + question="Which decorator is used to define a method that belongs to the class rather than an instance?", + options={ + "@staticmethod": "Incorrect. A static method does not belong to the class or instance.", + "@classmethod": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", + "@property": "Incorrect. The `@property` decorator is used to define getter methods.", + "@abstractmethod": "Incorrect. The `@abstractmethod` decorator is used in abstract classes.", + }, + correct_answer="@classmethod", + hint="This method takes `cls` as its first parameter.", + shuffle=True, + ) + + q2 = Question( + question="What is the purpose of the `@property` decorator?", + options={ + "To define a computed attribute": "Correct! The `@property` decorator is used to define computed attributes.", + "To define a static method": "Incorrect. Static methods are defined using the `@staticmethod` decorator.", + "To define a class method": "Incorrect. Class methods are defined using the `@classmethod` decorator.", + "To define an abstract method": "Incorrect. Abstract methods are defined using the `@abstractmethod` decorator.", + }, + correct_answer="To define a computed attribute", + hint="This decorator allows you to define methods that can be accessed like attributes.", + shuffle=True, + ) + + q3 = Question( + question="Which decorator is used to define a method that does not access the class or instance?", + options={ + "@staticmethod": "Correct! A static method does not access the class or instance.", + "@classmethod": "Incorrect. A class method accesses the class using `cls`.", + "@property": "Incorrect. The `@property` decorator is used to define getter methods.", + "@abstractmethod": "Incorrect. The `@abstractmethod` decorator is used in abstract classes.", + }, + correct_answer="@staticmethod", + hint="This method is often used for utility functions.", + shuffle=True, + ) + + q4 = Question( question="A method with which decorator takes `cls` as its first parameter?", options={ "@classmethod": "Correct! A class method is bound to a class rather than its instances and the parameter `cls` represents the class itself.", @@ -15,6 +148,63 @@ def __init__(self, title=""): shuffle=True, ) + q5 = Question( + question="Which decorator is used to define a method that belongs to the class rather than an instance?", + options={ + "@staticmethod": "Incorrect. A static method does not belong to the class or instance.", + "@classmethod": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", + "@property": "Incorrect. The `@property` decorator is used to define getter methods.", + "@abstractmethod": "Incorrect. The `@abstractmethod` decorator is used in abstract classes.", + }, + correct_answer="@classmethod", + hint="This method takes `cls` as its first parameter.", + shuffle=True, + ) + + q6 = Question( + question="What is the purpose of the `@classmethod` decorator?", + options={ + "To define a method that belongs to the class rather than an instance": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", + "To define a method that does not access the class or instance": "Incorrect. This describes a static method.", + "To define a computed attribute": "Incorrect. Computed attributes are defined using the `@property` decorator.", + "To define an abstract method": "Incorrect. Abstract methods are defined using the `@abstractmethod` decorator.", + }, + correct_answer="To define a method that belongs to the class rather than an instance", + hint="This method takes `cls` as its first parameter.", + shuffle=True, + ) + + q7 = Question( + question="What is the difference between `@staticmethod` and `@classmethod`?", + options={ + "`@staticmethod` does not access the class or instance, while `@classmethod` takes `cls` as its first parameter": "Correct! This is the key difference between the two decorators.", + "`@staticmethod` is used for utility functions, while `@classmethod` is used for abstract methods": "Incorrect. Abstract methods are unrelated to these decorators.", + "`@staticmethod` is faster than `@classmethod`": "Incorrect. Performance is not the defining difference.", + "`@staticmethod` is used for computed attributes, while `@classmethod` is used for class-level attributes": "Incorrect. Computed attributes are defined using `@property`.", + }, + correct_answer="`@staticmethod` does not access the class or instance, while `@classmethod` takes `cls` as its first parameter", + hint="Think about the parameters each decorator uses.", + shuffle=True, + ) + + super().__init__(questions=[q1, q2, q3, q4, q5, q6, q7]) + + +class OopAdvancedEncapsulation(Quiz): + def __init__(self, title=""): + q1 = Question( + question="Which naming convention is used to indicate a private attribute in Python?", + options={ + "_attribute": "Incorrect. A single underscore indicates a protected attribute.", + "__attribute": "Correct! A double underscore indicates a private attribute.", + "attribute_": "Incorrect. This is not a convention for private attributes.", + "__attribute__": "Incorrect. Double underscores at both ends are used for special methods.", + }, + correct_answer="__attribute", + hint="Private attributes use double underscores.", + shuffle=True, + ) + q2 = Question( question="Even though it's not recommended, which type of attributes and methods can be accessed using name mangling in Python?", options={ @@ -28,6 +218,35 @@ def __init__(self, title=""): ) q3 = Question( + question="What is the purpose of encapsulation in OOP?", + options={ + "To bundle data and methods into a single unit": "Correct! Encapsulation bundles data and methods into a single unit.", + "To define abstract methods": "Incorrect. Abstract methods are defined using the `abc` module.", + "To create a class that cannot be inherited": "Incorrect. Encapsulation does not restrict inheritance.", + "To define static methods": "Incorrect. Static methods are defined using the `@staticmethod` decorator.", + }, + correct_answer="To bundle data and methods into a single unit", + hint="Encapsulation is one of the fundamental principles of OOP.", + shuffle=True, + ) + + q4 = Question( + question="True or False: Protected attributes can be accessed directly from outside the class.", + options={ + "True": "Correct! Protected attributes can be accessed directly, but it is not recommended.", + "False": "Incorrect. Protected attributes can be accessed directly, but it is not recommended.", + }, + correct_answer="True", + hint="Protected attributes are indicated by a single underscore.", + shuffle=True, + ) + + super().__init__(questions=[q1, q2, q3, q4]) + + +class OopAdvancedAttrsDataclasses(Quiz): + def __init__(self, title=""): + q1 = Question( question="What is something that `attrs` provides but `dataclasses` doesn't?", options={ "__init__()": "Both packages automatically generate `__init__()`: `dataclasses` uses the `@dataclass` decorator, while `attrs` uses `@define`.", @@ -39,4 +258,4 @@ def __init__(self, title=""): shuffle=True, ) - super().__init__(questions=[q1, q2, q3]) + super().__init__(questions=[q1]) From 6c86d54c4321845e30e62a98db29a2172ed8ff87 Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou Date: Thu, 8 May 2025 09:40:34 +0200 Subject: [PATCH 2/7] wip exercises --- 13_object_oriented_programming_advanced.ipynb | 232 ++++++++++-------- .../object_oriented_programming_advanced.py | 5 +- ...13_object_oriented_programming_advanced.py | 226 +++++++++-------- 3 files changed, 264 insertions(+), 199 deletions(-) diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index 826f73db..2901ed22 100644 --- a/13_object_oriented_programming_advanced.ipynb +++ b/13_object_oriented_programming_advanced.ipynb @@ -74,7 +74,7 @@ "\n", "In the [Object-oriented Programming](./05_object_oriented_programming.ipynb) notebook, we explored the foundational concepts of OOP, including how to define classes, create instances, and work with attributes and methods.\n", "We covered Python's special methods like `__init__` for initialization, `__str__` and `__repr__` for string representation, and `__eq__` for equality checks.\n", - "Additionally, we introduced the `@property` decorator to define computed attributes, enabling cleaner access to derived values, and discussed encapsulation principles using public, private, and protected access modifiers.\n", + "We also introduced the `@property` decorator to define computed attributes.\n", "These concepts laid the groundwork for understanding how to structure and organize code, emphasizing the importance of modularity in software design.\n", "\n", "In the following notebook, we are going to explore the more advanced concepts of OOP, like inheritance, composition, abstraction, and much more." @@ -337,7 +337,7 @@ "source": [ "### Composition\n", "\n", - "Composition is a concept in OOP where a class is composed of one or more objects of other classes, instead of inheriting from them.\n", + "Composition is a concept in OOP where a class is composed of one or more **instances** of other classes, instead of inheriting from them.\n", "\n", "It is a way to build complex objects by combining simpler ones. Composition allows for greater **flexibility and modularity** in code compared to inheritance.\n", "\n", @@ -454,22 +454,45 @@ "id": "34", "metadata": {}, "outputs": [], + "source": [ + "class Animal:\n", + " def __init__(self, name):\n", + " self.name = name\n", + "\n", + " def speak(self):\n", + " print(f\"{self.name} makes a sound\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], "source": [ "class Dog(Animal):\n", " def speak(self):\n", " print(f\"{self.name} says Woof!\")\n", "\n", " def parent_speak(self):\n", - " super().speak()\n", - "\n", - "dog = Dog('Max')\n", + " super().speak()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "dog = Dog('Rex')\n", "dog.speak()\n", "dog.parent_speak()" ] }, { "cell_type": "markdown", - "id": "35", + "id": "37", "metadata": {}, "source": [ "### Quiz on Inheritance" @@ -478,7 +501,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -488,7 +511,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "39", "metadata": {}, "source": [ "### Exercise: Child Eye Color\n", @@ -503,9 +526,9 @@ "
\n", "

Question

\n", "
    \n", - "
  • Complete the solution function such that it creates classes Mother and Father, each with an attribute for eye_color.
  • \n", - "
  • Create class Child, which inherits the eye colors of Mother and Father and based on those, calculate the child's eye color, according to the rules above.
  • \n", - "
  • Create two parents, each with a randomly assigned eye color, by picking one of the available two. Then create their child and return its eye color.
  • \n", + "
  • Complete the solution function such that it defines classes Mother and Father, each with an attribute for the eye color.
  • \n", + "
  • Define class Child, which inherits from Mother and Father and has an attribute called eye_color. Then, based on the eye colors of the parents, calculate the child's eye color, according to the rules above.
  • \n", + "
  • Create an instance of Child, which is being initialized by using the arguments passed in the solution function, namely the eye colors of its parents. Lastly, return this instance.
  • \n", "
\n", "
" ] @@ -513,7 +536,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -523,25 +546,34 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "41", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", - "import random\n", "\n", - "colors = [\"blue\", \"brown\"]\n", - "mother_eye_color = random.choice(colors)\n", - "father_eye_color = random.choice(colors)\n", + "from attrs import define\n", "\n", "def solution_child_eye_color(mother_eye_color: str, father_eye_color: str) -> str:\n", - " # Write your solution here\n", - " pass" + " \"\"\"\n", + " Given the eye colors of the mother and father, defines the eye color of the child.\n", + " The possible eye colors are: brown or blue, with brown being the dominant one.\n", + " This function defines a class Mother and a class Father, which are used to create an instance of the class Child.\n", + " It returns an instance of the class Child with the eye color defined by the parents.\n", + "\n", + " Args:\n", + " mother_eye_color (str): Eye color of the mother.\n", + " father_eye_color (str): Eye color of the father.\n", + " Returns:\n", + " - an instance of class Child\n", + " \"\"\"\n", + "\n", + " return" ] }, { "cell_type": "markdown", - "id": "40", + "id": "42", "metadata": {}, "source": [ "## Abstract Classes\n", @@ -552,13 +584,13 @@ "Abstract classes define methods that **must** be implemented by any concrete (non-abstract) subclass.\n", "In Python, you can create abstract classes using the `abc` (**Abstract Base Classes**) module.\n", "\n", - "The ABC class from the abc module is used as the base class for your abstract class.\n", + "The `ABC` class from the abc module is used as the base class for your abstract class.\n", "You cannot create an instance of an abstract class, but you can create instances of concrete subclasses that inherit from the abstract class." ] }, { "cell_type": "markdown", - "id": "41", + "id": "43", "metadata": {}, "source": [ "We first create an abstract class which inherits from `ABC`:" @@ -567,7 +599,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -585,7 +617,7 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "45", "metadata": {}, "source": [ "Careful, you **cannot** create an instance of an abstract class!\n", @@ -595,7 +627,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -604,7 +636,7 @@ }, { "cell_type": "markdown", - "id": "45", + "id": "47", "metadata": {}, "source": [ "Let's create two concrete subclasses of `Shape`:" @@ -613,7 +645,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -642,7 +674,7 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "49", "metadata": {}, "source": [ "Now we are allowed to create instances of the subclasses and also call their methods:" @@ -651,7 +683,7 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -666,7 +698,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "51", "metadata": {}, "source": [ "### Quiz on Abstraction" @@ -675,7 +707,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "52", "metadata": {}, "outputs": [], "source": [ @@ -685,7 +717,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "53", "metadata": {}, "source": [ "### Exercise: Banking System\n", @@ -742,7 +774,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -752,7 +784,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -767,7 +799,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "56", "metadata": {}, "source": [ "## Decorators\n", @@ -780,7 +812,7 @@ }, { "cell_type": "markdown", - "id": "55", + "id": "57", "metadata": {}, "source": [ "### @classmethod\n", @@ -794,7 +826,7 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -833,7 +865,7 @@ }, { "cell_type": "markdown", - "id": "57", + "id": "59", "metadata": {}, "source": [ "### @staticmethod\n", @@ -846,7 +878,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "60", "metadata": {}, "source": [ "As seen in the example below, static methods have limited use, because they don't have access neither to the class attributes nor to any instance of the class.\n", @@ -859,7 +891,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -879,7 +911,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "62", "metadata": {}, "source": [ "### @property\n", @@ -893,7 +925,7 @@ }, { "cell_type": "markdown", - "id": "61", + "id": "63", "metadata": {}, "source": [ "In this simple example, we create a class `Circle`, which has a radius and an area.\n", @@ -903,7 +935,7 @@ { "cell_type": "code", "execution_count": null, - "id": "62", + "id": "64", "metadata": {}, "outputs": [], "source": [ @@ -925,7 +957,7 @@ }, { "cell_type": "markdown", - "id": "63", + "id": "65", "metadata": {}, "source": [ "We can access the `area` property, just like any other class attribute.\n", @@ -935,7 +967,7 @@ { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "66", "metadata": {}, "outputs": [], "source": [ @@ -944,7 +976,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "67", "metadata": {}, "source": [ "### Setters & Getters\n", @@ -962,7 +994,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "68", "metadata": {}, "outputs": [], "source": [ @@ -988,7 +1020,7 @@ }, { "cell_type": "markdown", - "id": "67", + "id": "69", "metadata": {}, "source": [ "Create an instance and use the getter:" @@ -997,7 +1029,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "70", "metadata": {}, "outputs": [], "source": [ @@ -1007,7 +1039,7 @@ }, { "cell_type": "markdown", - "id": "69", + "id": "71", "metadata": {}, "source": [ "Update the radius using the setter:" @@ -1016,7 +1048,7 @@ { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "72", "metadata": {}, "outputs": [], "source": [ @@ -1027,7 +1059,7 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "73", "metadata": {}, "source": [ "What happens when we enter an invalid value?" @@ -1036,7 +1068,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "74", "metadata": {}, "outputs": [], "source": [ @@ -1045,7 +1077,7 @@ }, { "cell_type": "markdown", - "id": "73", + "id": "75", "metadata": {}, "source": [ "Finally let's use the deleter:" @@ -1054,7 +1086,7 @@ { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "76", "metadata": {}, "outputs": [], "source": [ @@ -1063,7 +1095,7 @@ }, { "cell_type": "markdown", - "id": "75", + "id": "77", "metadata": {}, "source": [ "We are no longer able to access the deleted attribute:" @@ -1072,7 +1104,7 @@ { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "78", "metadata": {}, "outputs": [], "source": [ @@ -1081,7 +1113,7 @@ }, { "cell_type": "markdown", - "id": "77", + "id": "79", "metadata": {}, "source": [ "### Quiz on Decorators" @@ -1090,7 +1122,7 @@ { "cell_type": "code", "execution_count": null, - "id": "78", + "id": "80", "metadata": {}, "outputs": [], "source": [ @@ -1100,7 +1132,7 @@ }, { "cell_type": "markdown", - "id": "79", + "id": "81", "metadata": {}, "source": [ "## Encapsulation\n", @@ -1134,7 +1166,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80", + "id": "82", "metadata": {}, "outputs": [], "source": [ @@ -1152,7 +1184,7 @@ }, { "cell_type": "markdown", - "id": "81", + "id": "83", "metadata": {}, "source": [ "### Protected\n", @@ -1165,7 +1197,7 @@ { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "84", "metadata": {}, "outputs": [], "source": [ @@ -1183,7 +1215,7 @@ }, { "cell_type": "markdown", - "id": "83", + "id": "85", "metadata": {}, "source": [ "### Quiz on Encapsulation" @@ -1192,7 +1224,7 @@ { "cell_type": "code", "execution_count": null, - "id": "84", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -1202,7 +1234,7 @@ }, { "cell_type": "markdown", - "id": "85", + "id": "87", "metadata": {}, "source": [ "## How to write better classes" @@ -1210,7 +1242,7 @@ }, { "cell_type": "markdown", - "id": "86", + "id": "88", "metadata": {}, "source": [ "Lastly, we would like to offer some tips & tricks that will help you write your code in a cleaner and easier-to-maintain way." @@ -1218,7 +1250,7 @@ }, { "cell_type": "markdown", - "id": "87", + "id": "89", "metadata": {}, "source": [ "### Using dataclasses\n", @@ -1229,7 +1261,7 @@ { "cell_type": "code", "execution_count": null, - "id": "88", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -1242,7 +1274,7 @@ }, { "cell_type": "markdown", - "id": "89", + "id": "91", "metadata": {}, "source": [ "A simpler way, however, would be to import `dataclass` from the `dataclasses` module.\n", @@ -1255,7 +1287,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "92", "metadata": {}, "outputs": [], "source": [ @@ -1270,7 +1302,7 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "93", "metadata": {}, "source": [ "Now, with the use of these auto generated methods, we can create an instance of the class and print a representation of the object, without any additional code.\n", @@ -1280,7 +1312,7 @@ { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "94", "metadata": {}, "outputs": [], "source": [ @@ -1294,7 +1326,7 @@ }, { "cell_type": "markdown", - "id": "93", + "id": "95", "metadata": {}, "source": [ "### Using attrs\n", @@ -1307,7 +1339,7 @@ }, { "cell_type": "markdown", - "id": "94", + "id": "96", "metadata": {}, "source": [ "Let's rewrite the previous example, this time using `attrs`.\n", @@ -1317,7 +1349,7 @@ { "cell_type": "code", "execution_count": null, - "id": "95", + "id": "97", "metadata": {}, "outputs": [], "source": [ @@ -1339,7 +1371,7 @@ }, { "cell_type": "markdown", - "id": "96", + "id": "98", "metadata": {}, "source": [ "However, `attrs` also provides **validators**.\n", @@ -1351,7 +1383,7 @@ { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "99", "metadata": {}, "outputs": [], "source": [ @@ -1373,7 +1405,7 @@ }, { "cell_type": "markdown", - "id": "98", + "id": "100", "metadata": {}, "source": [ "### Quiz on `attrs` and `dataclasses`" @@ -1382,7 +1414,7 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "101", "metadata": {}, "outputs": [], "source": [ @@ -1392,7 +1424,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "102", "metadata": {}, "source": [ "## Exercises" @@ -1401,7 +1433,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1410,7 +1442,7 @@ }, { "cell_type": "markdown", - "id": "102", + "id": "104", "metadata": {}, "source": [ "### Store Inventory\n", @@ -1449,7 +1481,7 @@ { "cell_type": "code", "execution_count": null, - "id": "103", + "id": "105", "metadata": {}, "outputs": [], "source": [ @@ -1480,7 +1512,7 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "106", "metadata": {}, "source": [ "### Music Streaming Service\n", @@ -1508,7 +1540,7 @@ { "cell_type": "code", "execution_count": null, - "id": "105", + "id": "107", "metadata": {}, "outputs": [], "source": [ @@ -1539,7 +1571,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "108", "metadata": {}, "source": [ "### The N-body problem" @@ -1547,7 +1579,7 @@ }, { "cell_type": "markdown", - "id": "107", + "id": "109", "metadata": {}, "source": [ "On a boring and rainy Sunday afternoon, you decide that you want to attempt writing a Python program that simulates the orbits of Jupiter's moons. To start with, you decide to focus your efforts on tracking just **four** of the largest moons: Io, Europa, Ganymede, and Callisto.\n", @@ -1568,7 +1600,7 @@ }, { "cell_type": "markdown", - "id": "108", + "id": "110", "metadata": {}, "source": [ "
\n", @@ -1580,7 +1612,7 @@ { "cell_type": "code", "execution_count": null, - "id": "109", + "id": "111", "metadata": { "tags": [] }, @@ -1593,7 +1625,7 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "112", "metadata": {}, "source": [ "
\n", @@ -1604,7 +1636,7 @@ }, { "cell_type": "markdown", - "id": "111", + "id": "113", "metadata": {}, "source": [ "Each of the strings output by your solution function below should be something like\n", @@ -1618,7 +1650,7 @@ { "cell_type": "code", "execution_count": null, - "id": "112", + "id": "114", "metadata": { "tags": [] }, @@ -1632,7 +1664,7 @@ }, { "cell_type": "markdown", - "id": "113", + "id": "115", "metadata": {}, "source": [ "---\n", @@ -1656,7 +1688,7 @@ }, { "cell_type": "markdown", - "id": "114", + "id": "116", "metadata": {}, "source": [ "To have a complete account of the moons' orbits, you need to compute the **total energy of the system**. The total energy for a single moon is its **potential energy** multiplied by its **kinetic energy**.\n", @@ -1698,7 +1730,7 @@ }, { "cell_type": "markdown", - "id": "115", + "id": "117", "metadata": {}, "source": [ "
\n", @@ -1710,7 +1742,7 @@ { "cell_type": "code", "execution_count": null, - "id": "116", + "id": "118", "metadata": { "tags": [] }, @@ -1723,7 +1755,7 @@ }, { "cell_type": "markdown", - "id": "117", + "id": "119", "metadata": {}, "source": [ "
\n", @@ -1739,7 +1771,7 @@ }, { "cell_type": "markdown", - "id": "118", + "id": "120", "metadata": {}, "source": [ "
\n", @@ -1751,7 +1783,7 @@ { "cell_type": "code", "execution_count": null, - "id": "119", + "id": "121", "metadata": { "tags": [] }, diff --git a/tutorial/quiz/object_oriented_programming_advanced.py b/tutorial/quiz/object_oriented_programming_advanced.py index 1f77d9d6..317bf1b0 100644 --- a/tutorial/quiz/object_oriented_programming_advanced.py +++ b/tutorial/quiz/object_oriented_programming_advanced.py @@ -30,7 +30,7 @@ def __init__(self, title=""): question="What is the purpose of the `super()` function in Python?", options={ "To call a method from the parent class": "Correct! `super()` is used to call a method from the parent class.", - "To create a derived class": "Incorrect. TODO", + "To create a derived class": "Incorrect. `super()` is not used for creating derived classes.", "To initialize an object": "Incorrect. Object initialization is done using the `__init__` method.", }, correct_answer="To call a method from the parent class", @@ -43,8 +43,7 @@ def __init__(self, title=""): options={ "A way to build complex objects by combining simpler ones": "Correct! Composition involves including instances of other classes as attributes.", "A way to inherit methods from a base class": "Incorrect. This describes inheritance, not composition.", - "A way to define abstract methods": "Incorrect. Abstract methods are unrelated to composition.", - "A way to restrict access to class attributes": "Incorrect. This describes encapsulation, not composition.", + "A way to inherit methods from more than one class": "Incorrect. This describes multiple inheritance, not composition.", }, correct_answer="A way to build complex objects by combining simpler ones", hint="Think about combining objects rather than inheriting from them.", diff --git a/tutorial/tests/test_13_object_oriented_programming_advanced.py b/tutorial/tests/test_13_object_oriented_programming_advanced.py index d5b6795a..14e3fa2d 100644 --- a/tutorial/tests/test_13_object_oriented_programming_advanced.py +++ b/tutorial/tests/test_13_object_oriented_programming_advanced.py @@ -5,6 +5,12 @@ import pytest from numpy import average + +class SubAssertionError(AssertionError): + def __init__(self): + super().__init__("Solution must be a proper class instance with attributes.") + + # # Exercise 1: Child Eye Color # @@ -30,8 +36,34 @@ def set_eye_color(self): return self.eye_color_mother return "brown" - child = Child(mother_eye_color, father_eye_color) - return child.eye_color + return Child(mother_eye_color, father_eye_color) + + +def validate_child_eye_color(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__ == "Child", ( + "The class should be named 'Child'." + ) + # Check inheritance by base class names + base_class_names = [base.__name__ for base in type(solution_result).__bases__] + assert "Mother" in base_class_names, ( + "The 'Child' class must inherit from a class named 'Mother'." + ) + assert "Father" in base_class_names, ( + "The 'Child' class must inherit from a class named 'Father'." + ) + # Check the class attributes + try: + attrs = list(vars(solution_result)) + except TypeError: + raise SubAssertionError from None + assert len(attrs) == 1, "The class should have 1 attribute." + assert "eye_color" in attrs, "The class attribute should be 'eye_color'." @pytest.mark.parametrize( @@ -44,13 +76,106 @@ def set_eye_color(self): ], ) def test_child_eye_color(mother_eye_color, father_eye_color, function_to_test): + solution_result = function_to_test(mother_eye_color, father_eye_color) + reference_result = reference_child_eye_color(mother_eye_color, father_eye_color) + + validate_child_eye_color(solution_result) + assert solution_result.eye_color == reference_result.eye_color + + +# +# Exercise 2: Banking System +# + + +def reference_banking_system( + tax_rate: float, + interest_rate: float, + gross_salary: int, + savings_precentage: float, + years_passed: int, +) -> float: + class Account(ABC): + def __init__(self, account_number): + self.account_number = account_number + self.balance = 0 + + @abstractmethod + def credit(self, amount): + pass + + @abstractmethod + def get_balance(self): + pass + + def debit(self, amount): + if self.balance >= amount: + self.balance -= amount + else: + print("Insufficient funds.") + + class SalaryAccount(Account): + def __init__(self, account_number, tax_rate): + super().__init__(account_number) + self.tax_rate = tax_rate + + def credit(self, amount): + self.balance += amount - amount * self.tax_rate + + def get_balance(self): + return self.balance + + class SavingsAccount(Account): + def __init__(self, account_number, interest_rate): + super().__init__(account_number) + self.interest_rate = interest_rate + self.creation_year = datetime.now().year + + def credit(self, amount): + self.balance += amount + + def get_balance(self, years_passed): + interest = self.balance * self.interest_rate * years_passed + return self.balance + interest + + salary_account = SalaryAccount("SAL-001", tax_rate) + savings_account = SavingsAccount("SAV-001", interest_rate) + + salary_account.credit(gross_salary) + + amount_to_transfer = salary_account.get_balance() * savings_precentage + + salary_account.debit(amount_to_transfer) + savings_account.credit(amount_to_transfer) + + return savings_account.get_balance(years_passed) + + +@pytest.mark.parametrize( + "tax_rate, interest_rate, gross_salary, savings_precentage, years_passed", + [ + (0.20, 0.05, 10000, 0.3, 2), + (0.18, 0.04, 9300, 0.15, 3), + (0.13, 0.07, 8500, 0.18, 4), + ], +) +def test_banking_system( + tax_rate, + interest_rate, + gross_salary, + savings_precentage, + years_passed, + function_to_test, +): assert function_to_test( - mother_eye_color, father_eye_color - ) == reference_child_eye_color(mother_eye_color, father_eye_color) + tax_rate, interest_rate, gross_salary, savings_precentage, years_passed + ) == reference_banking_system( + tax_rate, interest_rate, gross_salary, savings_precentage, years_passed + ) # -# Exercise 2: Store Inventory +# Exercise 3: Store Inventory # @@ -244,97 +369,6 @@ def test_music_streaming_service(function_to_test): ) # both playlists should have the same keys and values -# -# Exercise 4: Banking System -# - - -def reference_banking_system( - tax_rate: float, - interest_rate: float, - gross_salary: int, - savings_precentage: float, - years_passed: int, -) -> float: - class Account(ABC): - def __init__(self, account_number): - self.account_number = account_number - self.balance = 0 - - @abstractmethod - def credit(self, amount): - pass - - @abstractmethod - def get_balance(self): - pass - - def debit(self, amount): - if self.balance >= amount: - self.balance -= amount - else: - print("Insufficient funds.") - - class SalaryAccount(Account): - def __init__(self, account_number, tax_rate): - super().__init__(account_number) - self.tax_rate = tax_rate - - def credit(self, amount): - self.balance += amount - amount * self.tax_rate - - def get_balance(self): - return self.balance - - class SavingsAccount(Account): - def __init__(self, account_number, interest_rate): - super().__init__(account_number) - self.interest_rate = interest_rate - self.creation_year = datetime.now().year - - def credit(self, amount): - self.balance += amount - - def get_balance(self, years_passed): - interest = self.balance * self.interest_rate * years_passed - return self.balance + interest - - salary_account = SalaryAccount("SAL-001", tax_rate) - savings_account = SavingsAccount("SAV-001", interest_rate) - - salary_account.credit(gross_salary) - - amount_to_transfer = salary_account.get_balance() * savings_precentage - - salary_account.debit(amount_to_transfer) - savings_account.credit(amount_to_transfer) - - return savings_account.get_balance(years_passed) - - -@pytest.mark.parametrize( - "tax_rate, interest_rate, gross_salary, savings_precentage, years_passed", - [ - (0.20, 0.05, 10000, 0.3, 2), - (0.18, 0.04, 9300, 0.15, 3), - (0.13, 0.07, 8500, 0.18, 4), - ], -) -def test_banking_system( - tax_rate, - interest_rate, - gross_salary, - savings_precentage, - years_passed, - function_to_test, -): - assert function_to_test( - tax_rate, interest_rate, gross_salary, savings_precentage, years_passed - ) == reference_banking_system( - tax_rate, interest_rate, gross_salary, savings_precentage, years_passed - ) - - # # Exercise 5: The N-body problem # From 1bb1a919237777ab34617d822ae73fe0a660dda0 Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou Date: Thu, 8 May 2025 15:55:41 +0200 Subject: [PATCH 3/7] fix banking system --- 13_object_oriented_programming_advanced.ipynb | 44 +++---- ...13_object_oriented_programming_advanced.py | 110 +++++++++++++----- 2 files changed, 104 insertions(+), 50 deletions(-) diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index 2901ed22..ac23dd78 100644 --- a/13_object_oriented_programming_advanced.ipynb +++ b/13_object_oriented_programming_advanced.ipynb @@ -722,52 +722,46 @@ "source": [ "### Exercise: Banking System\n", "\n", - "In this exercise, we will implement a very simple banking system where there are two different types of accounts: **Salary Accounts** and **Savings Accounts**.\n", + "In this exercise, we will implement a simple banking system where there are two different types of accounts: **Salary Accounts** and **Savings Accounts**.\n", "\n", "We assume the following Classes:\n", "\n", "**Account**:\n", "\n", - "An abstract base class representing a generic bank account with attributes `account_number` and `balance` and abstract methods `credit()` and `get_balance()`.\n", + "An abstract base class representing a generic bank account with attributes `account_number` and `balance`, and abstract methods `credit()` and `get_balance()`.\n", "It should also contain the method `debit()`, which, if funds are sufficient, should subtract a given amount (parameter) from the account balance.\n", "Method `debit()` should be common for all derived classes.\n", + "When an Account is created it should always be initialized with **balance equal to 0**.\n", "\n", "**SalaryAccount**:\n", "\n", "A derived class representing a salary account that contains an additional attribute for `tax_rate` and **overrides** methods `credit()` and `get_balance()`.\n", - "Method `credit()` should set the balance as the `gross_salary` after applying the `tax_rate` to it.\n", + "Method `credit()` should update the `balance` with the **net salary**, so after applying taxes.\n", "Method `get_balance()` should simply return the account balance.\n", "\n", "**SavingsAccount**:\n", "\n", - "A derived class representing a savings account that contains additional attributes for `interest_rate` and the account's `creation_year`, plus **overrides** methods `credit()` and `get_balance()`.\n", + "A derived class representing a savings account that contains an additional attribute for `interest_rate` and **overrides** methods `credit()` and `get_balance()`.\n", "Method `credit()` should simply add the given amount to the current balance.\n", - "Method `get_balance()` should return the account balance plus interest, based on the `interest_rate` and the `years_passed` from the account's creation.\n", + "Method `get_balance()` should return the account balance including interest.\n", "\n", "
\n", "

Question

\n", " Using abstraction in Python, create a banking system based on the entities mentioned above.\n", "
    \n", "
  • \n", - " Initialize Accounts:\n", - "

    Create a Salary Account and a Savings Account with an initial balance of 0 in each.

    \n", + "

    Define classes Account, SalaryAccount and SavingsAccount.

    \n", "
  • \n", "
  • \n", - " Update Salary Account:\n", - "

    Add an amount represented by gross_salary to the Salary Account's balance.

    \n", + "

    Create an instance of SalaryAccount, passing as parameters any account_number that you like and the tax_rate that is given in the solution function's parameters.

    \n", "
  • \n", "
  • \n", - " Transfer Funds:\n", - "

    Calculate a transfer amount from the Salary Account to the Savings Account using a given savings_percentage. Deduct this amount from the Salary Account and add it to the Savings Account.

    \n", + "

    Create an instance of SavingsAccount, passing as parameters any account_number that you like and the interest_rate that is given in the solution function's parameters.

    \n", "
  • \n", "
  • \n", - " Project Future Balance:\n", - "

    Return the projected balance of the Savings Account after a specified number of years, provided by years_passed.

    \n", + "

    Return a list containing the two instances you created.

    \n", "
  • \n", "
\n", - "\n", - "The solution function should return the balance of the Savings Account after the given number of years.\n", - "\n", "
" ] }, @@ -790,11 +784,21 @@ "source": [ "%%ipytest\n", "from abc import ABC, abstractmethod\n", - "from datetime import datetime\n", "\n", - "def solution_banking_system(tax_rate: float, interest_rate: float, gross_salary: int, savings_precentage: float, years_passed: int) -> float:\n", - " # Write your solution here\n", - " pass" + "def solution_banking_system(tax_rate: float, interest_rate: float) -> list:\n", + " \"\"\"\n", + " Defines abstract class `Account` with attributes `account_number` and `balance`, and methods `credit()`, `get_balance()`, and `debit()`.\n", + " Implements `SalaryAccount` (with `tax_rate`) and `SavingsAccount` (with `interest_rate`) as derived classes, overriding `credit()` and `get_balance()`.\n", + " Creates and returns instances of `SalaryAccount` and `SavingsAccount` in a list.\n", + "\n", + " Args:\n", + " tax_rate (float): Tax rate for SalaryAccount.\n", + " interest_rate (float): Interest rate for SavingsAccount.\n", + " Returns:\n", + " list: Instances of SalaryAccount and SavingsAccount.\n", + " \"\"\"\n", + "\n", + " return" ] }, { diff --git a/tutorial/tests/test_13_object_oriented_programming_advanced.py b/tutorial/tests/test_13_object_oriented_programming_advanced.py index 14e3fa2d..cc8012a5 100644 --- a/tutorial/tests/test_13_object_oriented_programming_advanced.py +++ b/tutorial/tests/test_13_object_oriented_programming_advanced.py @@ -1,6 +1,5 @@ import pathlib from abc import ABC, abstractmethod -from datetime import datetime import pytest from numpy import average @@ -91,9 +90,6 @@ def test_child_eye_color(mother_eye_color, father_eye_color, function_to_test): def reference_banking_system( tax_rate: float, interest_rate: float, - gross_salary: int, - savings_precentage: float, - years_passed: int, ) -> float: class Account(ABC): def __init__(self, account_number): @@ -129,49 +125,103 @@ class SavingsAccount(Account): def __init__(self, account_number, interest_rate): super().__init__(account_number) self.interest_rate = interest_rate - self.creation_year = datetime.now().year def credit(self, amount): self.balance += amount - def get_balance(self, years_passed): - interest = self.balance * self.interest_rate * years_passed - return self.balance + interest - - salary_account = SalaryAccount("SAL-001", tax_rate) - savings_account = SavingsAccount("SAV-001", interest_rate) - - salary_account.credit(gross_salary) + def get_balance(self): + return self.balance + self.balance * self.interest_rate - amount_to_transfer = salary_account.get_balance() * savings_precentage + return [ + SalaryAccount("SAL-001", tax_rate), + SavingsAccount("SAV-001", interest_rate), + ] - salary_account.debit(amount_to_transfer) - savings_account.credit(amount_to_transfer) - return savings_account.get_balance(years_passed) +def validate_banking_system(solution_result): + assert isinstance(solution_result, list), "Solution must return a list." + assert len(solution_result) == 2, "The list must contain exactly two elements." + assert all( + isinstance(item, object) and type(item).__module__ != "builtins" + for item in solution_result + ), "Both elements in the list must be instances of custom classes." + assert all( + "Account" in [base.__name__ for base in type(item).__bases__] + for item in solution_result + ), "Both elements in the list must inherit from a class named 'Account'." + assert type(solution_result[0]).__name__ == "SalaryAccount", ( + "The 1st class should be an instance of 'SalaryAccount'." + ) + assert type(solution_result[1]).__name__ == "SavingsAccount", ( + "The 2nd class should be an instance of 'SavingsAccount'." + ) + # Check the class attributes: SalaryAccount + try: + attrs = list(vars(solution_result[0])) + except TypeError: + raise SubAssertionError from None + assert len(attrs) == 3, "The class 'SalaryAccount' should have 3 attributes." + assert "account_number" in attrs, ( + "The class 'SalaryAccount' should have an attribute called 'account_number'." + ) + assert "balance" in attrs, ( + "The class 'SalaryAccount' should have an attribute called 'balance'." + ) + assert "tax_rate" in attrs, ( + "The class 'SalaryAccount' should have an attribute called 'tax_rate'." + ) + # Check the class attributes: SavingsAccount + try: + attrs = list(vars(solution_result[1])) + except TypeError: + raise SubAssertionError from None + assert len(attrs) == 3, "The class 'SavingsAccount' should have 3 attributes." + assert "account_number" in attrs, ( + "The class 'SavingsAccount' should have an attribute called 'account_number'." + ) + assert "balance" in attrs, ( + "The class 'SavingsAccount' should have an attribute called 'balance'." + ) + assert "interest_rate" in attrs, ( + "The class 'SavingsAccount' should have an attribute called 'interest_rate'." + ) + # Check that each class has the required methods + required_methods = {"credit", "get_balance"} + for item in solution_result: + class_methods = { + method for method in dir(item) if callable(getattr(item, method)) + } + assert required_methods.issubset(class_methods), ( + f"The class '{type(item).__name__}' must have the methods: {', '.join(required_methods)}." + ) @pytest.mark.parametrize( - "tax_rate, interest_rate, gross_salary, savings_precentage, years_passed", + "tax_rate, interest_rate", [ - (0.20, 0.05, 10000, 0.3, 2), - (0.18, 0.04, 9300, 0.15, 3), - (0.13, 0.07, 8500, 0.18, 4), + (0.20, 0.05), + (0.18, 0.04), ], ) def test_banking_system( tax_rate, interest_rate, - gross_salary, - savings_precentage, - years_passed, function_to_test, ): - assert function_to_test( - tax_rate, interest_rate, gross_salary, savings_precentage, years_passed - ) == reference_banking_system( - tax_rate, interest_rate, gross_salary, savings_precentage, years_passed - ) + solution_result = function_to_test(tax_rate, interest_rate) + reference_result = reference_banking_system(tax_rate, interest_rate) + + validate_banking_system(solution_result) + + amount = 10000 + # test SalaryAccount functions + solution_result[0].credit(amount) + reference_result[0].credit(amount) + assert solution_result[0].get_balance() == reference_result[0].get_balance() + # test SavingsAccount functions + solution_result[1].credit(amount) + reference_result[1].credit(amount) + assert solution_result[1].get_balance() == reference_result[1].get_balance() # @@ -255,7 +305,7 @@ def test_store_inventory(function_to_test): # -# Exercise 3: Music Streaming Service +# Exercise 4: Music Streaming Service # From 40f9e0f9da194f0d8c3b9a62279cce269400b5d5 Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou Date: Thu, 8 May 2025 18:08:15 +0200 Subject: [PATCH 4/7] finish updating exercises --- 13_object_oriented_programming_advanced.ipynb | 122 ++++++++--- ...13_object_oriented_programming_advanced.py | 194 +++++++++++------- 2 files changed, 213 insertions(+), 103 deletions(-) diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index ac23dd78..5a922c93 100644 --- a/13_object_oriented_programming_advanced.ipynb +++ b/13_object_oriented_programming_advanced.ipynb @@ -554,7 +554,7 @@ "\n", "from attrs import define\n", "\n", - "def solution_child_eye_color(mother_eye_color: str, father_eye_color: str) -> str:\n", + "def solution_child_eye_color(mother_eye_color: str, father_eye_color: str) -> list:\n", " \"\"\"\n", " Given the eye colors of the mother and father, defines the eye color of the child.\n", " The possible eye colors are: brown or blue, with brown being the dominant one.\n", @@ -1475,10 +1475,10 @@ "\n", "
\n", "

Question

\n", - " Complete the solution function such that it creates the instances of the two computers mentioned in the list below.\n", - " Pay attention to the type!\n", + " Complete the solution function such that it creates the instances of the two computers mentioned below.\n", + " They will be automatically passed as arguments to the solution function.\n", "
\n", - " This function should return a list that collects the string representations of the two computers.\n", + " This function should return a list that collects the two instances you created.\n", "
" ] }, @@ -1491,27 +1491,41 @@ "source": [ "%%ipytest\n", "\n", - "computers = [\n", - " {\n", - " \"type\": \"PC\",\n", - " \"name\": \"pc_1\",\n", - " \"price\": 1500,\n", - " \"quantity\": 1,\n", - " \"expansion_slots\": 2\n", - " },\n", - " {\n", - " \"type\": \"Laptop\",\n", - " \"name\": \"laptop_1\",\n", - " \"price\": 1200,\n", - " \"quantity\": 4,\n", - " \"battery_life\": 6\n", - " }\n", - "]\n", + "pc = {\n", + " \"name\": \"pc_1\",\n", + " \"price\": 1500,\n", + " \"quantity\": 1,\n", + " \"expansion_slots\": 2\n", + "}\n", + "\n", + "laptop = {\n", + " \"name\": \"laptop_1\",\n", + " \"price\": 1200,\n", + " \"quantity\": 4,\n", + " \"battery_life\": 6\n", + "}\n", + "\n", + "def solution_store_inventory(pc: dict, laptop: dict) -> list:\n", + " \"\"\"\n", + " Creates instances of `PC` and `Laptop` classes based on the provided input dictionaries.\n", + " The `Computer` class serves as the base class with attributes `name`, `price`, and `quantity`.\n", + " The `PC` and `Laptop` classes inherit from `Computer` and extend it with additional attributes:\n", + " - `PC` includes `expansion_slots`.\n", + " - `Laptop` includes `battery_life`.\n", "\n", + " Each class implements the `__init__` and `__str__` methods:\n", + " - `Computer.__str__`: Returns a string representation of the `Computer` instance.\n", + " - `PC.__str__`: Appends to the `Computer` string.\n", + " - `Laptop.__str__`: Appends to the `Computer` string.\n", "\n", - "def solution_store_inventory(computers: list[dict]) -> list[str]:\n", - " # Write your solution here\n", - " pass" + " Args:\n", + " pc: A dictionary containing the attributes for the `PC` instance.\n", + " laptop: A dictionary containing the attributes for the `Laptop` instance.\n", + " Returns:\n", + " A list containing the created the `PC` and `Laptop` instances.\n", + " \"\"\"\n", + "\n", + " return" ] }, { @@ -1529,15 +1543,18 @@ "- **User** with attributes: username, playlists.\n", "\n", "Based on these, create the respective classes:\n", - "- `Song`: should contain attributes `title` (string), `artist` (string) and `album_title` (string)\n", - "- `Playlist`: should contain attributes `name` (string) and `songs` (a list of `Song` instances). It should also include a method for adding song a song to the playlist.\n", - "- `User`: should contain attributes `name` (string) and `playlists` (a dict where key is the name of the playlist and value is a `Playlist` instance). It should also include a method for creating a playlist and a method for adding a specific song to a specific playlist.\n", + "- `Song`: should contain attributes `title` (string), `artist` (string) and `album_title` (string).\n", + "- `Playlist`: should contain attributes `name` (string) and `songs` (a list of `Song` instances). It should also include a method for adding a song to the playlist.\n", + "- `User`: should contain attributes `username` (string) and `playlists` (a dict where key is the name of the playlist and value is a `Playlist` instance). It should also include a method for creating a playlist and a method for adding a specific song to a specific playlist.\n", "\n", "
\n", "

Question

\n", " Using composition in Python, create a music streaming service system that includes the classes mentioned above.\n", " Create one user that has one playlist, containing the songs provided in the list below.\n", - " Your solution function should return the User instance.\n", + " They will be automatically passed as arguments to the solution function.\n", + " The user's username and the name of the playlist are also provided.\n", + "
\n", + " Your solution function should return the User instance.\n", "
" ] }, @@ -1568,9 +1585,29 @@ " },\n", "]\n", "\n", - "def solution_music_streaming_service(song_info: list[dict]):\n", - " # Write your solution here\n", - " pass" + "def solution_music_streaming_service(song_info: list[dict], username: str, playlist_name: str):\n", + " \"\"\"\n", + " Creates a music streaming service system using composition, including classes for `Song`, `Playlist`, and `User`:\n", + " - `Song` represents a song with:\n", + " - title\n", + " - artist\n", + " - album_title\n", + " - `Playlist` represents a playlist with:\n", + " - name\n", + " - songs\n", + " - Includes a method to add a song to the playlist.\n", + " - `User` represents a user with:\n", + " - username\n", + " - playlists\n", + " - Includes methods to create a playlist and add a song to a specific playlist.\n", + "\n", + " Args:\n", + " song_info: A list of dictionaries, where each dictionary contains the details of a song.\n", + " Returns:\n", + " An instance of the `User` class with one playlist containing the provided songs.\n", + " \"\"\"\n", + "\n", + " return" ] }, { @@ -1796,8 +1833,29 @@ "%%ipytest\n", "\n", "def solution_n_body(universe_start: str) -> int:\n", - " # Write your solution here\n", - " pass" + " \"\"\"\n", + " Simulates the motion of moons in a 3-dimensional space over a specified number of time steps\n", + " and calculates the average total energy of the system.\n", + "\n", + " The simulation involves:\n", + " 1. Updating the velocity of each moon based on the gravitational interaction with other moons.\n", + " 2. Updating the position of each moon by applying its velocity.\n", + " 3. Calculating the total energy of the system after the simulation.\n", + "\n", + " Total energy for a single moon is calculated as:\n", + " - Potential energy: The sum of the absolute values of its positional coordinates.\n", + " - Kinetic energy: The sum of the absolute values of its velocity components.\n", + " - Total energy: Potential energy multiplied by kinetic energy.\n", + "\n", + " Args:\n", + " universe_start (str): A string representing the initial positions of the moons in the format:\n", + " \"MoonName: x=, y=, z=\"\n", + "\n", + " Returns:\n", + " int: The average total energy of the system after simulating the universe for 1000 time steps.\n", + " \"\"\"\n", + "\n", + " return" ] } ], diff --git a/tutorial/tests/test_13_object_oriented_programming_advanced.py b/tutorial/tests/test_13_object_oriented_programming_advanced.py index cc8012a5..4acbb16d 100644 --- a/tutorial/tests/test_13_object_oriented_programming_advanced.py +++ b/tutorial/tests/test_13_object_oriented_programming_advanced.py @@ -15,7 +15,7 @@ def __init__(self): # -def reference_child_eye_color(mother_eye_color: str, father_eye_color: str) -> str: +def reference_child_eye_color(mother_eye_color: str, father_eye_color: str) -> list: class Mother: def __init__(self, eye_color: str): self.eye_color_mother = eye_color @@ -87,10 +87,7 @@ def test_child_eye_color(mother_eye_color, father_eye_color, function_to_test): # -def reference_banking_system( - tax_rate: float, - interest_rate: float, -) -> float: +def reference_banking_system(tax_rate: float, interest_rate: float) -> list: class Account(ABC): def __init__(self, account_number): self.account_number = account_number @@ -150,10 +147,10 @@ def validate_banking_system(solution_result): for item in solution_result ), "Both elements in the list must inherit from a class named 'Account'." assert type(solution_result[0]).__name__ == "SalaryAccount", ( - "The 1st class should be an instance of 'SalaryAccount'." + "The 1st element in the list should be an instance of 'SalaryAccount'." ) assert type(solution_result[1]).__name__ == "SavingsAccount", ( - "The 2nd class should be an instance of 'SavingsAccount'." + "The 2nd element in the list should be an instance of 'SavingsAccount'." ) # Check the class attributes: SalaryAccount try: @@ -203,11 +200,7 @@ def validate_banking_system(solution_result): (0.18, 0.04), ], ) -def test_banking_system( - tax_rate, - interest_rate, - function_to_test, -): +def test_banking_system(tax_rate, interest_rate, function_to_test): solution_result = function_to_test(tax_rate, interest_rate) reference_result = reference_banking_system(tax_rate, interest_rate) @@ -229,7 +222,7 @@ def test_banking_system( # -def reference_store_inventory(computers: list[dict]) -> list[str]: +def reference_store_inventory(pc: dict, laptop: dict) -> list: class Computer: """A class representing a computer sold by the online store""" @@ -237,7 +230,6 @@ def __init__(self, name: str, price: int, quantity: int): self.name = name self.price = price self.quantity = quantity - self.type = None def __str__(self): return f"Computer with name '{self.name}', price {self.price} CHF and quantity {self.quantity}." @@ -248,7 +240,6 @@ class PC(Computer): def __init__(self, name: str, price: int, quantity: int, expansion_slots: int): super().__init__(name, price, quantity) self.expansion_slots = expansion_slots - self.type = "PC" def __str__(self): return ( @@ -262,7 +253,6 @@ class Laptop(Computer): def __init__(self, name: str, price: int, quantity: int, battery_life: int): super().__init__(name, price, quantity) self.battery_life = battery_life - self.type = "Laptop" def __str__(self): return ( @@ -270,38 +260,88 @@ def __str__(self): + f" This laptop has a battery life of {self.battery_life} hours." ) - inventory = [] - for computer in computers: - computer_type = PC if computer["type"] == "PC" else Laptop - computer.pop("type") - inventory.append(computer_type(**computer)) - - result = [] - for item in inventory: - result.append(str(item)) - - return result - - -def test_store_inventory(function_to_test): - computers = [ - { - "type": "PC", - "name": "pc_1", - "price": 1500, - "quantity": 1, - "expansion_slots": 2, - }, - { - "type": "Laptop", - "name": "laptop_1", - "price": 1200, - "quantity": 4, - "battery_life": 6, - }, + return [ + PC(**pc), + Laptop(**laptop), ] - assert function_to_test(computers) == reference_store_inventory(computers) + +def validate_store_inventory(solution_result): + assert isinstance(solution_result, list), "Solution must return a list." + assert len(solution_result) == 2, "The list must contain exactly two elements." + assert all( + isinstance(item, object) and type(item).__module__ != "builtins" + for item in solution_result + ), "Both elements in the list must be instances of custom classes." + assert all( + "Computer" in [base.__name__ for base in type(item).__bases__] + for item in solution_result + ), "Both elements in the list must inherit from a class named 'Computer'." + assert type(solution_result[0]).__name__ == "PC", ( + "The 1st element in the list should be an instance of 'PC'." + ) + assert type(solution_result[1]).__name__ == "Laptop", ( + "The 2nd element in the list should be an instance of 'Laptop'." + ) + # Check the class attributes: PC + try: + attrs = list(vars(solution_result[0])) + except TypeError: + raise SubAssertionError from None + assert len(attrs) == 4, "The class 'PC' should have 4 attributes." + assert "name" in attrs, "The class 'PC' should have an attribute called 'name'." + assert "price" in attrs, "The class 'PC' should have an attribute called 'price'." + assert "quantity" in attrs, ( + "The class 'PC' should have an attribute called 'quantity'." + ) + assert "expansion_slots" in attrs, ( + "The class 'PC' should have an attribute called 'expansion_slots'." + ) + # Check the class attributes: Laptop + try: + attrs = list(vars(solution_result[1])) + except TypeError: + raise SubAssertionError from None + assert len(attrs) == 4, "The class 'Laptop' should have 4 attributes." + assert "name" in attrs, "The class 'Laptop' should have an attribute called 'name'." + assert "price" in attrs, ( + "The class 'Laptop' should have an attribute called 'price'." + ) + assert "quantity" in attrs, ( + "The class 'Laptop' should have an attribute called 'quantity'." + ) + assert "battery_life" in attrs, ( + "The class 'Laptop' should have an attribute called 'battery_life'." + ) + + +@pytest.mark.parametrize( + "pc, laptop", + [ + ( + { + "name": "pc_1", + "price": 1500, + "quantity": 1, + "expansion_slots": 2, + }, + { + "name": "laptop_1", + "price": 1200, + "quantity": 4, + "battery_life": 6, + }, + ), + ], +) +def test_store_inventory(pc, laptop, function_to_test): + solution_result = function_to_test(pc, laptop) + reference_result = reference_store_inventory(pc, laptop) + + validate_store_inventory(solution_result) + + assert str(solution_result[0]) == str(reference_result[0]) + assert str(solution_result[1]) == str(reference_result[1]) # @@ -309,7 +349,9 @@ def test_store_inventory(function_to_test): # -def reference_music_streaming_service(song_info: list[dict]): +def reference_music_streaming_service( + song_info: list[dict], username: str, playlist_name: str +): class Song: def __init__(self, title: str, artist: str, album_title: str): self.title = title @@ -355,39 +397,49 @@ def display_playlist(self, playlist_name: str): return f"Playlist '{playlist_name}' not found." return self.playlists[playlist_name].display_songs() - user = User("Bob") - user.create_playlist("Favorites from Queen") + user = User(username) + user.create_playlist(playlist_name) for info in song_info: user.add_song_to_playlist( - "Favorites from Queen", + playlist_name, Song(info["title"], info["artist"], info["album_title"]), ) return user -def test_music_streaming_service(function_to_test): - song_info = [ - { - "title": "Bohemian Rhapsody", - "artist": "Queen", - "album_title": "A Night at the Opera", - }, - { - "title": "We Will Rock You", - "artist": "Queen", - "album_title": "News of the World", - }, - { - "title": "I Want to Break Free", - "artist": "Queen", - "album_title": "The Works", - }, - ] - - solution_user = function_to_test(song_info) - reference_user = reference_music_streaming_service(song_info) +@pytest.mark.parametrize( + "song_info, username, playlist_name", + [ + ( + [ + { + "title": "Bohemian Rhapsody", + "artist": "Queen", + "album_title": "A Night at the Opera", + }, + { + "title": "We Will Rock You", + "artist": "Queen", + "album_title": "News of the World", + }, + { + "title": "I Want to Break Free", + "artist": "Queen", + "album_title": "The Works", + }, + ], + "Bob", + "Favorites from Queen", + ), + ], +) +def test_music_streaming_service(song_info, username, playlist_name, function_to_test): + solution_user = function_to_test(song_info, username, playlist_name) + reference_user = reference_music_streaming_service( + song_info, username, playlist_name + ) assert ( vars(solution_user).keys() == vars(reference_user).keys() From 879079ea214df82e724e6f7d2fd3a23d5f5bfea0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Yakutovich Date: Fri, 9 May 2025 00:09:58 +0200 Subject: [PATCH 5/7] Another pass --- 13_object_oriented_programming_advanced.ipynb | 404 ++++++++---------- 1 file changed, 176 insertions(+), 228 deletions(-) diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index 5a922c93..99369e01 100644 --- a/13_object_oriented_programming_advanced.ipynb +++ b/13_object_oriented_programming_advanced.ipynb @@ -62,7 +62,7 @@ "source": [ "- [super()](https://docs.python.org/3/library/functions.html#super)\n", "- [dataclasses](https://docs.python.org/3/library/dataclasses.html)\n", - "- [attrs](https://www.attrs.org/en/stable/index.htmlhttps://www.attrs.org/en/stable/index.html)" + "- [attrs](https://www.attrs.org/en/stable/index.html)" ] }, { @@ -97,19 +97,12 @@ "metadata": {}, "source": [ "### Single Inheritance\n", - "Inheritance is a mechanism in OOP that allows you to create a new class by inheriting the properties and methods of an existing class.\n", "\n", + "Inheritance is a mechanism in OOP that allows you to create a new class by inheriting the properties and methods of an existing class.\n", "The existing class is called the **base class** or **parent class**, and the new class is referred to as the **derived class** or **child class**. \n", - "\n", "The derived class inherits all the attributes and methods of the base class.\n", - "It can also override or extend those inherited methods." - ] - }, - { - "cell_type": "markdown", - "id": "7", - "metadata": {}, - "source": [ + "It can also override or extend those inherited methods.\n", + "\n", "Let's create a simple example in Python.\n", "We first define the base class `Animal`:" ] @@ -117,7 +110,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -131,23 +124,20 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "8", "metadata": {}, "source": [ "From your base class, you can define as many derived classes as you'd like.\n", "Simply pass the parent class name as a **parameter** in the child class definition.\n", - "\n", "Here, we create two classes that inherit from `Animal`, namely `Dog` and `Cat`.\n", - "\n", "Both derived classes **override** the generic `speak` method with a specific sound for each animal.\n", - "\n", "They also **extend** the `Animal` class individually, with the `fetch` and `chase` functions, which are animal specific." ] }, { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -169,7 +159,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "10", "metadata": {}, "source": [ "Now we can create instances of `Dog` and `Cat`: " @@ -178,7 +168,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -188,7 +178,7 @@ }, { "cell_type": "markdown", - "id": "13", + "id": "12", "metadata": {}, "source": [ "Let's see what happens when you call each instance's methods:" @@ -197,7 +187,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -208,7 +198,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -218,7 +208,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "15", "metadata": {}, "source": [ "Of course, you can always use the base class `Animal` **as is**.\n", @@ -228,7 +218,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -238,7 +228,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "17", "metadata": {}, "source": [ "### Multiple Inheritance\n", @@ -246,21 +236,14 @@ "It is also possible for a class to be derived from **more than one base classes** in Python.\n", "This is called multiple inheritance.\n", "\n", - "Let's see the example of a very famous dog who also happens to be a detective:" - ] - }, - { - "cell_type": "markdown", - "id": "19", - "metadata": {}, - "source": [ - "To do that, we first define a new class." + "Let's see the example of a very famous dog who also happens to be a detective.\n", + "To do that, we first define a new class:" ] }, { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -274,7 +257,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "19", "metadata": {}, "source": [ "Then, we create a derived class that inherits from two base classes. Notice that:\n", @@ -285,7 +268,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -300,7 +283,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "21", "metadata": {}, "source": [ "We can also call all methods inherited from each parent." @@ -309,7 +292,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -322,41 +305,33 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "23", "metadata": {}, "source": [ "While multiple inheritance can be powerful, it can also lead to complexities and potential conflicts, so it should only be used when really needed.\n", - "\n", "In some cases, composition may be preferred over multiple inheritance to achieve better code organization and maintainability." ] }, { "cell_type": "markdown", - "id": "26", + "id": "24", "metadata": {}, "source": [ "### Composition\n", "\n", "Composition is a concept in OOP where a class is composed of one or more **instances** of other classes, instead of inheriting from them.\n", - "\n", - "It is a way to build complex objects by combining simpler ones. Composition allows for greater **flexibility and modularity** in code compared to inheritance.\n", - "\n", + "It is a way to build complex objects by combining simpler ones.\n", + "Composition allows for greater **flexibility and modularity** in code compared to inheritance.\n", "In Python, composition is achieved by including instances of other classes as attributes within a class. \n", - "These instances become part of the containing class and are used to provide specific functionalities." - ] - }, - { - "cell_type": "markdown", - "id": "27", - "metadata": {}, - "source": [ + "These instances become part of the containing class and are used to provide specific functionalities.\n", + "\n", "Let's first create the classes for the `Engine` and the `Wheels` of a vehicle:" ] }, { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -381,7 +356,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "26", "metadata": {}, "source": [ "Then we can create a car, which has one engine and four wheels.\n", @@ -393,7 +368,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -416,7 +391,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -428,22 +403,15 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "29", "metadata": {}, "source": [ "### `super()`\n", "\n", "Python's `super()` function allows us to refer the superclass implicitly, so we don’t need to write the name of superclass explicitly.\n", - "\n", "It returns a proxy object that delegates method calls to a parent or sibling class.\n", - "This is useful for accessing inherited methods that have been overridden in a class." - ] - }, - { - "cell_type": "markdown", - "id": "33", - "metadata": {}, - "source": [ + "This is useful for accessing inherited methods that have been overridden in a class.\n", + "\n", "Let's re-write class `Dog`, which is a subclass of `Animal`.\n", "This time it not only overwrites the method `speak()`, but it also demonstrates how to call the parent's method, with the use of `super()`." ] @@ -451,7 +419,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -466,7 +434,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -481,7 +449,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -492,7 +460,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "33", "metadata": {}, "source": [ "### Quiz on Inheritance" @@ -501,7 +469,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -511,7 +479,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "35", "metadata": {}, "source": [ "### Exercise: Child Eye Color\n", @@ -527,7 +495,8 @@ "

Question

\n", "
    \n", "
  • Complete the solution function such that it defines classes Mother and Father, each with an attribute for the eye color.
  • \n", - "
  • Define class Child, which inherits from Mother and Father and has an attribute called eye_color. Then, based on the eye colors of the parents, calculate the child's eye color, according to the rules above.
  • \n", + "
  • Define class Child, which inherits from Mother and Father and has an attribute called eye_color.\n", + " Then, based on the eye colors of the parents, calculate the child's eye color, according to the rules above and assign it to the attribute eye_color.
  • \n", "
  • Create an instance of Child, which is being initialized by using the arguments passed in the solution function, namely the eye colors of its parents. Lastly, return this instance.
  • \n", "
\n", "
" @@ -536,7 +505,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -546,14 +515,12 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "37", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", - "from attrs import define\n", - "\n", "def solution_child_eye_color(mother_eye_color: str, father_eye_color: str) -> list:\n", " \"\"\"\n", " Given the eye colors of the mother and father, defines the eye color of the child.\n", @@ -567,39 +534,30 @@ " Returns:\n", " - an instance of class Child\n", " \"\"\"\n", - "\n", " return" ] }, { "cell_type": "markdown", - "id": "42", + "id": "38", "metadata": {}, "source": [ "## Abstract Classes\n", "\n", "Abstract classes are classes that cannot be instantiated directly.\n", "They are meant to be used as a blueprint for other classes.\n", - "\n", "Abstract classes define methods that **must** be implemented by any concrete (non-abstract) subclass.\n", "In Python, you can create abstract classes using the `abc` (**Abstract Base Classes**) module.\n", - "\n", "The `ABC` class from the abc module is used as the base class for your abstract class.\n", - "You cannot create an instance of an abstract class, but you can create instances of concrete subclasses that inherit from the abstract class." - ] - }, - { - "cell_type": "markdown", - "id": "43", - "metadata": {}, - "source": [ + "You cannot create an instance of an abstract class, but you can create instances of concrete subclasses that inherit from the abstract class.\n", + "\n", "We first create an abstract class which inherits from `ABC`:" ] }, { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -617,7 +575,7 @@ }, { "cell_type": "markdown", - "id": "45", + "id": "40", "metadata": {}, "source": [ "Careful, you **cannot** create an instance of an abstract class!\n", @@ -627,7 +585,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -636,7 +594,7 @@ }, { "cell_type": "markdown", - "id": "47", + "id": "42", "metadata": {}, "source": [ "Let's create two concrete subclasses of `Shape`:" @@ -645,7 +603,7 @@ { "cell_type": "code", "execution_count": null, - "id": "48", + "id": "43", "metadata": {}, "outputs": [], "source": [ @@ -674,7 +632,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "44", "metadata": {}, "source": [ "Now we are allowed to create instances of the subclasses and also call their methods:" @@ -683,7 +641,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "45", "metadata": {}, "outputs": [], "source": [ @@ -698,7 +656,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "46", "metadata": {}, "source": [ "### Quiz on Abstraction" @@ -707,7 +665,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "47", "metadata": {}, "outputs": [], "source": [ @@ -717,7 +675,7 @@ }, { "cell_type": "markdown", - "id": "53", + "id": "48", "metadata": {}, "source": [ "### Exercise: Banking System\n", @@ -768,7 +726,7 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "49", "metadata": {}, "outputs": [], "source": [ @@ -778,7 +736,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -803,7 +761,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "51", "metadata": {}, "source": [ "## Decorators\n", @@ -816,13 +774,12 @@ }, { "cell_type": "markdown", - "id": "57", + "id": "52", "metadata": {}, "source": [ "### @classmethod\n", "Defines a class method, which is a method bound to the class rather than its instances.\n", "However, class methods can be called by both class and object.\n", - "\n", "It takes the class itself (named `cls`) as its first parameter, allowing you to access and modify class-level attributes and methods.\n", "These changes would apply across all the instances of the class." ] @@ -830,7 +787,7 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "53", "metadata": {}, "outputs": [], "source": [ @@ -869,22 +826,15 @@ }, { "cell_type": "markdown", - "id": "59", + "id": "54", "metadata": {}, "source": [ "### @staticmethod\n", "Defines a static method, which is also a method bound to the class, but does not have access to the class or instance.\n", "Hence, it cannot modify the class state.\n", - "\n", "Static methods are typically used for utility functions that are related to the class but don't need access to instance-specific data.\n", - "A static method does not receive an implicit first argument." - ] - }, - { - "cell_type": "markdown", - "id": "60", - "metadata": {}, - "source": [ + "A static method does not receive an implicit first argument.\n", + "\n", "As seen in the example below, static methods have limited use, because they don't have access neither to the class attributes nor to any instance of the class.\n", "They **cannot access** `cls` or `self`.\n", "\n", @@ -895,7 +845,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "55", "metadata": {}, "outputs": [], "source": [ @@ -915,23 +865,16 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "56", "metadata": {}, "source": [ "### @property\n", "\n", "Defines properties in a class.\n", "It creates attributes that act like methods but can be accessed and assigned as regular attributes.\n", - "\n", "Properties are also useful for implementing attributes that require additional logic or validation when getting or setting their values.\n", - "They promote a cleaner way of working with attributes, while controlling their behavior behind the scenes." - ] - }, - { - "cell_type": "markdown", - "id": "63", - "metadata": {}, - "source": [ + "They promote a cleaner way of working with attributes, while controlling their behavior behind the scenes.\n", + "\n", "In this simple example, we create a class `Circle`, which has a radius and an area.\n", "We can create an instance of it, just like any other class." ] @@ -939,7 +882,7 @@ { "cell_type": "code", "execution_count": null, - "id": "64", + "id": "57", "metadata": {}, "outputs": [], "source": [ @@ -961,7 +904,7 @@ }, { "cell_type": "markdown", - "id": "65", + "id": "58", "metadata": {}, "source": [ "We can access the `area` property, just like any other class attribute.\n", @@ -971,7 +914,7 @@ { "cell_type": "code", "execution_count": null, - "id": "66", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -980,7 +923,7 @@ }, { "cell_type": "markdown", - "id": "67", + "id": "60", "metadata": {}, "source": [ "### Setters & Getters\n", @@ -998,7 +941,7 @@ { "cell_type": "code", "execution_count": null, - "id": "68", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -1024,7 +967,7 @@ }, { "cell_type": "markdown", - "id": "69", + "id": "62", "metadata": {}, "source": [ "Create an instance and use the getter:" @@ -1033,7 +976,7 @@ { "cell_type": "code", "execution_count": null, - "id": "70", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -1043,7 +986,7 @@ }, { "cell_type": "markdown", - "id": "71", + "id": "64", "metadata": {}, "source": [ "Update the radius using the setter:" @@ -1052,7 +995,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -1063,7 +1006,7 @@ }, { "cell_type": "markdown", - "id": "73", + "id": "66", "metadata": {}, "source": [ "What happens when we enter an invalid value?" @@ -1072,7 +1015,7 @@ { "cell_type": "code", "execution_count": null, - "id": "74", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -1081,7 +1024,7 @@ }, { "cell_type": "markdown", - "id": "75", + "id": "68", "metadata": {}, "source": [ "Finally let's use the deleter:" @@ -1090,7 +1033,7 @@ { "cell_type": "code", "execution_count": null, - "id": "76", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -1099,7 +1042,7 @@ }, { "cell_type": "markdown", - "id": "77", + "id": "70", "metadata": {}, "source": [ "We are no longer able to access the deleted attribute:" @@ -1108,7 +1051,7 @@ { "cell_type": "code", "execution_count": null, - "id": "78", + "id": "71", "metadata": {}, "outputs": [], "source": [ @@ -1117,7 +1060,7 @@ }, { "cell_type": "markdown", - "id": "79", + "id": "72", "metadata": {}, "source": [ "### Quiz on Decorators" @@ -1126,7 +1069,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80", + "id": "73", "metadata": {}, "outputs": [], "source": [ @@ -1136,17 +1079,14 @@ }, { "cell_type": "markdown", - "id": "81", + "id": "74", "metadata": {}, "source": [ "## Encapsulation\n", "\n", "Encapsulation is one of the fundamental principles of OOP and is a concept that plays a crucial role in Python and other OOP languages.\n", - "\n", "Encapsulation refers to the bundling of data (attributes) and methods (functions) that operate on that data into a single unit. \n", - "\n", "It also involves controlling the access to the data and methods, restricting direct access from outside the class.\n", - "\n", "The purpose of encapsulation is to hide the internal implementation details of a class and provide a simpler and cleaner way of interacting with it.\n", "\n", "By using encapsulation you can:\n", @@ -1155,22 +1095,33 @@ "- Make it easier to change the internal implementation of a class without affecting external code that uses the class.\n", "\n", "In Python, encapsulation is implemented using access modifiers and naming conventions.\n", - "There are three commonly used access modifiers:\n", - "\n", + "There are three commonly used access modifiers:" + ] + }, + { + "cell_type": "markdown", + "id": "75", + "metadata": {}, + "source": [ "### Public\n", - "In Python, all attributes and methods are public by default, which means they can be accessed from anywhere.\n", - "\n", + "In Python, all attributes and methods are public by default, which means they can be accessed from anywhere.\n" + ] + }, + { + "cell_type": "markdown", + "id": "76", + "metadata": {}, + "source": [ "### Private\n", "Attributes and methods with names starting with a **double underscore** (e.g., `__variable`, `__method()`) are considered private. \n", "They are not intended to be accessed directly from outside the class.\n", - "\n", "However, Python does not enforce strict access control, so you can still access them using **name mangling** (e.g., `_classname__variable`)." ] }, { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "77", "metadata": {}, "outputs": [], "source": [ @@ -1188,7 +1139,7 @@ }, { "cell_type": "markdown", - "id": "83", + "id": "78", "metadata": {}, "source": [ "### Protected\n", @@ -1201,7 +1152,7 @@ { "cell_type": "code", "execution_count": null, - "id": "84", + "id": "79", "metadata": {}, "outputs": [], "source": [ @@ -1219,7 +1170,7 @@ }, { "cell_type": "markdown", - "id": "85", + "id": "80", "metadata": {}, "source": [ "### Quiz on Encapsulation" @@ -1228,7 +1179,7 @@ { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "81", "metadata": {}, "outputs": [], "source": [ @@ -1238,23 +1189,17 @@ }, { "cell_type": "markdown", - "id": "87", - "metadata": {}, - "source": [ - "## How to write better classes" - ] - }, - { - "cell_type": "markdown", - "id": "88", + "id": "82", "metadata": {}, "source": [ + "## How to write better classes\n", + "\n", "Lastly, we would like to offer some tips & tricks that will help you write your code in a cleaner and easier-to-maintain way." ] }, { "cell_type": "markdown", - "id": "89", + "id": "83", "metadata": {}, "source": [ "### Using dataclasses\n", @@ -1265,7 +1210,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "84", "metadata": {}, "outputs": [], "source": [ @@ -1278,20 +1223,18 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "85", "metadata": {}, "source": [ "A simpler way, however, would be to import `dataclass` from the `dataclasses` module.\n", - "\n", "This module provides a decorator and functions for automatically adding generated special methods such as `__init__()` and `__repr__()` to user-defined classes.\n", - "\n", "This means that we no longer need to use `__init__()`, but only to specify the attributes of the class and their types:" ] }, { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -1306,7 +1249,7 @@ }, { "cell_type": "markdown", - "id": "93", + "id": "87", "metadata": {}, "source": [ "Now, with the use of these auto generated methods, we can create an instance of the class and print a representation of the object, without any additional code.\n", @@ -1316,7 +1259,7 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "88", "metadata": {}, "outputs": [], "source": [ @@ -1330,22 +1273,16 @@ }, { "cell_type": "markdown", - "id": "95", + "id": "89", "metadata": {}, "source": [ "### Using attrs\n", "\n", - "This Python package is for creating well-defined classes with a type, attributes and methods. When defining a class, it will add static methods to that class based on the attributes you declare.\n", - "\n", + "This Python package is for creating well-defined classes with a type, attributes and methods.\n", + "When defining a class, it will add static methods to that class based on the attributes you declare.\n", "`attrs` will operate only on the dunder methods of your class.\n", - "Hence, all of its tools will live in functions that operate on top of instances." - ] - }, - { - "cell_type": "markdown", - "id": "96", - "metadata": {}, - "source": [ + "Hence, all of its tools will live in functions that operate on top of instances.\n", + "\n", "Let's rewrite the previous example, this time using `attrs`.\n", "You will notice that it offers the same functionalities, i.e. `__init__()`, `__repr__()`, and object comparison." ] @@ -1353,7 +1290,7 @@ { "cell_type": "code", "execution_count": null, - "id": "97", + "id": "90", "metadata": {}, "outputs": [], "source": [ @@ -1375,7 +1312,7 @@ }, { "cell_type": "markdown", - "id": "98", + "id": "91", "metadata": {}, "source": [ "However, `attrs` also provides **validators**.\n", @@ -1387,7 +1324,7 @@ { "cell_type": "code", "execution_count": null, - "id": "99", + "id": "92", "metadata": {}, "outputs": [], "source": [ @@ -1409,7 +1346,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "93", "metadata": {}, "source": [ "### Quiz on `attrs` and `dataclasses`" @@ -1418,7 +1355,7 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "id": "94", "metadata": {}, "outputs": [], "source": [ @@ -1428,7 +1365,7 @@ }, { "cell_type": "markdown", - "id": "102", + "id": "95", "metadata": {}, "source": [ "## Exercises" @@ -1437,7 +1374,7 @@ { "cell_type": "code", "execution_count": null, - "id": "103", + "id": "96", "metadata": {}, "outputs": [], "source": [ @@ -1446,7 +1383,7 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "97", "metadata": {}, "source": [ "### Store Inventory\n", @@ -1464,11 +1401,12 @@ "\n", "- The output of Computer's `__str__` method should be: `Computer with name '', price CHF and quantity .`\n", "- The output of PC's `__str__` method should **append** Computer's output with: ` This PC has expansion slots.`\n", - "- The output of Laptop's `__str__` method should **append** Computer's output with: ` This laptop has a battery life of hours.`\n", + "- The output of the Laptop's `__str__` method should **append** the Computer's output with: ` This laptop has a battery life of hours.`\n", "\n", "
\n", "

Hint

\n", - " An example of a PC's string representation can be: Computer with name 'pc_1', price 1000 CHF and quantity 2. This PC has 3 expansion slots.\n", + " An example of a PC's string representation can be: Computer with the name 'pc_1', price 1000 CHF and quantity 2.\n", + " This PC has 3 expansion slots.\n", "
\n", " Pay attention to the single quotes and the whitespace between the two sentences.\n", "
\n", @@ -1485,7 +1423,7 @@ { "cell_type": "code", "execution_count": null, - "id": "105", + "id": "98", "metadata": {}, "outputs": [], "source": [ @@ -1530,7 +1468,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "99", "metadata": {}, "source": [ "### Music Streaming Service\n", @@ -1544,8 +1482,10 @@ "\n", "Based on these, create the respective classes:\n", "- `Song`: should contain attributes `title` (string), `artist` (string) and `album_title` (string).\n", - "- `Playlist`: should contain attributes `name` (string) and `songs` (a list of `Song` instances). It should also include a method for adding a song to the playlist.\n", - "- `User`: should contain attributes `username` (string) and `playlists` (a dict where key is the name of the playlist and value is a `Playlist` instance). It should also include a method for creating a playlist and a method for adding a specific song to a specific playlist.\n", + "- `Playlist`: should contain attributes `name` (string) and `songs` (a list of `Song` instances).\n", + " It should also include a method for adding a song to the playlist.\n", + "- `User`: should contain attributes `username` (string) and `playlists` (a dict where the key is the name of the playlist and the value is a `Playlist` instance).\n", + " It should also include a method for creating a playlist and a method for adding a specific song to a specific playlist.\n", "\n", "
\n", "

Question

\n", @@ -1561,7 +1501,7 @@ { "cell_type": "code", "execution_count": null, - "id": "107", + "id": "100", "metadata": {}, "outputs": [], "source": [ @@ -1612,22 +1552,26 @@ }, { "cell_type": "markdown", - "id": "108", + "id": "101", "metadata": {}, - "source": [ - "### The N-body problem" - ] + "source": [] }, { "cell_type": "markdown", - "id": "109", + "id": "102", "metadata": {}, "source": [ - "On a boring and rainy Sunday afternoon, you decide that you want to attempt writing a Python program that simulates the orbits of Jupiter's moons. To start with, you decide to focus your efforts on tracking just **four** of the largest moons: Io, Europa, Ganymede, and Callisto.\n", + "### The N-body problem\n", + "\n", + "On a boring and rainy Sunday afternoon, you decide to attempt to write a Python program that simulates the orbits of Jupiter's moons.\n", + "First, you focus your efforts on tracking just **four** of the largest moons: Io, Europa, Ganymede, and Callisto.\n", "\n", - "After a brief scan and some careful calculations, you successfully record the **position of each moon in a 3-dimensional space**. You set each moon's velocity to `0` in each direction, and the starting point for their orbits. Your next task is to simulate their motion over time, so you can avoid any potential collisions.\n", + "After a brief scan and some careful calculations, you successfully record the **position of each moon in a 3-dimensional space**.\n", + "You set each moon's velocity to `0` in each direction and the starting point for their orbits.\n", + "Your next task is to simulate their motion over time, so you can avoid any potential collisions.\n", "\n", - "You can simulate the motion of the moons in **time steps**. At each time step, first update the **velocity** of evey moon by computing the **gravity interaction** with the other moons. Then, once all the velocities are up to date, you can update the **position** of every moon by applying their velocities. Afterwards, your simulation can advance by one time step.\n", + "You can simulate the motion of the moons in **time steps**. At each time step, first update the **velocity** of every moon by computing the **gravity interaction** with the other moons.\n", + "Then, once all the velocities are up to date, you can update the **position** of every moon by applying their velocities. Afterwards, your simulation can advance by one-time step.\n", "\n", "For example, one possible starting configuration of the moons is\n", "\n", @@ -1641,19 +1585,20 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "103", "metadata": {}, "source": [ "
\n", "

Hint

\n", - " Write a Python class called Moon that stores all the properties of a single moon. You should have two lists of integers, one for the positions and one for the velocities.\n", + " Write a Python class called Moon that stores all the properties of a single moon.\n", + " You should have two lists of integers, one for the positions and one for the velocities.\n", "
\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "111", + "id": "104", "metadata": { "tags": [] }, @@ -1666,7 +1611,7 @@ }, { "cell_type": "markdown", - "id": "112", + "id": "105", "metadata": {}, "source": [ "
\n", @@ -1677,7 +1622,7 @@ }, { "cell_type": "markdown", - "id": "113", + "id": "106", "metadata": {}, "source": [ "Each of the strings output by your solution function below should be something like\n", @@ -1691,7 +1636,7 @@ { "cell_type": "code", "execution_count": null, - "id": "114", + "id": "107", "metadata": { "tags": [] }, @@ -1705,7 +1650,7 @@ }, { "cell_type": "markdown", - "id": "115", + "id": "108", "metadata": {}, "source": [ "---\n", @@ -1718,21 +1663,23 @@ "To perform a simulation, proceed as follows:\n", "\n", "1. Consider every **pair** of moons. On each axis, the velocity changes by **exactly `+1` or `-1`**\n", - "\n", - "2. To determine the sign of the velocity change, consider the moons' positions. For example, if `G` stands for Ganymede and `C` for Callisto:\n", + "1. To determine the sign of the velocity change, consider the moons' positions.\n", + " For example, if `G` stands for Ganymede and `C` for Callisto:\n", "\n", " * If `Gx = 3` (the `x` position of Ganymede) and `Cx = 5`, then Ganymede's `x` velocity changes by `+1` (because `5 > 3`), and Callisto's `x` velocity must change by `-1` (because `3 < 5`).\n", " * If the positions on a given axis **are the same**, then the velocity on that axis doesn't change at all.\n", " \n", - "3. Once the gravity has been calculated and the velocity updated, we should also update the position: simply **add the velocity** of each moon to its current position. For example, if Europa's position is `x=1, y=2, z=3` and its velocity `x=-2, y=0, z=3`, then the new position would be `x=-1, y=2, z=6`." + "1. Once the gravity has been calculated and the velocity updated, we should also update the position: simply **add the velocity** of each moon to its current position.\n", + " For example, if Europa's position is `x=1, y=2, z=3` and its velocity `x=-2, y=0, z=3`, then the new position would be `x=-1, y=2, z=6`." ] }, { "cell_type": "markdown", - "id": "116", + "id": "109", "metadata": {}, "source": [ - "To have a complete account of the moons' orbits, you need to compute the **total energy of the system**. The total energy for a single moon is its **potential energy** multiplied by its **kinetic energy**.\n", + "To have a complete account of the moons' orbits, you need to compute the **total energy of the system**.\n", + "The total energy for a single moon is its **potential energy** multiplied by its **kinetic energy**.\n", "\n", "The energies are defined as follows:\n", "\n", @@ -1771,19 +1718,20 @@ }, { "cell_type": "markdown", - "id": "117", + "id": "110", "metadata": {}, "source": [ "
\n", "

Hint

\n", - " You should create another class called Universe which contains all the moons in your system, and a method evolve() that performs the evolution of a single time step. You should also add a method that computes the total energy.\n", + " You should create another class called Universe which contains all the moons in your system, and a method evolve() that performs the evolution of a single time step.\n", + " You should also add a method that computes the total energy.\n", "
" ] }, { "cell_type": "code", "execution_count": null, - "id": "118", + "id": "111", "metadata": { "tags": [] }, @@ -1796,7 +1744,7 @@ }, { "cell_type": "markdown", - "id": "119", + "id": "112", "metadata": {}, "source": [ "
\n", @@ -1812,7 +1760,7 @@ }, { "cell_type": "markdown", - "id": "120", + "id": "113", "metadata": {}, "source": [ "
\n", @@ -1824,7 +1772,7 @@ { "cell_type": "code", "execution_count": null, - "id": "121", + "id": "114", "metadata": { "tags": [] }, @@ -1875,7 +1823,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.10" } }, "nbformat": 4, From 15a8a550b3b140c25a7cdd55772155e37e9423d8 Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou Date: Fri, 9 May 2025 00:25:30 +0200 Subject: [PATCH 6/7] fixes after review --- .../object_oriented_programming_advanced.py | 17 ++--------------- ...t_13_object_oriented_programming_advanced.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/tutorial/quiz/object_oriented_programming_advanced.py b/tutorial/quiz/object_oriented_programming_advanced.py index 317bf1b0..e9847453 100644 --- a/tutorial/quiz/object_oriented_programming_advanced.py +++ b/tutorial/quiz/object_oriented_programming_advanced.py @@ -148,19 +148,6 @@ def __init__(self, title=""): ) q5 = Question( - question="Which decorator is used to define a method that belongs to the class rather than an instance?", - options={ - "@staticmethod": "Incorrect. A static method does not belong to the class or instance.", - "@classmethod": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", - "@property": "Incorrect. The `@property` decorator is used to define getter methods.", - "@abstractmethod": "Incorrect. The `@abstractmethod` decorator is used in abstract classes.", - }, - correct_answer="@classmethod", - hint="This method takes `cls` as its first parameter.", - shuffle=True, - ) - - q6 = Question( question="What is the purpose of the `@classmethod` decorator?", options={ "To define a method that belongs to the class rather than an instance": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", @@ -173,7 +160,7 @@ def __init__(self, title=""): shuffle=True, ) - q7 = Question( + q6 = Question( question="What is the difference between `@staticmethod` and `@classmethod`?", options={ "`@staticmethod` does not access the class or instance, while `@classmethod` takes `cls` as its first parameter": "Correct! This is the key difference between the two decorators.", @@ -186,7 +173,7 @@ def __init__(self, title=""): shuffle=True, ) - super().__init__(questions=[q1, q2, q3, q4, q5, q6, q7]) + super().__init__(questions=[q1, q2, q3, q4, q5, q6]) class OopAdvancedEncapsulation(Quiz): diff --git a/tutorial/tests/test_13_object_oriented_programming_advanced.py b/tutorial/tests/test_13_object_oriented_programming_advanced.py index 4acbb16d..061f910d 100644 --- a/tutorial/tests/test_13_object_oriented_programming_advanced.py +++ b/tutorial/tests/test_13_object_oriented_programming_advanced.py @@ -61,8 +61,16 @@ def validate_child_eye_color(solution_result): attrs = list(vars(solution_result)) except TypeError: raise SubAssertionError from None - assert len(attrs) == 1, "The class should have 1 attribute." - assert "eye_color" in attrs, "The class attribute should be 'eye_color'." + assert len(attrs) == 3, "The class should have 3 attributes." + assert "eye_color" in attrs, ( + "The class should have an attribute called 'eye_color'." + ) + assert "eye_color_mother" in attrs, ( + "The class should have an attribute called 'eye_color_mother'." + ) + assert "eye_color_father" in attrs, ( + "The class should have an attribute called 'eye_color_father'." + ) @pytest.mark.parametrize( From cb6f8b7bdcbc7b3c5fa182674837aa776e4c9983 Mon Sep 17 00:00:00 2001 From: Edoardo Baldi Date: Fri, 9 May 2025 00:29:44 +0200 Subject: [PATCH 7/7] Fix remaining things * A few more details on class decorators * Few typos * Link to `attrs` * Removed redundant quiz * Test, exercise 1: fix return type hint --- 13_object_oriented_programming_advanced.ipynb | 460 +++++++++++------- .../object_oriented_programming_advanced.py | 17 +- ...13_object_oriented_programming_advanced.py | 2 +- 3 files changed, 274 insertions(+), 205 deletions(-) diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index 99369e01..cd0c03c5 100644 --- a/13_object_oriented_programming_advanced.ipynb +++ b/13_object_oriented_programming_advanced.ipynb @@ -162,7 +162,7 @@ "id": "10", "metadata": {}, "source": [ - "Now we can create instances of `Dog` and `Cat`: " + "Now we can create instances of `Dog` and `Cat`:" ] }, { @@ -236,14 +236,23 @@ "It is also possible for a class to be derived from **more than one base classes** in Python.\n", "This is called multiple inheritance.\n", "\n", - "Let's see the example of a very famous dog who also happens to be a detective.\n", - "To do that, we first define a new class:" + "Let's see the example of a very famous dog who also happens to be a detective:" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "To do that, we first define a new class." ] }, { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -257,7 +266,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "Then, we create a derived class that inherits from two base classes. Notice that:\n", @@ -268,7 +277,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -276,14 +285,16 @@ " def __init__(self, name, topic):\n", " Dog.__init__(self, name)\n", " Detective.__init__(self, topic)\n", - " \n", + "\n", " def detective_dog_intro(self):\n", - " print(f\"This detective is a dog called {self.name}. He solves mysteries about {self.topic}.\")" + " print(\n", + " f\"This detective is a dog called {self.name}. He solves mysteries about {self.topic}.\"\n", + " )" ] }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "We can also call all methods inherited from each parent." @@ -292,11 +303,11 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ - "scooby = DetectiveDog('Scooby Doo', 'ghosts')\n", + "scooby = DetectiveDog(\"Scooby Doo\", \"ghosts\")\n", "\n", "scooby.speak()\n", "scooby.detective_intro()\n", @@ -305,7 +316,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "24", "metadata": {}, "source": [ "While multiple inheritance can be powerful, it can also lead to complexities and potential conflicts, so it should only be used when really needed.\n", @@ -314,7 +325,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "### Composition\n", @@ -331,7 +342,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -356,7 +367,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "27", "metadata": {}, "source": [ "Then we can create a car, which has one engine and four wheels.\n", @@ -368,14 +379,14 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ "class Car:\n", " def __init__(self):\n", " self.engine = Engine()\n", - " self.wheels = [Wheel(i+1) for i in range(4)]\n", + " self.wheels = [Wheel(i + 1) for i in range(4)]\n", "\n", " def start(self):\n", " print(f\"Car starting: {self.engine.start()}\")\n", @@ -385,13 +396,13 @@ " def stop(self):\n", " print(f\"Car stopping: {self.engine.stop()}\")\n", " for wheel in self.wheels:\n", - " print(wheel.stop()) " + " print(wheel.stop())" ] }, { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -403,7 +414,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "### `super()`\n", @@ -413,13 +424,13 @@ "This is useful for accessing inherited methods that have been overridden in a class.\n", "\n", "Let's re-write class `Dog`, which is a subclass of `Animal`.\n", - "This time it not only overwrites the method `speak()`, but it also demonstrates how to call the parent's method, with the use of `super()`." + "This time, it doesn't only overwrite the method `speak()`, but it also demonstrates how to call the parent's method, using `super()`." ] }, { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -434,7 +445,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -449,18 +460,18 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ - "dog = Dog('Rex')\n", + "dog = Dog(\"Rex\")\n", "dog.speak()\n", "dog.parent_speak()" ] }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "### Quiz on Inheritance" @@ -469,17 +480,18 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", "oopa.OopAdvancedInheritance()" ] }, { "cell_type": "markdown", - "id": "35", + "id": "36", "metadata": {}, "source": [ "### Exercise: Child Eye Color\n", @@ -505,7 +517,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -515,7 +527,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -539,7 +551,7 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "39", "metadata": {}, "source": [ "## Abstract Classes\n", @@ -557,12 +569,13 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ "from abc import ABC, abstractmethod\n", "\n", + "\n", "class Shape(ABC):\n", " @abstractmethod\n", " def area(self):\n", @@ -575,9 +588,11 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "41", "metadata": {}, "source": [ + "We mark methods of an abstract class as **virtual** – meaning that they must be overridden by each subclass that inherits from the abstract parent – using the `abstractmethod()` decorator.\n", + "\n", "Careful, you **cannot** create an instance of an abstract class!\n", "The following line raises an error:" ] @@ -585,7 +600,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -594,7 +609,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "Let's create two concrete subclasses of `Shape`:" @@ -603,7 +618,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -612,7 +627,7 @@ " self.radius = radius\n", "\n", " def area(self):\n", - " return 3.14 * self.radius ** 2\n", + " return 3.14 * self.radius**2\n", "\n", " def perimeter(self):\n", " return 2 * 3.14 * self.radius\n", @@ -632,7 +647,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "45", "metadata": {}, "source": [ "Now we are allowed to create instances of the subclasses and also call their methods:" @@ -641,7 +656,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -656,7 +671,7 @@ }, { "cell_type": "markdown", - "id": "46", + "id": "47", "metadata": {}, "source": [ "### Quiz on Abstraction" @@ -665,17 +680,18 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", "oopa.OopAdvancedAbstractClasses()" ] }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ "### Exercise: Banking System\n", @@ -726,7 +742,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -736,13 +752,14 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "51", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "from abc import ABC, abstractmethod\n", "\n", + "\n", "def solution_banking_system(tax_rate: float, interest_rate: float) -> list:\n", " \"\"\"\n", " Defines abstract class `Account` with attributes `account_number` and `balance`, and methods `credit()`, `get_balance()`, and `debit()`.\n", @@ -761,7 +778,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "52", "metadata": {}, "source": [ "## Decorators\n", @@ -774,20 +791,29 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "53", "metadata": {}, "source": [ "### @classmethod\n", "Defines a class method, which is a method bound to the class rather than its instances.\n", "However, class methods can be called by both class and object.\n", "It takes the class itself (named `cls`) as its first parameter, allowing you to access and modify class-level attributes and methods.\n", - "These changes would apply across all the instances of the class." + "These changes would apply across all the instances of the class.\n", + "\n", + "Common use cases for class methods include:\n", + "1. Alternative constructors (factory methods) that return instances in different ways (e.g., create a new instance from a built-in type or another class)\n", + "2. Tracking class-wide statistics (e.g., counting instances)\n", + "3. Modifying class attributes that affect all instances at once\n", + "4. Implementing design patterns (e.g., Factory, Singleton)\n", + "\n", + "Unlike static methods (`@staticmethod`, see below), class methods are aware of the class they belong to\n", + "and work properly with inheritance (they receive the actual subclass when called from a subclass)." ] }, { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -801,9 +827,9 @@ "\n", " @classmethod\n", " def count(cls, gender):\n", - " if gender == 'M':\n", + " if gender == \"M\":\n", " cls.number_of_males += 1\n", - " elif gender == 'F':\n", + " elif gender == \"F\":\n", " cls.number_of_females += 1\n", " cls.number_of_total += 1\n", "\n", @@ -811,11 +837,14 @@ " def statistics(cls):\n", " male_percentage = cls.number_of_males / cls.number_of_total * 100\n", " female_percentage = cls.number_of_females / cls.number_of_total * 100\n", - " print(f\"There are {cls.number_of_total} persons: {cls.number_of_males} are Male & {cls.number_of_females} are Female.\")\n", + " print(\n", + " f\"There are {cls.number_of_total} persons: {cls.number_of_males} are Male & {cls.number_of_females} are Female.\"\n", + " )\n", " print(f\"So {male_percentage}% are Male & {female_percentage}% are Female.\")\n", "\n", + "\n", "persons = []\n", - "for gender in ['M', 'F', 'F', 'M', 'F']:\n", + "for gender in [\"M\", \"F\", \"F\", \"M\", \"F\"]:\n", " persons.append(Person(gender))\n", "\n", "for person in persons:\n", @@ -826,31 +855,48 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "55", "metadata": {}, "source": [ "### @staticmethod\n", "Defines a static method, which is also a method bound to the class, but does not have access to the class or instance.\n", "Hence, it cannot modify the class state.\n", "Static methods are typically used for utility functions that are related to the class but don't need access to instance-specific data.\n", - "A static method does not receive an implicit first argument.\n", + "A static method does not receive an implicit first argument like `self` or `cls`.\n", "\n", + "Unlike regular methods (which receive `self`) and class methods (which receive `cls`), static methods behave like regular functions\n", + "but are defined within the class namespace for organizational purposes." + ] + }, + { + "cell_type": "markdown", + "id": "56", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ "As seen in the example below, static methods have limited use, because they don't have access neither to the class attributes nor to any instance of the class.\n", "They **cannot access** `cls` or `self`.\n", "\n", - "However, they can be useful to group utilities together with a class.\n", - "They improve code readability and allow for method overriding." + "However, they can be useful for:\n", + "1. Grouping utility functions logically with the class they relate to\n", + "2. Providing helper functions that operate on class-related data but don't need class state\n", + "3. Creating pure functions with no side effects on class or instance state\n", + "4. Improving code organization and readability\n", + "5. Enabling method overriding in subclasses\n", + "\n", + "Choose static methods over standalone functions when the functionality is **conceptually tied to the class**,\n", + "even if it doesn't require class state." ] }, { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "57", "metadata": {}, "outputs": [], "source": [ "class MathOperations:\n", - "\n", " @staticmethod\n", " def add(x, y):\n", " return x + y\n", @@ -859,13 +905,14 @@ " def subtract(x, y):\n", " return x - y\n", "\n", + "\n", "print(MathOperations.add(2, 3))\n", "print(MathOperations.subtract(2, 3))" ] }, { "cell_type": "markdown", - "id": "56", + "id": "58", "metadata": {}, "source": [ "### @property\n", @@ -882,7 +929,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -896,7 +943,8 @@ "\n", " @property\n", " def area(self):\n", - " return 3.14 * self._radius ** 2\n", + " return 3.14 * self._radius**2\n", + "\n", "\n", "circle = Circle(5)\n", "print(\"Radius:\", circle.radius)" @@ -904,7 +952,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "60", "metadata": {}, "source": [ "We can access the `area` property, just like any other class attribute.\n", @@ -914,7 +962,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -923,7 +971,7 @@ }, { "cell_type": "markdown", - "id": "60", + "id": "62", "metadata": {}, "source": [ "### Setters & Getters\n", @@ -941,7 +989,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -967,7 +1015,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "64", "metadata": {}, "source": [ "Create an instance and use the getter:" @@ -976,7 +1024,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -986,7 +1034,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "66", "metadata": {}, "source": [ "Update the radius using the setter:" @@ -995,7 +1043,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "67", "metadata": {}, "outputs": [], "source": [ @@ -1006,7 +1054,7 @@ }, { "cell_type": "markdown", - "id": "66", + "id": "68", "metadata": {}, "source": [ "What happens when we enter an invalid value?" @@ -1015,7 +1063,7 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "69", "metadata": {}, "outputs": [], "source": [ @@ -1024,7 +1072,7 @@ }, { "cell_type": "markdown", - "id": "68", + "id": "70", "metadata": {}, "source": [ "Finally let's use the deleter:" @@ -1033,16 +1081,16 @@ { "cell_type": "code", "execution_count": null, - "id": "69", + "id": "71", "metadata": {}, "outputs": [], "source": [ - "del circle.radius " + "del circle.radius" ] }, { "cell_type": "markdown", - "id": "70", + "id": "72", "metadata": {}, "source": [ "We are no longer able to access the deleted attribute:" @@ -1051,7 +1099,7 @@ { "cell_type": "code", "execution_count": null, - "id": "71", + "id": "73", "metadata": {}, "outputs": [], "source": [ @@ -1060,7 +1108,7 @@ }, { "cell_type": "markdown", - "id": "72", + "id": "74", "metadata": {}, "source": [ "### Quiz on Decorators" @@ -1069,17 +1117,18 @@ { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "75", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", "oopa.OopAdvancedDecorators()" ] }, { "cell_type": "markdown", - "id": "74", + "id": "76", "metadata": {}, "source": [ "## Encapsulation\n", @@ -1100,7 +1149,7 @@ }, { "cell_type": "markdown", - "id": "75", + "id": "77", "metadata": {}, "source": [ "### Public\n", @@ -1109,11 +1158,11 @@ }, { "cell_type": "markdown", - "id": "76", + "id": "78", "metadata": {}, "source": [ "### Private\n", - "Attributes and methods with names starting with a **double underscore** (e.g., `__variable`, `__method()`) are considered private. \n", + "Attributes and methods with names starting with a **double underscore** (e.g., `__variable`, `__method()`) are considered private.\n", "They are not intended to be accessed directly from outside the class.\n", "However, Python does not enforce strict access control, so you can still access them using **name mangling** (e.g., `_classname__variable`)." ] @@ -1121,7 +1170,7 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "id": "79", "metadata": {}, "outputs": [], "source": [ @@ -1132,6 +1181,7 @@ " def public_method(self):\n", " return self.__private_var\n", "\n", + "\n", "obj = MyClass()\n", "\n", "print(obj.public_method())" @@ -1139,11 +1189,11 @@ }, { "cell_type": "markdown", - "id": "78", + "id": "80", "metadata": {}, "source": [ "### Protected\n", - "Attributes and methods with names starting with a **single underscore** (e.g., `_variable`, `_method()`) are considered protected. \n", + "Attributes and methods with names starting with a **single underscore** (e.g., `_variable`, `_method()`) are considered protected.\n", "This is a convention to indicate that they should not be accessed directly from outside the class, but there's no strict enforcement.\n", "\n", "As seen in the example below, you can access a protected attribute both ways:" @@ -1152,7 +1202,7 @@ { "cell_type": "code", "execution_count": null, - "id": "79", + "id": "81", "metadata": {}, "outputs": [], "source": [ @@ -1163,14 +1213,15 @@ " def public_method(self):\n", " return self._protected_var\n", "\n", + "\n", "obj = MyClass()\n", "print(obj.public_method())\n", - "print(obj._protected_var) " + "print(obj._protected_var)" ] }, { "cell_type": "markdown", - "id": "80", + "id": "82", "metadata": {}, "source": [ "### Quiz on Encapsulation" @@ -1179,17 +1230,18 @@ { "cell_type": "code", "execution_count": null, - "id": "81", + "id": "83", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", "oopa.OopAdvancedEncapsulation()" ] }, { "cell_type": "markdown", - "id": "82", + "id": "84", "metadata": {}, "source": [ "## How to write better classes\n", @@ -1199,10 +1251,10 @@ }, { "cell_type": "markdown", - "id": "83", + "id": "85", "metadata": {}, "source": [ - "### Using dataclasses\n", + "### Using `dataclasses`\n", "\n", "Assume we are implementing a simple class to represent a `Person`, it would look something like this:" ] @@ -1210,7 +1262,7 @@ { "cell_type": "code", "execution_count": null, - "id": "84", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -1223,7 +1275,7 @@ }, { "cell_type": "markdown", - "id": "85", + "id": "87", "metadata": {}, "source": [ "A simpler way, however, would be to import `dataclass` from the `dataclasses` module.\n", @@ -1234,12 +1286,13 @@ { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "88", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "\n", + "\n", "@dataclass\n", "class Person:\n", " name: str\n", @@ -1249,7 +1302,7 @@ }, { "cell_type": "markdown", - "id": "87", + "id": "89", "metadata": {}, "source": [ "Now, with the use of these auto generated methods, we can create an instance of the class and print a representation of the object, without any additional code.\n", @@ -1259,12 +1312,12 @@ { "cell_type": "code", "execution_count": null, - "id": "88", + "id": "90", "metadata": {}, "outputs": [], "source": [ - "john = Person('John', 25, 1.75)\n", - "jane = Person('Jane', 25, 1.75)\n", + "john = Person(\"John\", 25, 1.75)\n", + "jane = Person(\"Jane\", 25, 1.75)\n", "\n", "print(john)\n", "print(jane)\n", @@ -1273,16 +1326,31 @@ }, { "cell_type": "markdown", - "id": "89", + "id": "91", "metadata": {}, "source": [ - "### Using attrs\n", + "### Using `attrs`\n", "\n", - "This Python package is for creating well-defined classes with a type, attributes and methods.\n", + "This [Python package](https://pypi.org/project/attrs/) is for creating well-defined classes with a type, attributes and methods.\n", "When defining a class, it will add static methods to that class based on the attributes you declare.\n", + "\n", "`attrs` will operate only on the dunder methods of your class.\n", "Hence, all of its tools will live in functions that operate on top of instances.\n", "\n", + "Among the advantages of choosing `attrs` over the built-in `dataclasses`:\n", + "1. **Validators**: `attrs` offers built-in attribute validation to enforce constraints on values\n", + "2. **Converters**: Can automatically convert input values to the desired type during initialization\n", + "3. **Performance**: Generally has better performance characteristics, especially for classes with many attributes\n", + "\n", + "Other benefits include more flexible customization options, comprehensive hooks for attribute lifecycle,\n", + "and better integration with older Python versions (dataclasses require Python 3.7+)." + ] + }, + { + "cell_type": "markdown", + "id": "92", + "metadata": {}, + "source": [ "Let's rewrite the previous example, this time using `attrs`.\n", "You will notice that it offers the same functionalities, i.e. `__init__()`, `__repr__()`, and object comparison." ] @@ -1290,7 +1358,7 @@ { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "93", "metadata": {}, "outputs": [], "source": [ @@ -1302,8 +1370,9 @@ " age: int\n", " height: float\n", "\n", - "john = Person('John', 25, 1.75)\n", - "jane = Person('Jane', 25, 1.75)\n", + "\n", + "john = Person(\"John\", 25, 1.75)\n", + "jane = Person(\"Jane\", 25, 1.75)\n", "\n", "print(john)\n", "print(jane)\n", @@ -1312,7 +1381,7 @@ }, { "cell_type": "markdown", - "id": "91", + "id": "94", "metadata": {}, "source": [ "However, `attrs` also provides **validators**.\n", @@ -1324,12 +1393,13 @@ { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "95", "metadata": {}, "outputs": [], "source": [ "from attrs import define, field\n", "\n", + "\n", "@define\n", "class Person:\n", " name: str\n", @@ -1341,12 +1411,13 @@ " if value < 1:\n", " raise ValueError(\"Age must be greater than 0\")\n", "\n", - "john = Person('John', 0, 1.75)" + "\n", + "john = Person(\"John\", 0, 1.75)" ] }, { "cell_type": "markdown", - "id": "93", + "id": "96", "metadata": {}, "source": [ "### Quiz on `attrs` and `dataclasses`" @@ -1355,17 +1426,18 @@ { "cell_type": "code", "execution_count": null, - "id": "94", + "id": "97", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", "oopa.OopAdvancedAttrsDataclasses()" ] }, { "cell_type": "markdown", - "id": "95", + "id": "98", "metadata": {}, "source": [ "## Exercises" @@ -1374,7 +1446,7 @@ { "cell_type": "code", "execution_count": null, - "id": "96", + "id": "99", "metadata": {}, "outputs": [], "source": [ @@ -1383,7 +1455,7 @@ }, { "cell_type": "markdown", - "id": "97", + "id": "100", "metadata": {}, "source": [ "### Store Inventory\n", @@ -1423,25 +1495,16 @@ { "cell_type": "code", "execution_count": null, - "id": "98", + "id": "101", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", - "pc = {\n", - " \"name\": \"pc_1\",\n", - " \"price\": 1500,\n", - " \"quantity\": 1,\n", - " \"expansion_slots\": 2\n", - "}\n", + "pc = {\"name\": \"pc_1\", \"price\": 1500, \"quantity\": 1, \"expansion_slots\": 2}\n", + "\n", + "laptop = {\"name\": \"laptop_1\", \"price\": 1200, \"quantity\": 4, \"battery_life\": 6}\n", "\n", - "laptop = {\n", - " \"name\": \"laptop_1\",\n", - " \"price\": 1200,\n", - " \"quantity\": 4,\n", - " \"battery_life\": 6\n", - "}\n", "\n", "def solution_store_inventory(pc: dict, laptop: dict) -> list:\n", " \"\"\"\n", @@ -1468,7 +1531,7 @@ }, { "cell_type": "markdown", - "id": "99", + "id": "102", "metadata": {}, "source": [ "### Music Streaming Service\n", @@ -1501,7 +1564,7 @@ { "cell_type": "code", "execution_count": null, - "id": "100", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1511,21 +1574,20 @@ " {\n", " \"title\": \"Bohemian Rhapsody\",\n", " \"artist\": \"Queen\",\n", - " \"album_title\": \"A Night at the Opera\"\n", + " \"album_title\": \"A Night at the Opera\",\n", " },\n", " {\n", " \"title\": \"We Will Rock You\",\n", " \"artist\": \"Queen\",\n", - " \"album_title\": \"News of the World\"\n", - " },\n", - " {\n", - " \"title\": \"I Want to Break Free\",\n", - " \"artist\": \"Queen\",\n", - " \"album_title\": \"The Works\"\n", + " \"album_title\": \"News of the World\",\n", " },\n", + " {\"title\": \"I Want to Break Free\", \"artist\": \"Queen\", \"album_title\": \"The Works\"},\n", "]\n", "\n", - "def solution_music_streaming_service(song_info: list[dict], username: str, playlist_name: str):\n", + "\n", + "def solution_music_streaming_service(\n", + " song_info: list[dict], username: str, playlist_name: str\n", + "):\n", " \"\"\"\n", " Creates a music streaming service system using composition, including classes for `Song`, `Playlist`, and `User`:\n", " - `Song` represents a song with:\n", @@ -1552,13 +1614,13 @@ }, { "cell_type": "markdown", - "id": "101", + "id": "104", "metadata": {}, "source": [] }, { "cell_type": "markdown", - "id": "102", + "id": "105", "metadata": {}, "source": [ "### The N-body problem\n", @@ -1585,7 +1647,7 @@ }, { "cell_type": "markdown", - "id": "103", + "id": "106", "metadata": {}, "source": [ "
\n", @@ -1598,20 +1660,21 @@ { "cell_type": "code", "execution_count": null, - "id": "104", + "id": "107", "metadata": { + "lines_to_next_cell": 2, "tags": [] }, "outputs": [], "source": [ "class Moon:\n", " \"\"\"A class for a moon\"\"\"\n", - " # Write here your implementation here of the Moon class" + " # Write here your implementation here of the Moon class\n" ] }, { "cell_type": "markdown", - "id": "105", + "id": "108", "metadata": {}, "source": [ "
\n", @@ -1622,7 +1685,7 @@ }, { "cell_type": "markdown", - "id": "106", + "id": "109", "metadata": {}, "source": [ "Each of the strings output by your solution function below should be something like\n", @@ -1636,46 +1699,48 @@ { "cell_type": "code", "execution_count": null, - "id": "107", + "id": "110", "metadata": { + "lines_to_next_cell": 2, "tags": [] }, "outputs": [], "source": [ "%%ipytest\n", "def solution_moons(moons: str) -> list[str]:\n", - " # Write your solution here\n", - " pass" + " # Write your solution here\n" ] }, { "cell_type": "markdown", - "id": "108", + "id": "111", "metadata": {}, "source": [ - "---\n", - "\n", "
\n", "

Heads-up

\n", " Please, proceed with the next part only if you completed successfully the first part above.\n", "
\n", "\n", - "To perform a simulation, proceed as follows:\n", + "Each \"simulation step\" consists of two phases:\n", "\n", - "1. Consider every **pair** of moons. On each axis, the velocity changes by **exactly `+1` or `-1`**\n", - "1. To determine the sign of the velocity change, consider the moons' positions.\n", - " For example, if `G` stands for Ganymede and `C` for Callisto:\n", + "1. **Velocity Update Phase**:\n", + " - For each **pair** of bodies (A and B), compare their positions on each axis (x, y, z)\n", + " - Update their velocities according to gravitational effects:\n", + " * If A's position on an axis is less than B's (e.g., Ax < Bx), then A's velocity increases by +1 and B's decreases by -1\n", + " * If A's position is greater than B's (e.g., Ax > Bx), then A's velocity decreases by -1 and B's increases by +1\n", + " * If their positions on an axis are equal, velocities on that axis remain unchanged\n", + " - Apply this comparison independently for each axis (x, y, z)\n", "\n", - " * If `Gx = 3` (the `x` position of Ganymede) and `Cx = 5`, then Ganymede's `x` velocity changes by `+1` (because `5 > 3`), and Callisto's `x` velocity must change by `-1` (because `3 < 5`).\n", - " * If the positions on a given axis **are the same**, then the velocity on that axis doesn't change at all.\n", - " \n", - "1. Once the gravity has been calculated and the velocity updated, we should also update the position: simply **add the velocity** of each moon to its current position.\n", - " For example, if Europa's position is `x=1, y=2, z=3` and its velocity `x=-2, y=0, z=3`, then the new position would be `x=-1, y=2, z=6`." + "2. **Position Update Phase**:\n", + " - After all velocity updates are complete for all pairs of bodies, update each body's position\n", + " - Simply add the body's velocity vector to its position vector\n", + " - For example, if a body is at position (x=3, y=1, z=2) with velocity (vx=-2, vy=0, vz=1),\n", + " its new position will be (x=1, y=1, z=3)" ] }, { "cell_type": "markdown", - "id": "109", + "id": "112", "metadata": {}, "source": [ "To have a complete account of the moons' orbits, you need to compute the **total energy of the system**.\n", @@ -1718,7 +1783,7 @@ }, { "cell_type": "markdown", - "id": "110", + "id": "113", "metadata": {}, "source": [ "
\n", @@ -1731,36 +1796,49 @@ { "cell_type": "code", "execution_count": null, - "id": "111", + "id": "114", "metadata": { + "lines_to_next_cell": 2, "tags": [] }, "outputs": [], "source": [ "class Universe:\n", " \"\"\"A class for a universe\"\"\"\n", - " # Write here your implementation here of the Universe class " + " # Write here your implementation here of the Universe class\n" ] }, { "cell_type": "markdown", - "id": "112", + "id": "115", "metadata": {}, "source": [ "
\n", - "

Hint

\n", - " Your solution function reads an input string that represent the starting point of the Universe. It's a string like the following:\n", - "
Ganymede: x=-1, y=0, z=2\n",
-    "Io: x=2, y=-10, z=-7\n",
-    "Europa: x=4, y=-8, z=8\n",
-    "Callisto: x=3, y=5, z=-1\n",
-    "
\n", - "
" + "

Hint

\n", + "Your solution function reads an input string that represents the starting point of the Universe.\n", + "This input is formatted as a multi-line string where each line describes one moon's initial position.\n", + "Each line follows the format: MoonName: x=, y=, z=\n", + "
\n" ] }, { "cell_type": "markdown", - "id": "113", + "id": "116", + "metadata": {}, + "source": [ + "Here's an example of the input format:\n", + "\n", + "```\n", + "Ganymede: x=-1, y=0, z=2\n", + "Io: x=2, y=-10, z=-7\n", + "Europa: x=4, y=-8, z=8\n", + "Callisto: x=3, y=5, z=-1\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "117", "metadata": {}, "source": [ "
\n", @@ -1772,7 +1850,7 @@ { "cell_type": "code", "execution_count": null, - "id": "114", + "id": "118", "metadata": { "tags": [] }, @@ -1780,27 +1858,31 @@ "source": [ "%%ipytest\n", "\n", + "\n", "def solution_n_body(universe_start: str) -> int:\n", " \"\"\"\n", - " Simulates the motion of moons in a 3-dimensional space over a specified number of time steps\n", - " and calculates the average total energy of the system.\n", - "\n", - " The simulation involves:\n", - " 1. Updating the velocity of each moon based on the gravitational interaction with other moons.\n", - " 2. Updating the position of each moon by applying its velocity.\n", - " 3. Calculating the total energy of the system after the simulation.\n", - "\n", - " Total energy for a single moon is calculated as:\n", - " - Potential energy: The sum of the absolute values of its positional coordinates.\n", - " - Kinetic energy: The sum of the absolute values of its velocity components.\n", - " - Total energy: Potential energy multiplied by kinetic energy.\n", + " Simulate an N-body system of moons in 3D space and return average energy after 1000 steps.\n", + "\n", + " Simulation rules:\n", + " 1. Parse input string to get initial moon positions (all start with velocity 0)\n", + " 2. For 1000 steps:\n", + " a) Update velocities: For each pair of moons and each axis:\n", + " - If pos_A < pos_B: A's velocity +1, B's velocity -1\n", + " - If pos_A > pos_B: A's velocity -1, B's velocity +1\n", + " - If equal: no change\n", + " b) Update positions: Add velocity to position for each moon\n", + " 3. Calculate energy:\n", + " - Potential energy = |x| + |y| + |z|\n", + " - Kinetic energy = |vx| + |vy| + |vz|\n", + " - Moon energy = potential * kinetic\n", + " - Return average of all moons' energies (rounded to integer)\n", "\n", " Args:\n", - " universe_start (str): A string representing the initial positions of the moons in the format:\n", - " \"MoonName: x=, y=, z=\"\n", + " universe_start (str): Initial positions in format \"MoonName: x=, y=, z=\"\n", + " for each moon on separate lines\n", "\n", " Returns:\n", - " int: The average total energy of the system after simulating the universe for 1000 time steps.\n", + " int: Average total energy after 1000 simulation steps\n", " \"\"\"\n", "\n", " return" @@ -1823,7 +1905,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.12.10" } }, "nbformat": 4, diff --git a/tutorial/quiz/object_oriented_programming_advanced.py b/tutorial/quiz/object_oriented_programming_advanced.py index 317bf1b0..e9847453 100644 --- a/tutorial/quiz/object_oriented_programming_advanced.py +++ b/tutorial/quiz/object_oriented_programming_advanced.py @@ -148,19 +148,6 @@ def __init__(self, title=""): ) q5 = Question( - question="Which decorator is used to define a method that belongs to the class rather than an instance?", - options={ - "@staticmethod": "Incorrect. A static method does not belong to the class or instance.", - "@classmethod": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", - "@property": "Incorrect. The `@property` decorator is used to define getter methods.", - "@abstractmethod": "Incorrect. The `@abstractmethod` decorator is used in abstract classes.", - }, - correct_answer="@classmethod", - hint="This method takes `cls` as its first parameter.", - shuffle=True, - ) - - q6 = Question( question="What is the purpose of the `@classmethod` decorator?", options={ "To define a method that belongs to the class rather than an instance": "Correct! A class method belongs to the class and takes `cls` as its first parameter.", @@ -173,7 +160,7 @@ def __init__(self, title=""): shuffle=True, ) - q7 = Question( + q6 = Question( question="What is the difference between `@staticmethod` and `@classmethod`?", options={ "`@staticmethod` does not access the class or instance, while `@classmethod` takes `cls` as its first parameter": "Correct! This is the key difference between the two decorators.", @@ -186,7 +173,7 @@ def __init__(self, title=""): shuffle=True, ) - super().__init__(questions=[q1, q2, q3, q4, q5, q6, q7]) + super().__init__(questions=[q1, q2, q3, q4, q5, q6]) class OopAdvancedEncapsulation(Quiz): diff --git a/tutorial/tests/test_13_object_oriented_programming_advanced.py b/tutorial/tests/test_13_object_oriented_programming_advanced.py index 4acbb16d..4a1bda36 100644 --- a/tutorial/tests/test_13_object_oriented_programming_advanced.py +++ b/tutorial/tests/test_13_object_oriented_programming_advanced.py @@ -15,7 +15,7 @@ def __init__(self): # -def reference_child_eye_color(mother_eye_color: str, father_eye_color: str) -> list: +def reference_child_eye_color(mother_eye_color: str, father_eye_color: str): class Mother: def __init__(self, eye_color: str): self.eye_color_mother = eye_color