{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 5. Object-Oriented Programming II\n", " \n", "Let's now build upon the Abstraction and Encapsulation mindset from last session with Inheritance and Polymorphism." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Instance Interaction and Object Representation\n", "The class instances we create can interact by taking another instance/object as input to a method call. Let's illustrate this by a `Point` class example where the distance (Euclidean) between point objects can be determined:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import math\n", "class Point:\n", " ''' Defines a 2D 'Point' class that can compute distance between points'''\n", " \n", " def __init__(self, x, y):\n", " self._x = x\n", " self._y = y\n", " \n", " def distance_to(self, other):\n", " '''Return the distance to another point instance.'''\n", " x0, y0 = self._x, self._y\n", " x1, y1 = other._x, other._y\n", " return self.distance(x0, y0, x1, y1)\n", " \n", " @staticmethod\n", " def distance(x0, y0, x1, y1):\n", " return math.sqrt( (x1-x0)**2 + (y1-y0)**2 )\n", " \n", " def __repr__(self):\n", " # unambiguous/official description for debugging/development\n", " return f'{self.__class__.__name__}({self._x}, {self._y})' \n", " \n", " def __str__(self):\n", " # readable/informal description\n", " return f'({self._x}, {self._y})'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Initiation of instances for Origo and two points:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "origo = Point(0, 0)\n", "p1 = Point(1, 1)\n", "p2 = Point(3, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Evaluate distances between the various Point instances:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2.23606797749979" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1.distance_to(p2)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3.0" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p2.distance_to(origo)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Representation\n", "Because of the two dunder methods `__str__` and `__repr__` a much cleaner representation is obtained instead of the default `__main__.Point object at 0x0000022EC29BADC8`:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(3, 0)\n", "(3, 0)\n", "Point(3, 0)\n" ] }, { "data": { "text/plain": [ "Point(3, 0)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print(p2)\n", "print(str(p2)) # str(p2) = p2.__str__\n", "print(repr(p2)) # repr(p2) = p2.__repr__\n", "p2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some other examples of useful string representations:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "| Examples | str | repr |\n", "|:---------------|:--------------------|:------------------------------------------|\n", "| our point | (3, 0) | Point(3, 0) |\n", "| timestamp | 2016-02-22 19:32:04 | datetime.datetime(2016, 2, 22, 19, 32, 4) |\n", "| complex number | 10 + i20 | Rational(10, 20) |\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Magic/Dunder Methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**QUESTION**: \n", "Is the `len()` function actually an intelligent function that automatically detects what data type the input consists of before it decides what to do?" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "9" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# it can find the number of charactors in a string\n", "len('my string')" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# it can find the length of a list\n", "len([x for x in range(5)])" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# it can find the number of items in a dictionary\n", "len({'key1':'value1', 'key2':'value2'})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The length `len()` function call is actually a hidden object method call." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "9" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "'my string'.__len__()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[x for x in range(5)].__len__()" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{'key1':'value1', 'key2':'value2'}.__len__()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is refered to as dunder or magic methods. Note that double underscore is pronaunced \"dunder\".\n", "Other examples are: abs, str, init, sizeof, iter, dir..." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Operator Overloading\n", "Let's make our points more intelligent with build-in support for classical operators '+', '-', '*', '/'.\n", "This can be done by incorporating the corresponding dunder methods 'add', 'sub', 'mul', 'div' into our class:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "class Point:\n", " ''' Defines a 2D 'Point' class that can compute distance between points'''\n", " \n", " def __init__(self, x, y):\n", " self._x = x\n", " self._y = y\n", " \n", " def distance_to(self, other):\n", " '''Return the distance to another point instance.'''\n", " x0, y0 = self._x, self._y\n", " x1, y1 = other._x, other._y\n", " return self.distance(x0, y0, x1, y1)\n", " \n", " @staticmethod\n", " def distance(x0, y0, x1, y1):\n", " return math.sqrt( (x1-x0)**2 + (y1-y0)**2 )\n", " \n", " def __repr__(self):\n", " return f'{self.__class__.__name__}({self._x}, {self._y})' \n", " \n", " def __add__(self, other):\n", " x = self._x + other._x\n", " y = self._y + other._y\n", " return Point(x, y)\n", " \n", " def __sub__(self, other):\n", " x = self._x - other._x\n", " y = self._y - other._y\n", " return Point(x, y)\n", " \n", " def __mul__(self, other):\n", " # Check if the other object is an instance of class `int` or `float`\n", " if isinstance(other, (int, float)):\n", " \n", " # Perform multiplication and return\n", " x = self._x * other\n", " y = self._y * other\n", " return Point(x, y)\n", " else:\n", " return \"I don't know how to do multiply two points\"\n", " \n", " def __div__(self, other):\n", " return \"I don't know how to do divide two points\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First we re-create our point instances from this new class definition:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "origo = Point(0, 0)\n", "p1 = Point(1, 1)\n", "p2 = Point(3, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see what happens when we apply operations:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Point(4, 1)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1 + p2 # equivalent to p1.__add__(p2)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Point(-1, -1)" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "origo - p1 # equivalent to origo.__sub__(p1)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Point(5, 5)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1 * 5 # equivalent to p1.__mul__(5)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\"I don't know how to do multiply two points\"" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1 * p2 # equivalent to p1.__mul__(p2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So even just adding two numbers in Python involves OOP." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Polymorphism by Inheritance\n", "It wasn't that nice that we had to copy all the class definition code above when we wanted to add additional functionally to our class. This can be avoided by using inheritance to polymorph an existing class, so you don't have to start from scratch everytime you define a Class.\n", "\n", "Here we define a new class named NewPoint (could also overwrite Point if we wanted to). Notice how we specify where to inherit from by specifiying a ***parent*** class as the input to the class definition. The new ***child*** class will inherit all methods etc. from it's parent. \n", "\n", "*Note that it's also possible to inherit from multiple parents.*" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "class NewPoint(Point):\n", " '''NewPoint class which inherits from the `Point` class.'''\n", "\n", " def __pow__(self, other):\n", " return \"I don't know how to do point to the power of something\"" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "# Define a point with the new child class\n", "p3 = NewPoint(4, 4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `NewPoint` class has inherited from the `Point` class. Thus, all methods from `Point` are now available in `NewPoint`.\n", "\n", "We can see this e.g. by printing the class instance. Recall that we wrote the `__repr__` method in the `Point` to create a better printed message than `<__main__.{class_name} object at 0x00000284A08C0848>`." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "NewPoint(4, 4)\n" ] } ], "source": [ "print(p3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The new method in `NewPoint` is added \"on top\":" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "\"I don't know how to do point to the power of something\"" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p3**origo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We could also use inheritance for creating a new `Point3D` class. Notice how we use `super()` to refer to the parent's `__init__` method so we don't have to repeat any code. You just redefine any methods that you want to overwrite (known as ***Method Overriding***)." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "class Point3D(NewPoint):\n", "\n", " def __init__(self, x, y, z):\n", " super().__init__(x, y)\n", " self._z = z\n", " \n", " def distance_to(self, other):\n", " x0, y0, z0 = self._x, self._y, self._z\n", " x1, y1, z1 = other._x, other._y, other._z\n", " return self.distance(x0, y0, z0, x1, y1, z1)\n", " \n", " @staticmethod\n", " def distance(x0, y0, z0, x1, y1, z1):\n", " return math.sqrt( (x1-x0)**2 + (y1-y0)**2 + (z1-z0)**2 )\n" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1.7320508075688772" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p1 = Point3D(1, 1, 1)\n", "p2 = Point3D(2, 2, 2)\n", "p1.distance_to(p2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We're not limited to only work on our own classes. Let's e.g. define a new `MyFloat` class where for once negative times negative doesn't yield positive." ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "class MyFloat(float):\n", " \n", " def __mul__(self, other):\n", " if self.real < 0 and other.real < 0:\n", " return -self.real*other.real\n", " else:\n", " return self.real*other.real" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "8.0" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# normal float behaviour\n", "f1 = -2.\n", "f2 = -4.\n", "f1*f2 # equivalent to calling f1.__mul__(f2)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "-8.0" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# new MyFloat behaviour\n", "f1 = MyFloat(-2.)\n", "f2 = MyFloat(-4.)\n", "f1*f2" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.5" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# negative divide with negative to yields positive as we didn't overwrite that method\n", "f1 / f2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Beside this sort of \"flat\" usage of inheritance, there's also a vertical/hierarchical approach.\n", "\n", "If you for example have written two useful classes but suddenly see the need to duplicate some of the code in-between the two, because they both need some of the same functionality, it's properly because they should share a parent. \n", "Whatever they have in common should be defined in the shared parent (never duplicate code!).\n", "\n", "This type of inheritance should generally be limited to a depth of maximum 2-3 levels to avoid a program design that is hard to follow.\n", "\n", "In the following example we have a `Mammal` class (where instances are not allowed) with two children classes (`Person` and `Dog`) and finally with two grandchildren classes (`Student` and `Teacher`)." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "# Abstract Base Classes are special classes that cannot generate instances\n", "from abc import ABC, abstractmethod\n", "class Mammal(ABC):\n", " characteristics = 'nourished with milk from mother as young' # class variable\n", " \n", " @abstractmethod # This method must be overriden before instances are allowed\n", " def says(self):\n", " pass" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Can't instantiate abstract class Mammal with abstract methods says", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mmammal\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mMammal\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[1;31mTypeError\u001b[0m: Can't instantiate abstract class Mammal with abstract methods says" ] } ], "source": [ "mammal = Mammal()" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "class Person(Mammal):\n", " \n", " def __init__(self, fname, lname):\n", " self.fname = fname\n", " self.lname = lname\n", " \n", " @property\n", " def full_name(self):\n", " return f'{self.fname} {self.lname}'\n", " \n", " def says(self):\n", " return f'{self.full_name} says hello!'" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Kenneth Kleissl says hello!\n", "nourished with milk from mother as young\n" ] } ], "source": [ "me = Person('Kenneth', 'Kleissl')\n", "print(me.says())\n", "print(me.characteristics)" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "class Dog(Mammal):\n", " \n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def says(self):\n", " return f'{self.name} says Woof!'" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Max says Woof!\n", "nourished with milk from mother as young\n" ] } ], "source": [ "dog = Dog('Max')\n", "print(dog.says())\n", "print(dog.characteristics)" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "class Student(Person):\n", " \n", " def __init__(self, fname, lname, student_no):\n", " super().__init__(fname, lname)\n", " self.student_no = student_no\n", " \n", " def get_student_no(self):\n", " return self.student_no" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Kim Jensen says hello!\n", "student number is: s040203\n", "nourished with milk from mother as young\n" ] } ], "source": [ "student = Student('Kim', 'Jensen', 's040203')\n", "print(student.says())\n", "print('student number is:', student.get_student_no())\n", "print(student.characteristics)" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "class Teacher(Person):\n", " \n", " def __init__(self, fname, lname, department):\n", " super().__init__(fname, lname)\n", " self.department = department\n", " \n", " def get_department(self):\n", " return self.department" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Lars Hansen says hello!\n", "department: Civil Engineering\n", "nourished with milk from mother as young\n" ] } ], "source": [ "staff = Teacher('Lars', 'Hansen', 'Civil Engineering')\n", "print(staff.says())\n", "print('department:', staff.get_department())\n", "print(staff.characteristics)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 1\n", "Create a new enhanced Triangle class based on the one you did for the exercises in the previous session. Use inheritance to avoid retyping the original class definition.\n", "\n", "- Extend the class with a dunder method such that it may be scaled up by a scalar via multiplication\n", "- Extend the class with the `__lt__` (less than '<') dunder method so the two triangle areas are compared just by asking triangle1 < triangle2\n", "- Also add a `__repr__` dunder method with a reasonable descriptive output" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 2\n", "Create a Polyline class that is initialised from a list of Point instances with abitrary length.\n", "\n", "1. Define a Point class with (x, y)-coordinates attributes and an inter-point distance method (like the one presented in todays material)\n", "2. Provide the Polyline class with the `__len__` dunder method that returns the integer number of line segments.\n", "3. Provide the Polyline class with a `get_total_length` method that returns the total length of the polyline by using the distance method available in the point objects.\n", "\n", "#### Hint: try to avoid looping over indices for the last task. The more Pythonic approach is e.g. to loop over an iterator like `zip(points, points[1:])`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# End of exercises\n", "*The cell below is for setting the style of this document. It's not part of the exercises.*" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Apply css theme to notebook\n", "from IPython.display import HTML\n", "HTML(''.format(open('../css/cowi.css').read()))" ] } ], "metadata": { "hide_input": false, "kernelspec": { "display_name": "Python 3", "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.7.4" }, "latex_envs": { "LaTeX_envs_menu_present": true, "autoclose": false, "autocomplete": true, "bibliofile": "biblio.bib", "cite_by": "apalike", "current_citInitial": 1, "eqLabelWithNumbers": true, "eqNumInitial": 1, "hotkeys": { "equation": "Ctrl-E", "itemize": "Ctrl-I" }, "labels_anchors": false, "latex_user_defs": false, "report_style_numbering": false, "user_envs_cfg": false }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": false, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Table of Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 2 }