{ "cells": [ { "cell_type": "markdown", "source": [ "# Object-oriented design\n", "\n", "## Try me\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ffraile/computer_science_tutorials/blob/main/source/Introduction/tutorials/Objects.ipynb)[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/ffraile/computer_science_tutorials/main?labpath=source%2FIntroduction%2Ftutorials%2FObjects.ipynb)\n", "\n", "Object-oriented design, or Object oriented programming is a programming paradigm in which software is designed in terms of **real-world objects** and their relationships to one another. Recall that, as mentioned in the introduction, computer programming languages are intermediate or *proxy* languages between natural languages and machine code. With object-oriented programming, we create specific variables types called classes that define the properties and behaviours of concepts or objects in the scope of the problem that we are trying to solve with our program. For instance, think of a program to support the management of a course. Instead of relying exclusively on the built-in types provided by our programming language, imaging that we could have a type to model students, which would allow us to create a student by defining properties like the name and the age, just as we defined an imaginary number by defining the real part and the imaginary part, and then have methods to grade a student, or calculate the average grade of the student in an academic year. This would require the creation of a specific variable type (or class) for students, but in turn, it would allow us to code on a higher level of resemblance with the description of the problem in natural language, thus making our code easier to read and to maintain. After this introduction, let us established first the key concepts and terminology:\n", "\n", "- **Class**: As mentioned, the definition of an object or concept is called class, and is equivalent to the concept of a variable type. By defining custom classes, we will be able to create or **instantiate** variables of that type.\n", "- **Object**: It follows that an object is an individual **instance** or individual entity or a class. For instance, in the example above, you would be an instance of the class student.\n", "- **Attributes**: Attributes model properties of a class of objects. Examples of attributes of the class student are name, surname, email address, etc.\n", "- **Methods**: Methods model actions or behaviors performed over an object, possibly based on their attributes.\n", "\n", "The following image represents the relationship between these concepts.\n", "\n", "![Object Oriented Programming concepts with examples](img/OOP-examples.png)\n", "\n", "## Benefits of Object Oriented Programming\n", "The main benefits of using classes are:\n", "\n", "- **Encapsulation**: The process of combining attributes (data) and methods (functions) into a single object is called encapsulation. The main benefit is that data or methods might not be accessible from the context where the class is instantiated, and we can keep some data or functions private. This gives and additional level of control when accessing the object. We will then refer to:\n", "\n", " - **Private access:** When data or functions can only be accessed within the class, from other class methods\n", " - **Public access:** When data or functions can be accessed from external functions\n", "\n", "- **Abstraction**: We can create classes from other classes. This allows us to organise our code into different levels of abstraction, creating classes for more abstract concepts (for instance Person), and then extending these abstract classes to implement more concrete concepts (for instance Customer, or Employee). This allows us to hide implementation details when using a parent class.\n", "\n", "- **Inheritance**: Another benefit of creating classes from other classes is inheritance. We can implement common functionality in the parent class (Person) and reuse this functionality in the children classes (for instance, a send_email() method to send an email to a person, regardless if it´s a customer or an employee).This allows us to reduce redundant code, by implementing common methods and attributes in parent classes.\n", "\n", "- **Polymorphism**: Another benefit that arises from the fact that we can create classes from other classes is that we can use the same method name to implement different functionalities in each child class. For instance, following the example above, both Customer and Employee classes could have an attribute called ```welcome_message``` with a welcome message text, but, although the concept is the same, the text can be different for customers than for employees. This allows to make our code more consistent and easier to read, by having common names for common concepts that can have different implementations.\n", "\n", "## Definition of classes\n", "Let us now see how to create classes. Basically, what we need to do is to use the keyword ```class``` followed by the name of the class that we want to create, and then, within the class definition, use the ```def``` keyword to add methods to our class. Let us see an example and then explain the syntax." ], "metadata": { "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 5, "source": [ "class MyClass:\n", " def __init__(self, attr1, attr2): #Constructor definition\n", " self.attribute1 = attr1\n", " self.attribute2 = attr2\n", " \n", " def show(self): # method\n", " print(self.attribute1)\n", " print(self.attribute2)\n", "\n", "# Instantiation\n", "my_object = MyClass(\"value1\", \"value2\")\n", "\n", "# Use\n", "my_object.show()" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "value1\n", "value2\n" ] } ], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "The first method in the example is the **constructor**, the function that will be automatically called to create an instance of the class.\n", "The constructor name is always ```__init__```. The double underscore is used to note that this is an internal method, not meant to be called on an instance, and the *init* is clearly a shorthand for initialization, since this is the method that will be used to initialise a new instance of the class.\n", "Also, note that the first argument of any method in the class, including the constructor, is **self**, which represents the **instance** of the class: The variable in the context where the class has been instantiated that implements the class (```my_object``` in the example). In the constructor, we use the variable ```self``` to assign the values provided in the main context to the properties of the class.\n", "\n", "Let´s deepen into these concepts with another example. Let us implement a class for **Polygon** objects. For now, our Polygons have only one property: The number of sides. We will also add a method ```print_number_of_sides``` to show the number of sides of the polygon:" ], "metadata": { "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 6, "source": [ "class Polygon:\n", " def __init__(self, sides):\n", " self.sides = sides #the constructor assigns the value \"sides\" passed in the constructor to the property sides of the instance (self.sides)\n", " \n", " def print_number_of_sides(self): #This is a method of the class\n", " print(self.sides)" ], "outputs": [], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "To create an instance of our the polygon, we use the name of the class (```Polygon```), passing the arguments of the initialisation method. Once we have created the instance and assigned it to a variable, we can access its methods and properties:" ], "metadata": { "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 7, "source": [ "poly = Polygon(7)\n", "# Show the number of sides calling the method print_number_of_sides()\n", "poly.print_number_of_sides()\n", "# Show the number of sides accessing the attribute sides\n", "print(poly.sides)" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "7\n", "7\n" ] } ], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Abstraction\n", "We can create a subclass or a child class that inherits all methods and properties of its parent class, using abstraction. To extend a class, we need to pass the parent class between parenthesis following the class name in the ```class``` creation statement. We can access the parent class instance using the keyword ```super()```. For instance:" ], "metadata": { "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 8, "source": [ "class Square(Polygon): #The parent is defined as a parameter in the class definition.\n", " def __init__(self, color):\n", " super().__init__(4) #First we instantiate the parent class using super()\n", " self.color = color # We add a second property\n", " def change_color(self, new_color): # We define a new method, change color\n", " self.color = new_color\n", " def print_color(self): # We define a new method, print color\n", " print(self.color)" ], "outputs": [], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ " Note that we have created a class Square from the parent class Polygon by passing it between parenthesis (as if it was a parameter) in the class definition. Note also that, as a concept, Polygon is more abstract than a Square, as all squares are polygons. All methods and attributes from the *parent* class will be available and can be extended in the children class to implement more concrete concepts.\n", "\n", "In the constructor, we need to initialise the instance of the parent class through its constructor using ```super().__init__()```. Recall that the constructor of the class Polygon had an attribute which was the number of sides. Since a square has 4 sides, we use 4 as the argument in the init method.\n", "Finally, we extend our class with another attribute (color) and add two methods to change the color and to print the color.\n", "Let us see our brand new class in action:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 9, "source": [ "s = Square(\"blue\")\n", "s.print_number_of_sides()\n", "s.print_color()\n", "s.change_color(\"red\")\n", "s.print_color()" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4\n", "blue\n", "red\n" ] } ], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Note that we do not need to specify the number of sides when we create a square, since the number of sides is defined internally in the square class (hiding this implementation detail from the user). We can still access the methods and attributes of the parent class, for instance the ```print_number_of_sides()``` method." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "markdown", "source": [ "Inheritance\n", "------\n", "Inheritance allows us to reuse the code from parent and share common code. Following the example above, we can create another class called Pentagon:" ], "metadata": { "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 10, "source": [ "class Pentagon(Polygon):\n", " def __init__(self):\n", " super().__init__(5)" ], "outputs": [], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "And still reuse the number sides features implemented in the parent class Polygon." ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 11, "source": [ "pent = Pentagon()\n", "pent.print_number_of_sides()\n", "print(pent.sides)" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "5\n", "5\n" ] } ], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Polymorphism\n", "-------\n", "Objects with a common base can implement the same methods doing different things. For instance, following the example above, we can *draw* different polygons in different ways depending on their shape:" ], "metadata": { "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 12, "source": [ "class Triangle(Polygon):\n", " def __init__(self):\n", " super().__init__(3)\n", " def draw(self):\n", " print(\"This is a triangle:\")\n", " print(\" ^ \")\n", " print(\" / \\\\\")\n", " print(\"/___\\\\\")\n", " \n", "class Rectangle(Polygon):\n", " def __init__(self):\n", " super().__init__(4)\n", " def draw(self):\n", " print(\"This is a rectangle:\")\n", " print(\"+----+\")\n", " print(\"| |\")\n", " print(\"| |\")\n", " print(\"+----+\")\n", "t = Triangle()\n", "r = Rectangle()\n", "t.draw()\n", "r.draw()" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This is a triangle:\n", " ^ \n", " / \\\n", "/___\\\n", "This is a rectangle:\n", "+----+\n", "| |\n", "| |\n", "+----+\n" ] } ], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "Notice that, since the concept of drawing is the same, we use the same method name regardless of the type of polygon we have created. Note also that we do not need to know the concrete type of polygon to draw it, we do not need to know the specific class as long as we know that it is a polygon, so, for instance, we could do something like:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 13, "source": [ "# We can can call draw method without knowing which kind of polygon is\n", "polygons = [t, r]\n", "for p in polygons:\n", " p.draw()" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This is a triangle:\n", " ^ \n", " / \\\n", "/___\\\n", "This is a rectangle:\n", "+----+\n", "| |\n", "| |\n", "+----+\n" ] } ], "metadata": { "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "## Encapsulation\n", "Finally, we left encapsulation for last because it is not implement in-depth in Python. To define a private function or attribute that can only be accessible within the class, you just need to use the double underscore notation, just as we explained for the ```__init__``` method used as constructor. For instance:" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } }, { "cell_type": "code", "execution_count": 16, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "125\n" ] } ], "source": [ "class MyEncoder:\n", " def __init__(self, attr1, attr2):\n", " self.__private_atribute__ = 2 #This is a private attribute can only be accessed internally\n", " self.attr1 = attr1\n", " self.attr2 = attr2\n", "\n", "\n", " def __internal_formula__(self):\n", " return (self.attr1 + self.attr2)**self.__private_atribute__\n", "\n", " def encode_number(self, number):\n", " return number * self.__internal_formula__()\n", "\n", "my_encoder = MyEncoder(2, 3)\n", "encoded_number = my_encoder.encode_number(5)\n", "print(encoded_number)" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%%\n" } } }, { "cell_type": "markdown", "source": [ "In the example, the encoder class uses an internal attribute and an internal formula to encode a number. By using the double underscore notation, these attributes and methods are noted as private, however, they can still be accessed in the main context, because the double underscore is just a convention. Be aware of this when you use external libraries, because this notation is telling you that the developer did not mean those methods and attributes to be used externally and you might get unexpected results!" ], "metadata": { "collapsed": false, "pycharm": { "name": "#%% md\n" } } } ], "metadata": { "kernelspec": { "name": "python3", "display_name": "Python 3.8.1 64-bit" }, "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.8.1" }, "interpreter": { "hash": "2db524e06e9f5f4ffedc911c917cb75e12dbc923643829bf417064a77eb14d37" } }, "nbformat": 4, "nbformat_minor": 4 }