See More

{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Object-oriented Programming" ] }, { "cell_type": "markdown", "id": "1", "metadata": {}, "source": [ "# Table of Contents\n", " - [References](#References)\n", " - [A `class` as a blueprint of objects](#A-class-as-a-blueprint-of-objects)\n", " - [Example 1](#Example-1)\n", " - [Properties and methods](#Properties-and-methods)\n", " - [Example 2](#Example-2)\n", " - [Python's special methods](#Python's-special-methods)\n", " - [`__str__` and `__repr__`](#__str__-and-__repr__)\n", " - [Example 3](#Example-3)\n", " - [Comparison methods](#Comparison-methods)\n", " - [Example 4](#Example-4)\n", " - [More comparison methods](#More-comparison-methods)\n", " - [The `@property` keyword](#The-@property-keyword)\n", " - [Quick glossary](#Quick-glossary)\n", " - [Quiz](#Quiz)\n", " - [Exercises](#Exercises)\n", " - [Exercise 1: Ice cream scoop](#Exercise-1:-Ice-cream-scoop)\n", " - [Exercise 2: Ice cream bowl](#Exercise-2:-Ice-cream-bowl)\n", " - [Exercise 3: Ice cream shop](#Exercise-3:-Ice-cream-shop)\n", " - [Exercise 4: Intcode computer 🌶️](#Exercise-4:-Intcode-computer-🌶️)" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "# References" ] }, { "cell_type": "markdown", "id": "3", "metadata": {}, "source": [ "- [Python classes (read)](https://www.py4e.com/html3/14-objects)\n", "- [Python classes (video lecture)](https://www.py4e.com/lessons/Objects#)\n", "- [Python Data Model](https://docs.python.org/3/reference/datamodel.html)" ] }, { "cell_type": "markdown", "id": "4", "metadata": { "tags": [] }, "source": [ "# A `class` as a blueprint of objects" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "Object-oriented programming (OOP) is probably the most well-known approach to programming. Almost every programming language in some way or another supports this paradigm.\n", "\n", "The idea behind OOP is simple: instead of defining our functions in one part of the code, and the data on which those functions operate in a separate part of the code, we define them together.\n", "\n", "The concept of **object** is the heart of OOP, and the fundamental building block is the **class**. We have already seen many types of objects, each belonging to different classes:" ] }, { "cell_type": "code", "execution_count": null, "id": "6", "metadata": { "tags": [] }, "outputs": [], "source": [ "data = \"Python\", 2023, True, 3.14159, (1, 2.0, \"3.0\"), {\"a\": 10, \"b\": 11}\n", "\n", "for d in data:\n", " print(type(d))" ] }, { "cell_type": "markdown", "id": "7", "metadata": {}, "source": [ "In Python, to create a custom class we use the `class` keyword, and we can initialize class attributes in the special method `__init__`.\n", "\n", "For example, let's create a class that represents a rectangle as a geometrical entity:" ] }, { "cell_type": "code", "execution_count": null, "id": "8", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height" ] }, { "cell_type": "markdown", "id": "9", "metadata": {}, "source": [ "The first argument (`self`) is automatically filled in by Python and contains **the object being created**. It's the way a class can modify itself, in some sense.\n", "\n", "

\n", "

Note

\n", " Using the name self is just a convention (although a good one, and you should use it to make your code more understandable by others), you could really call it whatever (valid) name you want\n", "
\n", "\n", "We create **instances** of the `Rectangle` class by calling it with arguments that are passed to the `__init__` method as the second and third arguments.\n", "\n", "You **don't** have to pass `self` when creating a new instance of a class." ] }, { "cell_type": "code", "execution_count": null, "id": "10", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(10, 20)\n", "r2 = Rectangle(3, 5)" ] }, { "cell_type": "code", "execution_count": null, "id": "11", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1.width # this will return the `self.width` value" ] }, { "cell_type": "code", "execution_count": null, "id": "12", "metadata": { "tags": [] }, "outputs": [], "source": [ "r2.height # this will return the `self.height` value" ] }, { "cell_type": "markdown", "id": "13", "metadata": {}, "source": [ "## Example 1" ] }, { "cell_type": "code", "execution_count": null, "id": "14", "metadata": {}, "outputs": [], "source": [ "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "markdown", "id": "15", "metadata": {}, "source": [ "
\n", "

Question

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

Question

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

Hint

\n", " Make sure to use the self. notation when you wish to access the class attributes.\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "26", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", "def solution_oop_fullname(first_name: str, last_name: str):\n", " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", "\n", " Person is a class with two attributes called 'first_name' and 'last_name',\n", " and a method called 'full_name' which should return the string 'My name is {first_name} {last_name}'.\n", "\n", " Args:\n", " first_name: a string used to initialize the Person instance\n", " last_name: a string used to initialize the Person instance\n", " Returns:\n", " - an instance of Person\n", " \"\"\"\n", "\n", " # Write your solution here\n", " return" ] }, { "cell_type": "markdown", "id": "27", "metadata": { "tags": [] }, "source": [ "# Python's special methods" ] }, { "cell_type": "markdown", "id": "28", "metadata": {}, "source": [ "Special methods are methods that Python defines automatically. If you define a custom class, then you are responsible of defining the **expected behavior** of these methods. Otherwise, Python will fallback to the default, built-in definition, or it will raise an error if it doesn't know what to do.\n", "\n", "These are also called **dunder methods**, that is, \"double-underscore methods\", because their names look like `__method__`. There are special **attributes** as well." ] }, { "cell_type": "markdown", "id": "29", "metadata": { "tags": [] }, "source": [ "## `__str__` and `__repr__`\n", "\n", "For example, we can obtain a string representation of an integer using the built-in `str` function:" ] }, { "cell_type": "code", "execution_count": null, "id": "30", "metadata": {}, "outputs": [], "source": [ "str(10)" ] }, { "cell_type": "markdown", "id": "31", "metadata": {}, "source": [ "What happens if we try this with our `Rectangle` object?" ] }, { "cell_type": "code", "execution_count": null, "id": "32", "metadata": { "tags": [] }, "outputs": [], "source": [ "str(r1)" ] }, { "cell_type": "markdown", "id": "33", "metadata": {}, "source": [ "Not exactly what we might have expected. On the other hand, how is Python supposed to know how to display our rectangle as a string?\n", "\n", "We could write a method in the class such as:" ] }, { "cell_type": "code", "execution_count": null, "id": "34", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " def area(self):\n", " return self.width * self.height\n", " \n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def to_str(self):\n", " return 'Rectangle (width={0}, height={1})'.format(self.width, self.height)" ] }, { "cell_type": "markdown", "id": "35", "metadata": {}, "source": [ "So now we could get a string from our object as follows:" ] }, { "cell_type": "code", "execution_count": null, "id": "36", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(10, 20)\n", "r1.to_str()" ] }, { "cell_type": "markdown", "id": "37", "metadata": {}, "source": [ "However, using the built-in `str` function still does not work 🤔" ] }, { "cell_type": "code", "execution_count": null, "id": "38", "metadata": { "tags": [] }, "outputs": [], "source": [ "str(r1)" ] }, { "cell_type": "markdown", "id": "39", "metadata": {}, "source": [ "This is where these special methods come in. When we call `str(r1)`, Python will first look to see if our class (`Rectangle`) has a special method called `__str__`.\n", "\n", "If the `__str__` method is present, then Python will call it and return that value.\n", "\n", "There's actually another one called `__repr__` which is related, but we'll just focus on `__str__` for now." ] }, { "cell_type": "code", "execution_count": null, "id": "40", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " def area(self):\n", " return self.width * self.height\n", " \n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def __str__(self):\n", " \"\"\"A string representation of a Rectangle\"\"\"\n", " return f'Rectangle (width={self.width}, height={self.height})'" ] }, { "cell_type": "code", "execution_count": null, "id": "41", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(10, 20)" ] }, { "cell_type": "code", "execution_count": null, "id": "42", "metadata": { "tags": [] }, "outputs": [], "source": [ "str(r1)" ] }, { "cell_type": "markdown", "id": "43", "metadata": {}, "source": [ "However, in Jupyter, look what happens here:" ] }, { "cell_type": "code", "execution_count": null, "id": "44", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1" ] }, { "cell_type": "markdown", "id": "45", "metadata": {}, "source": [ "As you can see we still get the default. That's because here Python is **not** converting `r1` to a string, but instead looking for a string *representation* of the object. It is looking for the [`__repr__` method](https://docs.python.org/3/reference/datamodel.html#object.__repr__), which is defined as\n", "\n", "> the “official” string representation of an object\n", "\n", "Ideally, the `__repr__` method should return a **valid Python expression** that can be used to recreate an instance of the object. If it's not possible, it should return a simple string. For this tutorial, we can define a `__repr__` method that behaves **identically** to the `__str__` method." ] }, { "cell_type": "code", "execution_count": null, "id": "46", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " def area(self):\n", " return self.width * self.height\n", " \n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def __str__(self):\n", " return f'Rectangle (width={self.width}, height={self.height})'\n", " \n", " def __repr__(self):\n", " return self.__str__()" ] }, { "cell_type": "markdown", "id": "47", "metadata": {}, "source": [ "So, let's try to define and print `r1` again, to see what happens." ] }, { "cell_type": "code", "execution_count": null, "id": "48", "metadata": {}, "outputs": [], "source": [ "r1 = Rectangle(10, 20)\n", "r1" ] }, { "cell_type": "markdown", "id": "49", "metadata": {}, "source": [ "### Example 3\n", "\n", "
\n", "

Question

\n", " Modify your code from Example 2 so that class Person will now have a __str__() method instead of full_name(), with the same functionality. Then, add a __repr__() method that returns the string: Person({first_name}, {last_name}). Create an instance of this class, representing a person, which is being initialized by using the arguments passed in the solution function. Lastly, return the instance.\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "50", "metadata": {}, "outputs": [], "source": [ "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "code", "execution_count": null, "id": "51", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", "def solution_oop_str_and_repr(first_name: str, last_name: str):\n", " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", "\n", " Person is a class with two attributes called 'first_name' and 'last_name'.\n", " Person also implements:\n", " - The __str__() method which should return the string 'My name is {first_name} {last_name}',\n", " - The __repr__() method which should return the string 'Person({first_name}, {last_name})'.\n", "\n", " Args:\n", " first_name: a string used to initialize the Person instance\n", " last_name: a string used to initialize the Person instance\n", " Returns:\n", " - an instance of Person\n", " \"\"\"\n", "\n", " # Write your solution here\n", " return" ] }, { "cell_type": "markdown", "id": "52", "metadata": { "tags": [] }, "source": [ "## Comparison methods" ] }, { "cell_type": "markdown", "id": "53", "metadata": {}, "source": [ "How about the comparison operator, such as `==`? How can we tell Python how it should compare two different rectangles?" ] }, { "cell_type": "code", "execution_count": null, "id": "54", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(10, 20)\n", "r2 = Rectangle(10, 20)" ] }, { "cell_type": "code", "execution_count": null, "id": "55", "metadata": {}, "outputs": [], "source": [ "r1 == r2" ] }, { "cell_type": "markdown", "id": "56", "metadata": {}, "source": [ "As you can see, Python does not consider `r1` and `r2` as equal (using the `==` operator). Again, how is Python supposed to know that two rectangle objects with the same height and width should be considered equal?" ] }, { "cell_type": "markdown", "id": "57", "metadata": {}, "source": [ "We just need to tell Python how to do it, using the special method `__eq__`. Let's see how:" ] }, { "cell_type": "code", "execution_count": null, "id": "58", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " def area(self):\n", " return self.width * self.height\n", " \n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def __str__(self):\n", " return f'Rectangle (width={self.width}, height={self.height})'\n", " \n", " def __repr__(self):\n", " return self.__str__()\n", " \n", " def __eq__(self, other):\n", " \n", " # This statement is for debugging purposes. We will remove it later\n", " print(f'self={self}, other={other}')\n", " \n", " # Make sure we are comparing a Rectangle with another Rectangle\n", " if isinstance(other, Rectangle):\n", " return (self.width, self.height) == (other.width, other.height)\n", " else:\n", " return False" ] }, { "cell_type": "code", "execution_count": null, "id": "59", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(10, 20)\n", "r2 = Rectangle(10, 20)" ] }, { "cell_type": "markdown", "id": "60", "metadata": {}, "source": [ "We now have two **different** objects, two instances of our `Rectangle` class. In fact, we can check that the two objects are different with the `is` operator" ] }, { "cell_type": "code", "execution_count": null, "id": "61", "metadata": { "tags": [] }, "outputs": [], "source": [ "print(f\"Is r1 the same as r2? {r1 is r2}. No! Because their IDs are {id(r1)} (r1) and {id(r2)} (r2)\")" ] }, { "cell_type": "markdown", "id": "62", "metadata": {}, "source": [ "However, they are **equal** according to our `__eq__` function: rectangles are considered equal if their widths and heights are equal" ] }, { "cell_type": "code", "execution_count": null, "id": "63", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 == r2" ] }, { "cell_type": "code", "execution_count": null, "id": "64", "metadata": { "tags": [] }, "outputs": [], "source": [ "r3 = Rectangle(2, 3)" ] }, { "cell_type": "code", "execution_count": null, "id": "65", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 == r3" ] }, { "cell_type": "markdown", "id": "66", "metadata": {}, "source": [ "And if we try to compare our Rectangle to a different type:" ] }, { "cell_type": "code", "execution_count": null, "id": "67", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 == 100, type(r1), type(100)" ] }, { "cell_type": "markdown", "id": "68", "metadata": {}, "source": [ "That's because our `Rectangle` class automatically returns `False` if we try to compare for equality an instance of `Rectangle` and any other Python object." ] }, { "cell_type": "markdown", "id": "69", "metadata": {}, "source": [ "Here's our final class, without any `print` statement – remember, we should try to avoid side-effects when they are **not** necessary:" ] }, { "cell_type": "code", "execution_count": null, "id": "70", "metadata": {}, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " def area(self):\n", " return self.width * self.height\n", " \n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def __str__(self):\n", " return f'Rectangle (width={self.width}, height={self.height})'\n", " \n", " def __repr__(self):\n", " return self.__str__()\n", " \n", " def __eq__(self, other):\n", " if isinstance(other, Rectangle):\n", " return (self.width, self.height) == (other.width, other.height)\n", " else:\n", " return False" ] }, { "cell_type": "markdown", "id": "71", "metadata": {}, "source": [ "### Example 4\n", "\n", "
\n", "

Question

\n", " Modify your code from Example 1 so that class Person will now have an additional attribute called age. Then, add a __eq__() comparison method that makes sure that two persons are the same when they have the same first name, last name and age. Create an instance of this class, representing a person, which is being initialized by using the arguments passed in the solution function. Lastly, return the instance.\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "72", "metadata": {}, "outputs": [], "source": [ "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "code", "execution_count": null, "id": "73", "metadata": {}, "outputs": [], "source": [ "%%ipytest\n", "\n", "def solution_oop_compare_persons(first_name: str, last_name: str, age: int):\n", " \"\"\"A function that contains the definition of a class Person, and returns an instance of it.\n", "\n", " Person is a class with three attributes called 'first_name', 'last_name' and 'age'.\n", " Person implements the __eq__() comparison method,\n", " which should return True when two persons have the same first name, last name and age.\n", "\n", " Args:\n", " first_name: a string used to initialize the Person instance\n", " last_name: a string used to initialize the Person instance\n", " age: an integer used to initialize the Person instance\n", " Returns:\n", " - an instance of Person\n", " \"\"\"\n", "\n", " # Write your solution here\n", " return" ] }, { "cell_type": "markdown", "id": "74", "metadata": {}, "source": [ "## More comparison methods" ] }, { "cell_type": "markdown", "id": "75", "metadata": {}, "source": [ "What about `<`, `>`, `<=`, etc.?\n", "\n", "Again, Python has special methods we can use to provide that functionality.\n", "\n", "These are `__lt__`, `__gt__`, `__le__` methods. There are [many more](https://docs.python.org/3/reference/datamodel.html)!" ] }, { "cell_type": "markdown", "id": "76", "metadata": {}, "source": [ "
\n", "

Note

\n", " While we can define custom methods for the comparison operators, it doesn't mean it makes sense. In this example with the Rectangle class, the meaning of the greater/less than operations is not formally defined. We chose to compare the rectangles areas but that's completely arbitrary.\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "77", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " def area(self):\n", " return self.width * self.height\n", " \n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def __str__(self):\n", " return f'Rectangle (width={self.width}, height={self.height})'\n", " \n", " def __repr__(self):\n", " return self.__str__()\n", " \n", " def __eq__(self, other):\n", " if isinstance(other, Rectangle):\n", " return (self.width, self.height) == (other.width, other.height)\n", " else:\n", " return False\n", " \n", " def __lt__(self, other):\n", " if isinstance(other, Rectangle):\n", " return self.area() < other.area()\n", " else:\n", " return NotImplemented" ] }, { "cell_type": "code", "execution_count": null, "id": "78", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(100, 200)\n", "r2 = Rectangle(10, 20)" ] }, { "cell_type": "code", "execution_count": null, "id": "79", "metadata": {}, "outputs": [], "source": [ "r1 < r2" ] }, { "cell_type": "code", "execution_count": null, "id": "80", "metadata": {}, "outputs": [], "source": [ "r2 < r1" ] }, { "cell_type": "markdown", "id": "81", "metadata": {}, "source": [ "What about `>`?" ] }, { "cell_type": "code", "execution_count": null, "id": "82", "metadata": {}, "outputs": [], "source": [ "r1 > r2" ] }, { "cell_type": "markdown", "id": "83", "metadata": {}, "source": [ "How did that work? We did not define a `__gt__` method.\n", "\n", "Well, Python decided that, since `r1 > r2` was not implemented, it would give `r2 < r1` a try. And since, `__lt__` **is** defined, it worked! It just a matter of swapping the terms in the comparison. Clever, eh? 😎" ] }, { "cell_type": "markdown", "id": "84", "metadata": {}, "source": [ "Of course, `<=` is not going to magically work!" ] }, { "cell_type": "code", "execution_count": null, "id": "85", "metadata": {}, "outputs": [], "source": [ "r1 <= r2" ] }, { "cell_type": "markdown", "id": "86", "metadata": { "tags": [] }, "source": [ "# The `@property` keyword" ] }, { "cell_type": "markdown", "id": "87", "metadata": {}, "source": [ "A question you might have about our `Rectangle` class is the following: why should we **call** a function to return its area? Isn't area a **property** of a rectangle?\n", "\n", "Unfortunately, Python doesn't think the same way. If we try to do" ] }, { "cell_type": "code", "execution_count": null, "id": "88", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1.area" ] }, { "cell_type": "markdown", "id": "89", "metadata": {}, "source": [ "It would tell us that `r1.area` is in fact an object. It's actually a **function**" ] }, { "cell_type": "code", "execution_count": null, "id": "90", "metadata": { "tags": [] }, "outputs": [], "source": [ "print(f\"r1.area is a {type(r1.area)}. Is is a function? {callable(r1.area)}\")" ] }, { "cell_type": "markdown", "id": "91", "metadata": {}, "source": [ "Which means that if you want to return the Rectangle's area, you should do the following: " ] }, { "cell_type": "code", "execution_count": null, "id": "92", "metadata": {}, "outputs": [], "source": [ "r1.area()" ] }, { "cell_type": "markdown", "id": "93", "metadata": {}, "source": [ "But what if you want to define area as one of Rectangle's properties? The only difference with the other ones, it that this property requires some additional logic in the background, in order for its value to be calculated." ] }, { "cell_type": "markdown", "id": "94", "metadata": {}, "source": [ "Python provides you the special keyword `@property`. We are not going into the details of what this keyword does, but here's how you can use it in your classes to make them more \"user-friendly\":" ] }, { "cell_type": "code", "execution_count": null, "id": "95", "metadata": { "tags": [] }, "outputs": [], "source": [ "class Rectangle:\n", " def __init__(self, width, height):\n", " self.width = width\n", " self.height = height\n", " \n", " @property\n", " def area(self):\n", " return self.width * self.height\n", " \n", " @property\n", " def perimeter(self):\n", " return 2 * (self.width + self.height)\n", " \n", " def __str__(self):\n", " return f'Rectangle (width={self.width}, height={self.height})'\n", " \n", " def __repr__(self):\n", " return self.__str__()" ] }, { "cell_type": "markdown", "id": "96", "metadata": {}, "source": [ "You can simply add `@property` **just above** the line that defines your property. For example, the `def area()` or `def perimeter()` functions above." ] }, { "cell_type": "markdown", "id": "97", "metadata": {}, "source": [ "By doing so, you can now access the values of area and perimeter in the same way that you would for any other property of the class:" ] }, { "cell_type": "code", "execution_count": null, "id": "98", "metadata": { "tags": [] }, "outputs": [], "source": [ "r1 = Rectangle(1, 10)\n", "r2 = Rectangle(10, 20)\n", "\n", "print(f\"Perimeters: r1={r1.perimeter}, r2={r2.perimeter}\")\n", "print(f\"Areas: r1={r1.area}, r2={r2.area}\")" ] }, { "cell_type": "markdown", "id": "99", "metadata": {}, "source": [ "The only difference is that in this way we are unable to directly set the value of these properties. Their values are always internally calculated based on their defined logic, and we are only able to access them." ] }, { "cell_type": "markdown", "id": "100", "metadata": {}, "source": [ "See what happens when you try to execute the following line:" ] }, { "cell_type": "code", "execution_count": null, "id": "101", "metadata": {}, "outputs": [], "source": [ "r1.area = 5" ] }, { "cell_type": "markdown", "id": "102", "metadata": {}, "source": [ "If you want to learn more about the `@property` keyword, you can check out [this link](https://docs.python.org/3/library/functions.html#property). **Beware**, it's rather advanced stuff! If it's your first time with Python, just skip it for the time being." ] }, { "cell_type": "markdown", "id": "103", "metadata": { "tags": [] }, "source": [ "# Quick glossary\n" ] }, { "cell_type": "markdown", "id": "104", "metadata": {}, "source": [ "\n", "| Term | Definition |\n", "| --- | --- |\n", "| Class | A blueprint for creating objects that define their attributes and methods. |\n", "| Instance | An individual occurrence of a class, created from the blueprint of the class. |\n", "| Method | A function defined within a class that performs some action on an instance of that class or the class itself. |\n", "| Attribute | A variable that is bound to a class or instance and holds some value or reference. |\n", "| `__init__` | A special method that is automatically called when an instance of a class is created, used for initializing the instance's attributes. |\n", "| `__str__/__repr__` | Special methods used for defining a string representation of a class or instance, used for debugging or displaying information about the object. `__str__` is used for human-readable output, while `__repr__` is used for machine-readable output. |\n", "| `@property` | A special keyword used to define a method that can be accessed like an attribute, when additional logic or validation is required. |" ] }, { "cell_type": "markdown", "id": "105", "metadata": { "tags": [] }, "source": [ "---" ] }, { "cell_type": "markdown", "id": "106", "metadata": {}, "source": [ "# Quiz\n", "\n", "Run the following cell to test your knowledge with a small quiz." ] }, { "cell_type": "code", "execution_count": null, "id": "107", "metadata": {}, "outputs": [], "source": [ "from tutorial.quiz import object_oriented_programming as oop\n", "\n", "oop.OopQuiz()" ] }, { "cell_type": "markdown", "id": "108", "metadata": {}, "source": [ "# Exercises" ] }, { "cell_type": "code", "execution_count": null, "id": "109", "metadata": {}, "outputs": [], "source": [ "%reload_ext tutorial.tests.testsuite" ] }, { "cell_type": "markdown", "id": "110", "metadata": {}, "source": [ "## Exercise 1: Ice cream scoop" ] }, { "cell_type": "markdown", "id": "111", "metadata": {}, "source": [ "Define a class `Scoop` that represents a single scoop of ice cream. Each scoop should have a **single** attribute, `flavor`, a string that you can initialize when you create the instance of `Scoop`.\n", "\n", "Define also a `__str__` method to return a string reprensentation of a scoop. The output should be `Ice cream scoop with flavor ''`, where `\n", "

Question

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

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

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

Hint

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

Question

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

Hints

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

Question

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

Note

\n", " This is a recap exercise of intermediate difficulty.\n", " If you never used classes or objects before, please try to solve and understand the previous exercises first.\n", "
" ] }, { "cell_type": "markdown", "id": "119", "metadata": {}, "source": [ "An **Intcode program** is a list of integers separated by commas (e.g. `1,0,0,3,99`). The first number is called \"position `0`\". Each number represents either an **opcode** or a **position**.\n", "\n", "An opcode indicates what to do and it's either `1`, `2`, or `99`. The meaning of each opcode is the following:\n", "\n", "1. `99` means that the program should immediately terminate. No instruction will be executed after encountering this opcode.\n", "\n", "2. `1` **adds** together numbers read from **two positions** and stores the result in a **third position**. The three integers **immediately after** the opcode indicates these three positions. For example, the Intcode program `1,10,20,30` should be executed as: read the values at positions `10` and `20`, add those values, and then overwrite the value at position `30`.\n", "\n", "3. `2` works exactly like opcode `1`, but it **multiplies** the two inputs instead of adding them. Again, the three integers following the opcode indicates **where** the inputs and outputs are, not their **values**.\n", "\n", "Finally, when the computer is done with an opcode, it moves to the next one by stepping **forward 4 positions**.\n", "\n", "For example, consider the following program\n", "\n", "```\n", "1,9,10,3,2,3,11,0,99,30,40,50\n", "```\n", "\n", "which can be splitted into multiple lines to indicate the 4 instructions (opcodes):\n", "\n", "```\n", "1,9,10,3,\n", "2,3,11,0,\n", "99,\n", "30,40,50\n", "```\n", "\n", "The first line represents the **sum** (opcode `1`) of the values stored at positions `9` (that is `30`) and `10` (that is `40`). The result (`70`) is then stored at position `3`. Afterward, the program becomes:\n", "\n", "```\n", "1,9,10,70,\n", "2,3,11,0,\n", "99,\n", "30,40,50\n", "```\n", "\n", "Stepping forward by 4 positions, we end up on the second line, which represents a **multiplication** operation (opcode `2`). Take the values at positions `3` and `11`, multiply them, and save the result at position `0`. You obtain:\n", "\n", "```\n", "3500,9,10,70,\n", "2,3,11,0,\n", "99,\n", "30,40,50\n", "```" ] }, { "cell_type": "markdown", "id": "120", "metadata": {}, "source": [ "
\n", "

Question

\n", " What value is left at position 0 after the program stops?\n", "
" ] }, { "cell_type": "markdown", "id": "121", "metadata": {}, "source": [ "Here are the initial and final states of a few small programs:\n", "\n", "- `1,0,0,0,99` becomes `2,0,0,0,99`, so the value at `0` is `2`\n", "- `2,3,0,3,99` becomes `2,3,0,6,99` (value at `0` is `2`)\n", "- `1,1,1,4,99,5,6,0,99` becomes `30,1,1,4,2,5,6,0,99` (value at `0` is `30`)" ] }, { "cell_type": "markdown", "id": "122", "metadata": {}, "source": [ "
\n", "

Hint

\n", "
    \n", "
  • Write a Python class called Computer with a method called run()
  • \n", "
  • The class __init__() method should process your input string and assign the attribute program, which should be a list of integers
  • \n", "
  • The solution function takes a single parameter: the string representing your intcode program. It should return a single value, an integer number
  • \n", "
\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "123", "metadata": { "tags": [] }, "outputs": [], "source": [ "%%ipytest \n", "def solution_intcode_computer(intcode: str) -> int:\n", " \"\"\"A function that contains the definition of a class Computer, and returns a single integer value.\n", "\n", " Computer is a class with an attribute called 'program', which is where you store the processed input as a list of integers.\n", " Computer also has a method called 'run' which executes an intcode program.\n", "\n", " Intcode programs values are integers that can be a 'position' or an 'opcode' (operation code). Operation codes can be:\n", " - 99: Immediately terminates the program\n", " - 1: Adds values from two positions and stores result in third position\n", " - 2: Multiplies values from two positions and stores result in third position\n", "\n", " For opcodes 1 and 2:\n", " - The three integers after the opcode specify positions (not values)\n", " - First two positions are for input values\n", " - Third position is where the result is stored\n", " - After processing, move forward 4 positions to next opcode\n", "\n", " Args:\n", " intcode: a string of integers separated by commas\n", " Returns:\n", " - the value left at position 0 after executing the intcode program\n", " \"\"\"\n", "\n", " # Write your solution here\n", " return" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.13" } }, "nbformat": 4, "nbformat_minor": 5 }