diff --git a/13_object_oriented_programming_advanced.ipynb b/13_object_oriented_programming_advanced.ipynb index e60022bf..cd0c03c5 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)" ] }, { @@ -58,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)" ] }, { @@ -66,10 +70,14 @@ "id": "4", "metadata": {}, "source": [ - "## Inheritance\n", + "## Recap\n", "\n", - "Inheritance is a fundamental concept in object-oriented programming (OOP).\n", - "It allows you to create a hierarchy of classes with shared behaviors and attributes." + "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", + "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." ] }, { @@ -77,13 +85,10 @@ "id": "5", "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", - "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", + "## Inheritance\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." + "Inheritance is a fundamental concept in object-oriented programming (OOP).\n", + "It allows you to create a hierarchy of classes with shared behaviors and attributes." ] }, { @@ -91,6 +96,13 @@ "id": "6", "metadata": {}, "source": [ + "### Single Inheritance\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", + "The derived class inherits all the attributes and methods of the base class.\n", + "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,11 +129,8 @@ "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." ] }, @@ -153,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`:" ] }, { @@ -233,7 +242,9 @@ { "cell_type": "markdown", "id": "18", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "To do that, we first define a new class." ] @@ -274,9 +285,11 @@ " 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", + " )" ] }, { @@ -294,7 +307,7 @@ "metadata": {}, "outputs": [], "source": [ - "scooby = DetectiveDog('Scooby Doo', 'ghosts')\n", + "scooby = DetectiveDog(\"Scooby Doo\", \"ghosts\")\n", "\n", "scooby.speak()\n", "scooby.detective_intro()\n", @@ -307,7 +320,6 @@ "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." ] }, @@ -318,26 +330,19 @@ "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", - "\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", + "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", + "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": "26", - "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": "27", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -362,7 +367,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "27", "metadata": {}, "source": [ "Then we can create a car, which has one engine and four wheels.\n", @@ -374,14 +379,14 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "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", @@ -391,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": "30", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -409,30 +414,38 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "30", "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." + "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 doesn't only overwrite the method `speak()`, but it also demonstrates how to call the parent's method, using `super()`." ] }, { - "cell_type": "markdown", - "id": "32", + "cell_type": "code", + "execution_count": null, + "id": "31", "metadata": {}, + "outputs": [], "source": [ - "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()`." + "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": "33", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -441,9 +454,17 @@ " 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": "33", + "metadata": {}, + "outputs": [], + "source": [ + "dog = Dog(\"Rex\")\n", "dog.speak()\n", "dog.parent_speak()" ] @@ -453,35 +474,108 @@ "id": "34", "metadata": {}, "source": [ - "## Abstract Classes\n", + "### Quiz on Inheritance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "from tutorial.quiz import object_oriented_programming_advanced as oopa\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", + "oopa.OopAdvancedInheritance()" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### Exercise: Child Eye Color\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", + "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", - "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." + "
\n", + "

Question

\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\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", + " 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", + " return" ] }, { "cell_type": "markdown", - "id": "35", + "id": "39", "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", + "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", + "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.\n", + "\n", "We first create an abstract class which inherits from `ABC`:" ] }, { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "40", "metadata": {}, "outputs": [], "source": [ "from abc import ABC, abstractmethod\n", "\n", + "\n", "class Shape(ABC):\n", " @abstractmethod\n", " def area(self):\n", @@ -494,9 +588,11 @@ }, { "cell_type": "markdown", - "id": "37", + "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:" ] @@ -504,7 +600,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -513,7 +609,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "43", "metadata": {}, "source": [ "Let's create two concrete subclasses of `Shape`:" @@ -522,7 +618,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -531,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", @@ -551,7 +647,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "45", "metadata": {}, "source": [ "Now we are allowed to create instances of the subclasses and also call their methods:" @@ -560,7 +656,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -575,7 +671,114 @@ }, { "cell_type": "markdown", - "id": "43", + "id": "47", + "metadata": {}, + "source": [ + "### Quiz on Abstraction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", + "oopa.OopAdvancedAbstractClasses()" + ] + }, + { + "cell_type": "markdown", + "id": "49", + "metadata": {}, + "source": [ + "### Exercise: Banking System\n", + "\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", + "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 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 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 including interest.\n", + "\n", + "
\n", + "

Question

\n", + " Using abstraction in Python, create a banking system based on the entities mentioned above.\n", + " \n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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", + " 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" + ] + }, + { + "cell_type": "markdown", + "id": "52", "metadata": {}, "source": [ "## Decorators\n", @@ -588,21 +791,29 @@ }, { "cell_type": "markdown", - "id": "44", + "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", - "\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": "45", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -616,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", @@ -626,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", @@ -641,38 +855,48 @@ }, { "cell_type": "markdown", - "id": "46", + "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", - "\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." + "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": "47", - "metadata": {}, + "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": "48", + "id": "57", "metadata": {}, "outputs": [], "source": [ "class MathOperations:\n", - "\n", " @staticmethod\n", " def add(x, y):\n", " return x + y\n", @@ -681,29 +905,23 @@ " 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": "49", + "id": "58", "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": "50", - "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." ] @@ -711,7 +929,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "59", "metadata": {}, "outputs": [], "source": [ @@ -725,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)" @@ -733,7 +952,7 @@ }, { "cell_type": "markdown", - "id": "52", + "id": "60", "metadata": {}, "source": [ "We can access the `area` property, just like any other class attribute.\n", @@ -743,7 +962,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "61", "metadata": {}, "outputs": [], "source": [ @@ -752,7 +971,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "62", "metadata": {}, "source": [ "### Setters & Getters\n", @@ -770,7 +989,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -796,7 +1015,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "64", "metadata": {}, "source": [ "Create an instance and use the getter:" @@ -805,7 +1024,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -815,7 +1034,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "66", "metadata": {}, "source": [ "Update the radius using the setter:" @@ -824,82 +1043,99 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "# Using the setter methods\n", + "circle.radius = 7\n", + "print(\"Updated Radius:\", circle.radius)" + ] + }, + { + "cell_type": "markdown", + "id": "68", + "metadata": {}, + "source": [ + "What happens when we enter an invalid value?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", "metadata": {}, "outputs": [], "source": [ - "# Using the setter methods\n", - "circle.radius = 7\n", - "print(\"Updated Radius:\", circle.radius)" + "circle.radius = -5" ] }, { "cell_type": "markdown", - "id": "60", + "id": "70", "metadata": {}, "source": [ - "What happens when we enter an invalid value?" + "Finally let's use the deleter:" ] }, { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "71", "metadata": {}, "outputs": [], "source": [ - "circle.radius = -5" + "del circle.radius" ] }, { "cell_type": "markdown", - "id": "62", + "id": "72", "metadata": {}, "source": [ - "Finally let's use the deleter:" + "We are no longer able to access the deleted attribute:" ] }, { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "73", "metadata": {}, "outputs": [], "source": [ - "del circle.radius " + "print(circle.radius)" ] }, { "cell_type": "markdown", - "id": "64", + "id": "74", "metadata": {}, "source": [ - "We are no longer able to access the deleted attribute:" + "### Quiz on Decorators" ] }, { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "75", "metadata": {}, "outputs": [], "source": [ - "print(circle.radius)" + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", + "oopa.OopAdvancedDecorators()" ] }, { "cell_type": "markdown", - "id": "66", + "id": "76", "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", @@ -908,22 +1144,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": "77", + "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": "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", - "\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": "67", + "id": "79", "metadata": {}, "outputs": [], "source": [ @@ -934,6 +1181,7 @@ " def public_method(self):\n", " return self.__private_var\n", "\n", + "\n", "obj = MyClass()\n", "\n", "print(obj.public_method())" @@ -941,11 +1189,11 @@ }, { "cell_type": "markdown", - "id": "68", + "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:" @@ -954,7 +1202,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69", + "id": "81", "metadata": {}, "outputs": [], "source": [ @@ -965,33 +1213,48 @@ " 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": "70", + "id": "82", + "metadata": {}, + "source": [ + "### Quiz on Encapsulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83", "metadata": {}, + "outputs": [], "source": [ - "## How to write better classes" + "from tutorial.quiz import object_oriented_programming_advanced as oopa\n", + "\n", + "oopa.OopAdvancedEncapsulation()" ] }, { "cell_type": "markdown", - "id": "71", + "id": "84", "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": "72", + "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:" ] @@ -999,7 +1262,7 @@ { "cell_type": "code", "execution_count": null, - "id": "73", + "id": "86", "metadata": {}, "outputs": [], "source": [ @@ -1012,25 +1275,24 @@ }, { "cell_type": "markdown", - "id": "74", + "id": "87", "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": "75", + "id": "88", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "\n", + "\n", "@dataclass\n", "class Person:\n", " name: str\n", @@ -1040,7 +1302,7 @@ }, { "cell_type": "markdown", - "id": "76", + "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", @@ -1050,12 +1312,12 @@ { "cell_type": "code", "execution_count": null, - "id": "77", + "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", @@ -1064,20 +1326,29 @@ }, { "cell_type": "markdown", - "id": "78", + "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. When defining a class, it will add static methods to that class based on the attributes you declare.\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." + "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": "79", + "id": "92", "metadata": {}, "source": [ "Let's rewrite the previous example, this time using `attrs`.\n", @@ -1087,7 +1358,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80", + "id": "93", "metadata": {}, "outputs": [], "source": [ @@ -1099,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", @@ -1109,7 +1381,7 @@ }, { "cell_type": "markdown", - "id": "81", + "id": "94", "metadata": {}, "source": [ "However, `attrs` also provides **validators**.\n", @@ -1121,12 +1393,13 @@ { "cell_type": "code", "execution_count": null, - "id": "82", + "id": "95", "metadata": {}, "outputs": [], "source": [ "from attrs import define, field\n", "\n", + "\n", "@define\n", "class Person:\n", " name: str\n", @@ -1138,34 +1411,33 @@ " 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": "83", + "id": "96", "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": "97", "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": "98", "metadata": {}, "source": [ "## Exercises" @@ -1174,7 +1446,7 @@ { "cell_type": "code", "execution_count": null, - "id": "86", + "id": "99", "metadata": {}, "outputs": [], "source": [ @@ -1183,50 +1455,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": "100", "metadata": {}, "source": [ "### Store Inventory\n", @@ -1244,59 +1473,65 @@ "\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", "\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", "
" ] }, { "cell_type": "code", "execution_count": null, - "id": "90", + "id": "101", "metadata": {}, "outputs": [], "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 = {\"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", + "\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" ] }, { "cell_type": "markdown", - "id": "91", + "id": "102", "metadata": {}, "source": [ "### Music Streaming Service\n", @@ -1309,22 +1544,27 @@ "- **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).\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", " 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", "
" ] }, { "cell_type": "code", "execution_count": null, - "id": "92", + "id": "103", "metadata": {}, "outputs": [], "source": [ @@ -1334,115 +1574,66 @@ " {\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]):\n", - " # Write your solution here\n", - " pass" - ] - }, - { - "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", - " Initialize Accounts:\n", - "

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

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

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

    \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", - "
  • \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", - "
  • \n", - "
\n", "\n", - "The solution function should return the balance of the Savings Account after the given number of years.\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", + " - 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", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "94", - "metadata": {}, - "outputs": [], - "source": [ - "%%ipytest\n", - "from abc import ABC, abstractmethod\n", - "from datetime import datetime\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", - "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" + " return" ] }, { "cell_type": "markdown", - "id": "95", + "id": "104", "metadata": {}, - "source": [ - "### The N-body problem" - ] + "source": [] }, { "cell_type": "markdown", - "id": "96", + "id": "105", "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", @@ -1456,32 +1647,34 @@ }, { "cell_type": "markdown", - "id": "97", + "id": "106", "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": "98", + "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": "99", + "id": "108", "metadata": {}, "source": [ "
\n", @@ -1492,7 +1685,7 @@ }, { "cell_type": "markdown", - "id": "100", + "id": "109", "metadata": {}, "source": [ "Each of the strings output by your solution function below should be something like\n", @@ -1506,48 +1699,52 @@ { "cell_type": "code", "execution_count": null, - "id": "101", + "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": "102", + "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. **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", - "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", - "\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`." + "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": "103", + "id": "112", "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", @@ -1586,48 +1783,62 @@ }, { "cell_type": "markdown", - "id": "104", + "id": "113", "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": "105", + "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": "106", + "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": "107", + "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", @@ -1639,7 +1850,7 @@ { "cell_type": "code", "execution_count": null, - "id": "108", + "id": "118", "metadata": { "tags": [] }, @@ -1647,9 +1858,34 @@ "source": [ "%%ipytest\n", "\n", + "\n", "def solution_n_body(universe_start: str) -> int:\n", - " # Write your solution here\n", - " pass" + " \"\"\"\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): Initial positions in format \"MoonName: x=, y=, z=\"\n", + " for each moon on separate lines\n", + "\n", + " Returns:\n", + " int: Average total energy after 1000 simulation steps\n", + " \"\"\"\n", + "\n", + " return" ] } ], @@ -1669,7 +1905,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "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 3aa0d478..e9847453 100644 --- a/tutorial/quiz/object_oriented_programming_advanced.py +++ b/tutorial/quiz/object_oriented_programming_advanced.py @@ -1,9 +1,141 @@ 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. `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", + 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 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.", + 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 +147,50 @@ def __init__(self, title=""): shuffle=True, ) + q5 = 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, + ) + + 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.", + "`@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]) + + +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 +204,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 +244,4 @@ def __init__(self, title=""): shuffle=True, ) - super().__init__(questions=[q1, q2, q3]) + super().__init__(questions=[q1]) diff --git a/tutorial/tests/test_13_object_oriented_programming_advanced.py b/tutorial/tests/test_13_object_oriented_programming_advanced.py index d5b6795a..6170c51d 100644 --- a/tutorial/tests/test_13_object_oriented_programming_advanced.py +++ b/tutorial/tests/test_13_object_oriented_programming_advanced.py @@ -1,16 +1,21 @@ import pathlib from abc import ABC, abstractmethod -from datetime import datetime 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 # -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): class Mother: def __init__(self, eye_color: str): self.eye_color_mother = eye_color @@ -30,8 +35,42 @@ 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) == 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( @@ -44,17 +83,154 @@ def set_eye_color(self): ], ) def test_child_eye_color(mother_eye_color, father_eye_color, function_to_test): - assert function_to_test( - mother_eye_color, father_eye_color - ) == reference_child_eye_color(mother_eye_color, father_eye_color) + 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: Store Inventory +# Exercise 2: Banking System # -def reference_store_inventory(computers: list[dict]) -> list[str]: +def reference_banking_system(tax_rate: float, interest_rate: float) -> list: + 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 + + def credit(self, amount): + self.balance += amount + + def get_balance(self): + return self.balance + self.balance * self.interest_rate + + return [ + SalaryAccount("SAL-001", tax_rate), + SavingsAccount("SAV-001", interest_rate), + ] + + +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 element in the list should be an instance of 'SalaryAccount'." + ) + assert type(solution_result[1]).__name__ == "SavingsAccount", ( + "The 2nd element in the list 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", + [ + (0.20, 0.05), + (0.18, 0.04), + ], +) +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) + + 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() + + +# +# Exercise 3: Store Inventory +# + + +def reference_store_inventory(pc: dict, laptop: dict) -> list: class Computer: """A class representing a computer sold by the online store""" @@ -62,7 +238,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}." @@ -73,7 +248,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 ( @@ -87,7 +261,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 ( @@ -95,46 +268,98 @@ 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]) # -# Exercise 3: Music Streaming Service +# Exercise 4: Music Streaming Service # -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 @@ -180,39 +405,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() @@ -244,97 +479,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 #