{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Object Oriented Programming\n",
    "\n",
    "Object Oriented Programming (OOP) tends to be one of the major obstacles for beginners when they are first starting to learn Python.\n",
    "\n",
    "There are many, many tutorials and lessons covering OOP so feel free to Google search other lessons, and I have also put some links to other useful tutorials online at the bottom of this Notebook.\n",
    "\n",
    "For this lesson we will construct our knowledge of OOP in Python by building on the following topics:\n",
    "\n",
    "* Objects\n",
    "* Using the *class* keyword\n",
    "* Creating class attributes\n",
    "* Creating methods in a class\n",
    "* Learning about Inheritance\n",
    "* Learning about Polymorphism\n",
    "* Learning about Special Methods for classes\n",
    "\n",
    "Lets start the lesson by remembering about the Basic Python Objects. For example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "lst = [1,2,3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Remember how we could call methods on a list?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "lst.count(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "What we will basically be doing in this lecture is exploring how we could create an Object type like a list. We've already learned about how to create functions. So let's explore Objects in general:\n",
    "\n",
    "## Objects\n",
    "In Python, *everything is an object*. Remember from previous lectures we can use type() to check the type of object something is:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'int'>\n",
      "<class 'list'>\n",
      "<class 'tuple'>\n",
      "<class 'dict'>\n"
     ]
    }
   ],
   "source": [
    "print(type(1))\n",
    "print(type([]))\n",
    "print(type(()))\n",
    "print(type({}))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So we know all these things are objects, so how can we create our own Object types? That is where the <code>class</code> keyword comes in.\n",
    "## class\n",
    "User defined objects are created using the <code>class</code> keyword. The class is a blueprint that defines the nature of a future object. From classes we can construct instances. An instance is a specific object created from a particular class. For example, above we created the object <code>lst</code> which was an instance of a list object. \n",
    "\n",
    "Let see how we can use <code>class</code>:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class '__main__.Sample'>\n"
     ]
    }
   ],
   "source": [
    "# Create a new object type called Sample\n",
    "class Sample:\n",
    "    pass\n",
    "\n",
    "# Instance of Sample\n",
    "x = Sample()\n",
    "\n",
    "print(type(x))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By convention we give classes a name that starts with a capital letter. Note how <code>x</code> is now the reference to our new instance of a Sample class. In other words, we **instantiate** the Sample class.\n",
    "\n",
    "Inside of the class we currently just have pass. But we can define class attributes and methods.\n",
    "\n",
    "An **attribute** is a characteristic of an object.\n",
    "A **method** is an operation we can perform with the object.\n",
    "\n",
    "For example, we can create a class called Dog. An attribute of a dog may be its breed or its name, while a method of a dog may be defined by a .bark() method which returns a sound.\n",
    "\n",
    "Let's get a better understanding of attributes through an example.\n",
    "\n",
    "## Attributes\n",
    "The syntax for creating an attribute is:\n",
    "    \n",
    "    self.attribute = something\n",
    "    \n",
    "There is a special method called:\n",
    "\n",
    "    __init__()\n",
    "\n",
    "This method is used to initialize the attributes of an object. For example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Dog:\n",
    "    def __init__(self,breed):\n",
    "        self.breed = breed\n",
    "        \n",
    "sam = Dog(breed='Lab')\n",
    "frank = Dog(breed='Huskie')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lets break down what we have above.The special method \n",
    "\n",
    "    __init__() \n",
    "is called automatically right after the object has been created:\n",
    "\n",
    "    def __init__(self, breed):\n",
    "Each attribute in a class definition begins with a reference to the instance object. It is by convention named self. The breed is the argument. The value is passed during the class instantiation.\n",
    "\n",
    "     self.breed = breed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we have created two instances of the Dog class. With two breed types, we can then access these attributes like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Lab'"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sam.breed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Huskie'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "frank.breed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note how we don't have any parentheses after breed; this is because it is an attribute and doesn't take any arguments.\n",
    "\n",
    "In Python there are also *class object attributes*. These Class Object Attributes are the same for any instance of the class. For example, we could create the attribute *species* for the Dog class. Dogs, regardless of their breed, name, or other attributes, will always be mammals. We apply this logic in the following manner:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Dog:\n",
    "    \n",
    "    # Class Object Attribute\n",
    "    species = 'mammal'\n",
    "    \n",
    "    def __init__(self,breed,name):\n",
    "        self.breed = breed\n",
    "        self.name = name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "sam = Dog('Lab','Sam')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Sam'"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sam.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that the Class Object Attribute is defined outside of any methods in the class. Also by convention, we place them first before the init."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'mammal'"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sam.species"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Methods\n",
    "\n",
    "Methods are functions defined inside the body of a class. They are used to perform operations with the attributes of our objects. Methods are a key concept of the OOP paradigm. They are essential to dividing responsibilities in programming, especially in large applications.\n",
    "\n",
    "You can basically think of methods as functions acting on an Object that take the Object itself into account through its *self* argument.\n",
    "\n",
    "Let's go through an example of creating a Circle class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Radius is:  1\n",
      "Area is:  3.14\n",
      "Circumference is:  6.28\n"
     ]
    }
   ],
   "source": [
    "class Circle:\n",
    "    pi = 3.14\n",
    "\n",
    "    # Circle gets instantiated with a radius (default is 1)\n",
    "    def __init__(self, radius=1):\n",
    "        self.radius = radius \n",
    "        self.area = radius * radius * Circle.pi\n",
    "\n",
    "    # Method for resetting Radius\n",
    "    def setRadius(self, new_radius):\n",
    "        self.radius = new_radius\n",
    "        self.area = new_radius * new_radius * self.pi\n",
    "\n",
    "    # Method for getting Circumference\n",
    "    def getCircumference(self):\n",
    "        return self.radius * self.pi * 2\n",
    "\n",
    "\n",
    "c = Circle()\n",
    "\n",
    "print('Radius is: ',c.radius)\n",
    "print('Area is: ',c.area)\n",
    "print('Circumference is: ',c.getCircumference())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the \\__init__ method above, in order to calculate the area attribute, we had to call Circle.pi. This is because the object does not yet have its own .pi attribute, so we call the Class Object Attribute pi instead.<br>\n",
    "In the setRadius method, however, we'll be working with an existing Circle object that does have its own pi attribute. Here we can use either Circle.pi or self.pi.<br><br>\n",
    "Now let's change the radius and see how that affects our Circle object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Radius is:  2\n",
      "Area is:  12.56\n",
      "Circumference is:  12.56\n"
     ]
    }
   ],
   "source": [
    "c.setRadius(2)\n",
    "\n",
    "print('Radius is: ',c.radius)\n",
    "print('Area is: ',c.area)\n",
    "print('Circumference is: ',c.getCircumference())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Great! Notice how we used self. notation to reference attributes of the class within the method calls. Review how the code above works and try creating your own method.\n",
    "\n",
    "## Inheritance\n",
    "\n",
    "Inheritance is a way to form new classes using classes that have already been defined. The newly formed classes are called derived classes, the classes that we derive from are called base classes. Important benefits of inheritance are code reuse and reduction of complexity of a program. The derived classes (descendants) override or extend the functionality of base classes (ancestors).\n",
    "\n",
    "Let's see an example by incorporating our previous work on the Dog class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Animal:\n",
    "    def __init__(self):\n",
    "        print(\"Animal created\")\n",
    "\n",
    "    def whoAmI(self):\n",
    "        print(\"Animal\")\n",
    "\n",
    "    def eat(self):\n",
    "        print(\"Eating\")\n",
    "\n",
    "\n",
    "class Dog(Animal):\n",
    "    def __init__(self):\n",
    "        Animal.__init__(self)\n",
    "        print(\"Dog created\")\n",
    "\n",
    "    def whoAmI(self):\n",
    "        print(\"Dog\")\n",
    "\n",
    "    def bark(self):\n",
    "        print(\"Woof!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Animal created\n",
      "Dog created\n"
     ]
    }
   ],
   "source": [
    "d = Dog()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Dog\n"
     ]
    }
   ],
   "source": [
    "d.whoAmI()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Eating\n"
     ]
    }
   ],
   "source": [
    "d.eat()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Woof!\n"
     ]
    }
   ],
   "source": [
    "d.bark()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example, we have two classes: Animal and Dog. The Animal is the base class, the Dog is the derived class. \n",
    "\n",
    "The derived class inherits the functionality of the base class. \n",
    "\n",
    "* It is shown by the eat() method. \n",
    "\n",
    "The derived class modifies existing behavior of the base class.\n",
    "\n",
    "* shown by the whoAmI() method. \n",
    "\n",
    "Finally, the derived class extends the functionality of the base class, by defining a new bark() method."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Polymorphism\n",
    "\n",
    "We've learned that while functions can take in different arguments, methods belong to the objects they act on. In Python, *polymorphism* refers to the way in which different object classes can share the same method name, and those methods can be called from the same place even though a variety of different objects might be passed in. The best way to explain this is by example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Niko says Woof!\n",
      "Felix says Meow!\n"
     ]
    }
   ],
   "source": [
    "class Dog:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "\n",
    "    def speak(self):\n",
    "        return self.name+' says Woof!'\n",
    "    \n",
    "class Cat:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "\n",
    "    def speak(self):\n",
    "        return self.name+' says Meow!' \n",
    "    \n",
    "niko = Dog('Niko')\n",
    "felix = Cat('Felix')\n",
    "\n",
    "print(niko.speak())\n",
    "print(felix.speak())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here we have a Dog class and a Cat class, and each has a `.speak()` method. When called, each object's `.speak()` method returns a result unique to the object.\n",
    "\n",
    "There a few different ways to demonstrate polymorphism. First, with a for loop:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Niko says Woof!\n",
      "Felix says Meow!\n"
     ]
    }
   ],
   "source": [
    "for pet in [niko,felix]:\n",
    "    print(pet.speak())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Another is with functions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Niko says Woof!\n",
      "Felix says Meow!\n"
     ]
    }
   ],
   "source": [
    "def pet_speak(pet):\n",
    "    print(pet.speak())\n",
    "\n",
    "pet_speak(niko)\n",
    "pet_speak(felix)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "In both cases we were able to pass in different object types, and we obtained object-specific results from the same mechanism.\n",
    "\n",
    "A more common practice is to use abstract classes and inheritance. An abstract class is one that never expects to be instantiated. For example, we will never have an Animal object, only Dog and Cat objects, although Dogs and Cats are derived from Animals:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fido says Woof!\n",
      "Isis says Meow!\n"
     ]
    }
   ],
   "source": [
    "class Animal:\n",
    "    def __init__(self, name):    # Constructor of the class\n",
    "        self.name = name\n",
    "\n",
    "    def speak(self):              # Abstract method, defined by convention only\n",
    "        raise NotImplementedError(\"Subclass must implement abstract method\")\n",
    "\n",
    "\n",
    "class Dog(Animal):\n",
    "    \n",
    "    def speak(self):\n",
    "        return self.name+' says Woof!'\n",
    "    \n",
    "class Cat(Animal):\n",
    "\n",
    "    def speak(self):\n",
    "        return self.name+' says Meow!'\n",
    "    \n",
    "fido = Dog('Fido')\n",
    "isis = Cat('Isis')\n",
    "\n",
    "print(fido.speak())\n",
    "print(isis.speak())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Real life examples of polymorphism include:\n",
    "* opening different file types - different tools are needed to display Word, pdf and Excel files\n",
    "* adding different objects - the `+` operator performs arithmetic and concatenation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Special Methods\n",
    "Finally let's go over special methods. Classes in Python can implement certain operations with special method names. These methods are not actually called directly but by Python specific language syntax. For example let's create a Book class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Book:\n",
    "    def __init__(self, title, author, pages):\n",
    "        print(\"A book is created\")\n",
    "        self.title = title\n",
    "        self.author = author\n",
    "        self.pages = pages\n",
    "\n",
    "    def __str__(self):\n",
    "        return \"Title: %s, author: %s, pages: %s\" %(self.title, self.author, self.pages)\n",
    "\n",
    "    def __len__(self):\n",
    "        return self.pages\n",
    "\n",
    "    def __del__(self):\n",
    "        print(\"A book is destroyed\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "A book is created\n",
      "Title: Python Rocks!, author: Jose Portilla, pages: 159\n",
      "159\n",
      "A book is destroyed\n"
     ]
    }
   ],
   "source": [
    "book = Book(\"Python Rocks!\", \"Jose Portilla\", 159)\n",
    "\n",
    "#Special Methods\n",
    "print(book)\n",
    "print(len(book))\n",
    "del book"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "    The __init__(), __str__(), __len__() and __del__() methods\n",
    "These special methods are defined by their use of underscores. They allow us to use Python specific functions on objects created through our class.\n",
    "\n",
    "**Great! After this lecture you should have a basic understanding of how to create your own objects with class in Python. You will be utilizing this heavily in your next milestone project!**\n",
    "\n",
    "For more great resources on this topic, check out:\n",
    "\n",
    "[Jeff Knupp's Post](https://jeffknupp.com/blog/2014/06/18/improve-your-python-python-classes-and-object-oriented-programming/)\n",
    "\n",
    "[Mozilla's Post](https://developer.mozilla.org/en-US/Learn/Python/Quickly_Learn_Object_Oriented_Programming)\n",
    "\n",
    "[Tutorial's Point](http://www.tutorialspoint.com/python/python_classes_objects.htm)\n",
    "\n",
    "[Official Documentation](https://docs.python.org/3/tutorial/classes.html)"
   ]
  }
 ],
 "metadata": {
  "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.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}