{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Object Oriented Programming in Python" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import addutils.toc ; addutils.toc.js(ipy_notebook=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "__Object Oriented (OOP)__ is a programming paradigm that allows abstraction through the concept of interacting entities.\n", " This is usally opposed to a more conventional model, called procedural, in which programs are organized as a sequence of commands (statements) to perform.\n", " We can think an object as an entity that resides in memory, has a states and it's able to perform some actions. \n", " \n", " More formally objects are entities that represent **instances** of a general abstract concept called **class**. In `Python` the variables defining an object state are called `attributes` and the possible actions are called `methods`.\n", "\n", "In Python everything is an object also classes and functions." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from addutils import css_notebook\n", "css_notebook()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1 How to define classes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.1 Creating a class" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Suppose we want to create a class, named Person, as a prototype, a sort of template for any number of 'Person' objects (instances)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The python syntax to define a class is the following:\n", "\n", " class ClassName(base_classes):\n", " statements\n", "\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Class names should always be uppercase (it's a naming convention)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Say we need to model a Person as:\n", "\n", "* Name\n", "* Surname \n", "* Age " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<__main__.Person object at 0x7fb64bb540b8>\n", "Alec Baldwin was born in 1958.\n" ] } ], "source": [ "class Person:\n", " pass\n", "\n", "john_doe = Person()\n", "john_doe.name = \"Alec\"\n", "john_doe.surname = \"Baldwin\"\n", "john_doe.year_of_birth = 1958\n", "\n", "\n", "print(john_doe)\n", "print(\"%s %s was born in %d.\" %\n", " (john_doe.name, john_doe.surname, john_doe.year_of_birth))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The preceding example defines an empty class (i.e. the class doesn't have a state) called _Person_ then creates a _Person_ instance called _john_doe_ and adds three attributes to _john_doe_. We see that we can access objects attributes using the __. dot __ operator.\n", "\n", "This isn't a raccomended style because classes should describe omogeneous entities. A way to do so is the following:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class Person:\n", " def __init__(self, name, surname, year_of_birth):\n", " self.name = name\n", " self.surname = surname\n", " self.year_of_birth = year_of_birth" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " __init__(self, ...)\n", "Is a special _Python_ method that is automatically called after an object construction, its purpose is to initialize every object state. The first argument (by convention) __self__ is automatically passed either and refers to the object itself.\n", "\n", "In the preceding example `__init__` adds three attributes to every object that is instantiated. So the class is actually describing each object's state.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A class isn't something which we can directly manipulate, we need to create an instance of the class:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<__main__.Person object at 0x7fb64bb54ef0>\n", "Alec Baldwin was born in 1958.\n" ] } ], "source": [ "alec = Person(\"Alec\", \"Baldwin\", 1958)\n", "print(alec)\n", "print(\"%s %s was born in %d.\" % \n", " (alec.name, alec.surname, alec.year_of_birth))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "We have just created an instance of the Person class, bound to the variable `alec`. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.2 Methods" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Alec Baldwin was born in 1958 .\n", "56\n" ] } ], "source": [ "class Person:\n", " def __init__(self, name, surname, year_of_birth):\n", " self.name = name\n", " self.surname = surname\n", " self.year_of_birth = year_of_birth\n", " \n", " def age(self, current_year):\n", " return current_year - self.year_of_birth\n", " \n", " def __str__(self):\n", " return \"%s %s was born in %d .\" % (self.name, self.surname, self.year_of_birth)\n", " \n", "alec = Person(\"Alec\", \"Baldwin\", 1958)\n", "print(alec)\n", "print(alec.age(2014))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We defined two more methods `age` and `__str__`. The latter is once again a special method that is called by Python when the object has to be represented as a string (e.g. when has to be printed). If the `__str__` method isn't defined the **print** command shows the type of object and its address in memory. We can see that in order to call a method we use the same syntax for attributes (**instance_name.instance _method**)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.3 Bad practice" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is possible to create a class without the `__init__` method, but this isn't a raccomended style because classes should describe omogeneous entities." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class Person:\n", " \n", " def set_name(self, name):\n", " self.name = name\n", " \n", " def set_surname(self, surname):\n", " self.surname = surname\n", " \n", " def set_year_of_birth(self, year_of_birth):\n", " self.year_of_birth = year_of_birth\n", " \n", " def age(self, current_year):\n", " return current_year - self.year_of_birth\n", " \n", " def __str__(self):\n", " return \"%s %s was born in %d .\" \\\n", " % (self.name, self.surname, self.year_of_birth)\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this case an empty instance of the class Person is created, and no attributes have been initialized while instantiating:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "president = Person()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'Person' object has no attribute 'name'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# This code will raise an attribute error:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpresident\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m: 'Person' object has no attribute 'name'" ] } ], "source": [ "# This code will raise an attribute error:\n", "print(president.name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This raises an Attribute Error... We need to set the attributes:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "president.set_name('John')\n", "president.set_surname('Doe')\n", "president.set_year_of_birth(1940)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mr John Doe is the president, and he is very old. He is 74\n" ] } ], "source": [ "print('Mr', president.name, president.surname,\n", " 'is the president, and he is very old. He is',\n", " president.age(2014))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1.4 Protect your abstraction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since objects are a powerfull mean of abstraction they shouldn't reveal internal implementation detail, hence instance attributes shouldn't be accessible by the end user of an object.\n", "Python doesn't have a strict mechanism to protect objects attributes, but official guidelines suggest that a variable that has an underscore `_` prefix should be treated as private.\n", "Moreover prepending two underscores to a variable name makes the intepreter mangle a little the variable name." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Alec Baldwin and was born 1958.\n", "56\n" ] } ], "source": [ "class Person:\n", " def __init__(self, name, surname, year_of_birth):\n", " self._name = name\n", " self._surname = surname\n", " self._year_of_birth = year_of_birth\n", " \n", " def age(self, current_year):\n", " return current_year - self._year_of_birth\n", " \n", " def __str__(self):\n", " return \"%s %s and was born %d.\" \\\n", " % (self._name, self._surname, self._year_of_birth)\n", " \n", "alec = Person(\"Alec\", \"Baldwin\", 1958)\n", "print(alec)\n", "print(alec.age(2014))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "dict_keys(['_Person__name', '_Person__surname', '_Person__year_of_birth'])\n" ] } ], "source": [ "class Person:\n", " def __init__(self, name, surname, year_of_birth):\n", " self.__name = name\n", " self.__surname = surname\n", " self.__year_of_birth = year_of_birth\n", " \n", " def age(self, current_year):\n", " return current_year - self.__year_of_birth\n", " \n", " def __str__(self):\n", " return \"%s %s and was born %d.\" \\\n", " % (self.__name, self.__surname, self.__year_of_birth)\n", " \n", "alec = Person(\"Alec\", \"Baldwin\", 1958)\n", "print(alec.__dict__.keys())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`__dict__` is a special attribute that is a dictionary containing each attribute of an object. We can see that prepending two underscores every key has `_ClassName__` prepended." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2 Inheritance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once a class is defined it models a concept. Often is usefull to extend a class behavior to model a less general concept. Say we need to model a Student, but we know that every student is also a Person so we shouldn't model the Person again but inherit from it instead." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Charlie Brown and was born 2006.\n", "\n", "True\n", "True\n" ] } ], "source": [ "class Student(Person):\n", " def __init__(self, student_id, *args, **kwargs):\n", " super(Student, self).__init__(*args, **kwargs)\n", " self._student_id = student_id\n", " \n", "charlie = Student(1, 'Charlie', 'Brown', 2006)\n", "print(charlie)\n", "print(type(charlie))\n", "print(isinstance(charlie, Person))\n", "print(isinstance(charlie, object))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Charlie now has the same behavior of a Person, but his state has also a student ID. Person is one of the base classes of Student and Student is one of the sub classes of Person. Be aware that a subclass knows about its superclasses but the converse isn't true.\n", "\n", "A sub class doesn't only inherits from its base classes, but from its base classes base classes too, forming an inheritance tree that starts from object (every class base class).\n", "\n", " super(Class, instance)\n", " \n", "is a function that returns a proxy-object that delegates method calls to a parent or sibling class of type.\n", "So we used it to access Person's `__init__`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2.1 Overriding methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Inheritance allows to add new methods to a subclass, but often is usefull to change the behavior of a method defined in the superclass. To override a method just define it again." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Charlie Brown and was born 2006. And has ID: 1\n" ] } ], "source": [ "class Student(Person):\n", " def __init__(self, student_id, *args, **kwargs):\n", " super(Student, self).__init__(*args, **kwargs)\n", " self._student_id = student_id\n", " \n", " def __str__(self):\n", " return super(Student, self).__str__() + \" And has ID: %d\" % self._student_id\n", " \n", "charlie = Student(1, 'Charlie', 'Brown', 2006)\n", "print(charlie)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We defined `__str__` again overriding the one wrote in Person, but we wanted to extend it, so we used super to achieve our goal." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3 Encapsulation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another powerful way to extend a class is called __encapsulation__ and consists on wrapping an object with a second one.\n", "There are two main reasons to use encapsulation:\n", "* Composition\n", "* Dynamic Extension\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3.1 Composition" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The abstraction process relies on creating a simplified model that remove useless details from a concept. In order to be simplified a model should be described in terms of other simpler concepts.\n", "For example we can say that a car is composed by:\n", "* Tyres\n", "* Engine\n", "* Body\n", "\n", "And break down each one of this elements in simpler parts until we reach primitive data." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Tyres: \n", " \tBranch: Pirelli\n", " \tBelted-bias: True\n", " \tOptimal pressure: 2.0\n", "Engine: \n", " \tFuel type: Diesel\n", " \tNoise level:3\n", "Body:\n", " \tSize: Medium\n" ] } ], "source": [ "class Tyres:\n", " def __init__(self, branch, belted_bias, opt_pressure):\n", " self.branch = branch\n", " self.belted_bias = belted_bias\n", " self.opt_pressure = opt_pressure\n", " \n", " def __str__(self):\n", " return (\"Tyres: \\n \\tBranch: \" + self.branch +\n", " \"\\n \\tBelted-bias: \" + str(self.belted_bias) + \n", " \"\\n \\tOptimal pressure: \" + str(self.opt_pressure))\n", " \n", "class Engine:\n", " def __init__(self, fuel_type, noise_level):\n", " self.fuel_type = fuel_type\n", " self.noise_level = noise_level\n", " \n", " def __str__(self):\n", " return (\"Engine: \\n \\tFuel type: \" + self.fuel_type +\n", " \"\\n \\tNoise level:\" + str(self.noise_level))\n", " \n", "class Body:\n", " def __init__(self, size):\n", " self.size = size\n", " \n", " def __str__(self):\n", " return \"Body:\\n \\tSize: \" + self.size\n", " \n", "class Car:\n", " def __init__(self, tyres, engine, body):\n", " self.tyres = tyres\n", " self.engine = engine\n", " self.body = body\n", " \n", " def __str__(self):\n", " return str(self.tyres) + \"\\n\" + str(self.engine) + \"\\n\" + str(self.body)\n", "\n", " \n", "t = Tyres('Pirelli', True, 2.0)\n", "e = Engine('Diesel', 3)\n", "b = Body('Medium')\n", "c = Car(t, e, b)\n", "print(c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3.2 Dynamic Extension" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sometimes it's necessary to model a concept that may be a subclass of another one, but it isn't possible to know which class should be its superclass until runtime." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.2.1 Example" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Suppose we want to model a simple dog school that trains instructors too. It will be nice to re-use Person and Student but students can be dogs or peoples. So we can remodel it this way:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Kudrjavka is a Laika born in 1954.\n" ] } ], "source": [ "class Dog:\n", " def __init__(self, name, year_of_birth, breed):\n", " self._name = name\n", " self._year_of_birth = year_of_birth\n", " self._breed = breed\n", "\n", " def __str__(self):\n", " return \"%s is a %s born in %d.\" % (self._name, self._breed, self._year_of_birth)\n", "\n", "kudrjavka = Dog(\"Kudrjavka\", 1954, \"Laika\")\n", "print(kudrjavka)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Alec Baldwin and was born 1958. Student ID: 1\n", "Kudrjavka is a Laika born in 1954. Student ID: 2\n" ] } ], "source": [ "class Student:\n", " def __init__(self, anagraphic, student_id):\n", " self._anagraphic = anagraphic\n", " self._student_id = student_id\n", " def __str__(self):\n", " return str(self._anagraphic) + \" Student ID: %d\" % self._student_id\n", "\n", "\n", "alec_student = Student(alec, 1)\n", "kudrjavka_student = Student(kudrjavka, 2)\n", "\n", "print(alec_student)\n", "print(kudrjavka_student)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4 Polymorphism and DuckTyping" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Python` uses dynamic typing (also called duck typing). If an object implements a method you can use it, no matter what the type is. This is different from statically typed languages, where the type of a construct need to be explicitly declared. Polymorphism is the ability to use the same syntax for objects of different types:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n", "['a', 'b', 'c', 'd', 'e']\n", "abracadabra\n" ] } ], "source": [ "def summer(a, b):\n", " return a + b\n", "\n", "print(summer(1, 1))\n", "print(summer([\"a\", \"b\", \"c\"], [\"d\", \"e\"]))\n", "print(summer(\"abra\", \"cadabra\"))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5 How long does a class should be?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "There is an Object Oriented Programming principle called SRP single responsability principle and it states: \"A class should have one single responsability\" or \"A class should have only one reason to change\". If you come up with a class that doesn't follow this principle you should consider to split it. You will be grateful to SRP during your software manteinance." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "Visit [www.add-for.com]() for more tutorials and updates.\n", "\n", "This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License." ] } ], "metadata": { "kernelspec": { "display_name": "Python [conda env:addfor_tutorials]", "language": "python", "name": "conda-env-addfor_tutorials-py" }, "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.6.4" } }, "nbformat": 4, "nbformat_minor": 1 }