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
#