{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Object-Oriented Programming in Python\n", "\n", "### An experiment\n", "What if I wanted to to create heros and monsters in a video game?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#You might start off with\n", "name = \"hero\"\n", "health = 100\n", "level = 10\n", "#or if you were clever and wanted to make a single object you might just go with a dictionary\n", "hero = {name: \"hero\", health: 100, level: 1}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But now let's say we want to make a monster for the hero to fight. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "monster_1 = {name: \"Gargantuan\", health: 1000, level: 100}\n", "#Now I have the monster, but I need a bunch of them to fight, but we can at least always give them the same stats\n", "#I'll let you all make two more. Also put in a category that declares what the monster wants to fight\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#make your monsters here. Actually, make 10" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, this can get pretty old, make difficult to read code, and just suck in general. But we can make this much easier! And do some powerful things along the way. First, let's make a class." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What is a class?\n", "A blueprint for an object that contains:\n", " - Properties\n", " - Functions related to that object type\n", "![class.png](img/class.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's make a class!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Creature():\n", " \n", " #the equals I am putting here represent what the default will be if nothing is passed\n", " def __init__(self, name, health=10, level=1):\n", " self.name = name\n", " self.health = health\n", " self.level = level\n", " \n", " def receive_damage(self,damage): #we'll talk about why I am writing this this way in a second.\n", " self.health = self.health - damage" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The \"\\__init\\__\" part is sometimes called a constructer. It determines what happens when an instance of that class is created or *instantiated*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Now let's create some\n", "hero = Creature(\"hero1\", 100, 10)\n", "monster1 = Creature(\"Gargantuan\", 1000, 100)\n", "\n", "print(\"monster name: \" + str(monster1.name) )\n", "print(\"hero level: \" + str(hero.level) )\n", "\n", "#Can we make a few more even more easily? Yes! With default values!\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Yay! Now we have some characters! But what if we want to create a bunch? Let's make a list!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "list_of_creatures=[Creature(\"monster\" + str(i)) for i in range(10)]\n", "print(\"Monster number: \" + list_of_creatures[0].name)\n", "\n", "#make it automatically make reciprocal enemies by self.enemy = new_enemy and new_enemy.enemy = self\n", "\n", "#at least mention static and class methods" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Question: what is the difference between a function and method?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Inheritence\n", "One of the fundamental concepts of OOP is inheritence. Basically it means you can create a subclass of a class and import everything from the parent class automatically. For example: a monster is a creature. But we want to add other properties to the monster.\n", "\n", "![inheritance.png](img/poo-images-animaux.gif)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Monster(Creature):\n", " #these are class properties. These are shared by the entire class, and are not particular to a single instance.\n", " monster_count = 0\n", " \n", " def __init__(self, name, health=10, level=1):\n", " #anything coded here with \"self\" is an instance property\n", " Creature.__init__(self, name, health, level) #here we must include a call to the parent class's init function\n", " #if we want to keeep it because the child class init overrides it\n", " self.enemy = [] #this will be a list of all enemies\n", " Monster.monster_count = Monster.monster_count + 1\n", " \n", " def add_enemy(self, enemy):\n", " self.enemy.append(enemy)\n", " enemy.enemy = self #This is very interesting, we can program a reciprocal change of state!\n", " #You'll also notice we never declared an enemy property in Creature, but\n", " #we're doing it anyway. Python is very flexible, but you probably \n", " #shouldn't do this!\n", " \n", " def attack(self, enemy):\n", " enemy.health = enemy.health - 1 #Why is this method a terrible idea?\n", " #what should I make instead?\n", " \n", " def attack_creature(self, enemy): #fill this one in in a better way\n", " enemy.receive_damage(1)\n", " " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_monster = Monster(\"monster15\")\n", "my_monster.add_enemy(hero)\n", "\n", "print(\"Enemy of monster: \" + my_monster.enemy[0].name)\n", "print(\"Enemy of hero: \" + hero.enemy.name)\n", "print(\"Monster count: \" + str(Monster.monster_count))\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Now what if we make a new monster?\n", "second_monster = Monster(\"Monster16\")\n", "print(\"Monster count: \" + str(Monster.monster_count))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#attack\n", "my_monster.attack_creature(hero)\n", "my_monster.__dict__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fundamental Concepts:\n", "### Abstraction and Encapsulation\n", " These are actually two different concepts but we'll combine them because they do overlap. Basically, they mean we create objects with the idea of focusing on what they do rather than how they work. Everyone knows what a car does and the methods for getting it from point A to point B, at least in principle. But far fewer people know all the details of how everything works under the hood.\n", " \n", " Where have we seen this so far?\n", " 1. __init__:\n", " Dunder is a way for Python to make sure a method or porperty is not easily called outside the class.\n", " 2. The attack method\n", " \n", "### Polymorphism\n", "This basically means that objects can be created that do the same thing with different data types. Python has so much polymorphism flowing through it it really is hardly worth it to point it out. The biggest reason to notice is to point out that it is acceptable to override a method of the same name from a higher class. That can be a big time saver or make things work that didn't before. But it can also lead to errors later when people mess with your code and remove yur override and leave the code depending on it behind for example. Mostly that's important to know for some high-level debugging. But in data science you should never really have to know that." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Why not always use OOP, especially in data science?\n", "1. May add unnecessary complexity\n", "2. Can slow down a program" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#look at everything inside just one object\n", "class myClass():\n", " pass\n", "thing = myClass()\n", "dir(thing)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cool techniques\n", "### Method Chaining\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class fMRI_Data_Volume():\n", " def __init__(self, volume, scan_type):\n", " self.volume = volume\n", " self.scan_type = scan_type\n", " \n", " def double_size(self):\n", " self.volume.extend(self.volume) #adds a copy of the list to the list\n", " return self\n", " \n", " def halve_size(self):\n", " self.volume = self.volume[:round((len(self.volume) / 2))] #selects the first half of my list\n", " return self\n", " \n", " def add_a(self,item):\n", " self.volume.append(item)\n", " return self\n", " \n", " def add_items(self,items):\n", " self.volume.extend(items)\n", " return self" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_volume = fMRI_Data_Volume([5,256,3], \"dti\")\n", "\n", "print(my_volume.volume)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_volume.double_size()\n", "print(my_volume.volume)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_volume.double_size().halve_size()\n", "print(my_volume.volume)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "my_volume.double_size().add_a(6).halve_size().add_a(6).add_items([2,8,7,6,5,4,3,2,1]).double_size()\n", "print(my_volume.volume)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### In Python, everything is an object!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def func():\n", " print(\"blah\")\n", "func()\n", "#functions can have attributes\n", "#func.other_name = \"hello\"\n", "#func.other_name\n", "\n", "#we can even reassign, just like any other object\n", "bloop = func\n", "bloop()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#What are all the parts of bloop? What does it inherit?\n", "dir(bloop)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#what about just inside of the object?\n", "bloop.__dict__" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#There was nothing inside. Let's add an attribute\n", "#Then we can see what belongs to just this object\n", "bloop.my_attr = \"1st attribute\"\n", "#now\n", "bloop.__dict__" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#What about the string \"hello\"\n", "dir(\"hello\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#What else can we see inside?\n", "dir(bloop.__code__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Testing the class, or using it as a script\n", "anything under if \\__name\\__ == '\\__main\\__':\n", "runs when running the class as a script. If you ever import it into something else, it won't" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Creature():\n", " \n", " #the equals I am putting here represent what the default will be if nothing is passed\n", " def __init__(self, name, health=10, level=1):\n", " self.name = name\n", " self.health = health\n", " self.level = level\n", " \n", " def receive_damage(self,damage): #we'll talk about why I am writing this this way in a second.\n", " self.health = self.health - damage\n", " \n", " #let's test: if you import it later, it won't run\n", " if __name__ == '__main__':\n", " a_hero = Creature(\"Jordyn\")\n", " print(a_hero.name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#later, if we import it, (from a different file or from an installed package)\n", "from Creature import Creature\n", "new_c = Creature(\"hello\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Excercises\n", "1. Make a basic car and give it attributes like speed, color, and anything else you want\n", "2. Make a specific brand of car that inherits from that class and includes features specific to that brand, like headlamp washers.\n", "3. Make a method for the car that can be chained. Maybe for increasing its speed\n", "4. Take a look at what the members of your car sublcass are using dir() and \\__dict\\__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### IF we have time, go over wrappers\n", "\n", "\\__getattr\\__ intercepts access to nonexistent attributes. Can be used to route arbitrary access to a wrapped object.\n", "retains the interface of the wrapped object and may add additional operations of its own.\n", "Basically, you might need them when using code that changes interfaces, like from an IP port to Bluetooth, or from one programming language to another." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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.5.5" } }, "nbformat": 4, "nbformat_minor": 2 }