{ "metadata": { "name": "" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Classes and Objects\n", "\n", "_author Cindee Madison\n", "\n", "**Inspired by Materials by Tommy Guy and Anthony Scopatz**\n", "\n", "\n", "You can get a master copy (with all solutions) [here](http://swcarpentry.github.io/2014-04-14-wise/advanced/python/classes-master.ipynb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Object Orientation\n", "\n", "**Object oriented programming** is a way of thinking about and defining how different pieces of software and ideas work together. In object-oriented programming, there are two main interfaces: **classes** and **objects**. \n", "\n", "* **Classes** are types of things such as int, float, person, or square.\n", "* **Objects** are **instances** of those types such a 1, 42.0, me, and a square with side length 2.\n", "\n", "Unlike functional or procedural paradigms, there are three main features that classes provide.\n", "\n", "* **Encapsulation:** Classes are container which may have any kind of other programming element living on them: variables, functions, and even other classes. In Python, members of a class are known as **attributes** for normal variables and **methods** for functions.\n", "\n", "* **Inheritence:** A class may automatically gain all of the attributes and methods from another class it is related to. The new class is called a **subclass** or sometimes a **subtype**. Multiple levels of inheritance sets up a **class heirarchy**. For example:\n", "\n", " - Shape is a class with an area attribute.\n", " - Rectangle is a subclass of Shape.\n", " - Square is a subclass of Rectangle (which also makes it a subclass of Shape).\n", " \n", " \n", "* **Polymorphism:** Subclasses may override methods and attributes of their parents in a way that suitable to them. For example:.\n", "\n", " - Shape is a class with an area method.\n", " - Square is a subclass of Shape which computes area by $x^2$.\n", " - Circle is a subclass of Shape which computes area by $\\pi r^2$\n", "\n", "If this seems more complicated than writing functions and calling them in sequence that is because it is! However, obeject orientation enables authors to cleanly separate out ideas into independent classes. It is also good to know because in many languages - Python included - it is the way that you modify the type system. \n", "\n", "### We will learn about classes and find ways to use them to make your life easier\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Basic Classes\n", "\n", "Object oriented programming revolves around the creation and\n", "manipulation of objects that have attributes and can do things. \n", "\n", "They can\n", "be as simple as a coordinate with **x** and **y** values or as complicated as a\n", "dynamic webpage framework. \n", "\n", "Here is the code for making a very simple class \n", "that sets an attribute. " ] }, { "cell_type": "code", "collapsed": false, "input": [ "class MyClass(object):\n", " def give_me_a_name(self, name):\n", " self.name = name\n", " " ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# create an instance called my_object\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# set the name of my_object to 'Tris' using give_me_a_name\n", "# print my_object.name\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# set the name through direct access, and print result" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the object oriented terminology above:\n", "\n", "- MyClass - a user defined type.\n", "- my_object - an instance of MyClass.\n", "- give_me_a_name() - a MyClass method (member function)\n", "- self - a reference to the object that is used to define method. This must be the first agument of any method.\n", "- name - an attribute (member variable) of MyClass.\n", "- `object` - a special class which should be the parent of all classes.\n", "\n", "You *write* a class and you *create* an instance of an object.\n", "\n", "###Exercise:\n", "\n", "1. create a new instance of the MyClass object called **newclass**\n", "2. what is contained in the **name** attribute?\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##Constructors\n", "\n", "Usually you want to create an object with a set of initial or default values for\n", "things. Perhaps an object needs certain information to be created. \n", "\n", "For this you write a **constructor**. In python, constructors are just methods\n", "with the special name ``__init__()``:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class Person(object):\n", " \"\"\"creates a person with the name Ada\"\"\"\n", " def __init__(self):\n", " self.name = \"Ada\"" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# create an instance of Person called person\n", "# print person.name\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Constructors may take arguments just like any other method or function. This gives us flexibility for the different instances." ] }, { "cell_type": "code", "collapsed": false, "input": [ "class Person(object):\n", " def __init__(self, name, title=\"Programmer\"):\n", " \"\"\"Create Person object with name and title\"\"\"\n", " self.name = name\n", " self.title = title" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## create a new object called grace, setting the name to 'Grace Hopper'\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## create a new object called 'Ada Lovelace' and change her title to 'Mother of Computing'\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Example Class Gotcha\n", "\n", "If you specify an attribute outside fo the init (and you will see examples of this), you need to be aware of behavior if the attribute is a mutable attribute (such as an array).\n", "\n", "Consider the following class" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class MakeList(object):\n", " something = []\n", " def append_input(self, input):\n", " self.something.append(input)\n", " \n", " \n", "firstlist = MakeList()\n", "secondlist = MakeList()\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## put 23 into the 'something' list of your first instance\n", "\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "#what will be in the something attribute for firstlist\n", "#what about secondlist?\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is an example of an attribute that is shared across instances of a class. And is not the behavior we would gernerally want.\n", "\n", "So How do we fix this?\n", "\n", "## Excercise\n", "Fix the broken behavior in MakeList by moving the attribute **something** to ```__init__```" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# Lets make a quick test so you can see if your solution works\n", "def test_MakeList():\n", " first = MakeList()\n", " second = MakeList()\n", " first.append_input(42)\n", " print first.something, second.something\n", " assert (first.something == second.something) is False\n", " \n", "#this should raise an Assertion Error\n", "test_MakeList()\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## make a new class, create something in the __init__ method\n", " " ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## Test it!\n", "test_MakeList()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example Class for data \n", "\n", "If you want a to create a class that behaves mostly like another class,\n", "you should not have to copy code. What you do is subclass and change the\n", "things that need changing. When we created classes we were already\n", "subclassing the built in python class \"object.\"\n", "\n", "\n", "Consider the following data model.\n", "\n", "You are a biologist collecting data on a group of creatures (dogs, cats birds, choose your favorite).\n", "\n", "* you have a unique ID for each creature\n", "* you care about weight and gender\n", "* you want to be able to easily compare weights (eg creature_a is heavier than creature_b\n", "\n", "you could create a nested dictionary\n", "\n", "```python\n", "mycreatures = {\n", " 'id001': {'weight': 32.4, 'gender': 'Female'},\n", " 'id002': {'weight': 36.7, 'gender': 'Male'},\n", " }\n", "```\n", "\n", "But it makes it hard to easily access and compare the different creatures. So lets make a subclass of the basic object class in python and over-ride a couple methods (functions) to get the behavior we want.\n", "\n", "\n", "To do this we have to implement part of the [Python Data Model](http://docs.python.org/2/reference/datamodel.html). Python has a list of special - or sometimes known as magic - method names that you can override to implement support for many language operations. All of these method names start and end with a double underscore `__`. This is because no regular method would ever use such an obtuse name. It also lets the user and other developers know that something special is happening in those methods and that they aren't meant to be called directly. Many of these has a predefined interface they must follow.\n", "\n", "We have already seen an example of this with the `__init__()` constructor method. Now let's try to make comparisons work for Creature, specifically by comparing weights. From the documentation, there is:\n", "\n", "```python\n", "object.__lt__(self, other) **less than**\n", "object.__gt__(self, other) **greater than**\n", "object.__eq__(self, other) **equal**\n", "object.__ne__(self, other) **not equal**\n", "```\n", "\n", "\n", "Another useful special method is the `__str__()` method, which allows you to provide a string representation of the object.\n", "In our case this lets us easily print the name, weight and gender of our creature.\n", "\n", " " ] }, { "cell_type": "code", "collapsed": false, "input": [ "# make a generic creature class\n", "\n", "class Creature(object):\n", " def __init__(self, name, weight, gender):\n", " \"\"\"Create instance of creature with a name, weight, and gender\"\"\"\n", " self.name = name\n", " self.weight = weight\n", " self.gender = gender\n", " \n", " def __lt__(self, other):\n", " \"\"\"check if weight is less than other\"\"\"\n", " return self.weight < other\n", " def __gt__(self, other):\n", " \"\"\"check if weight is greater than other\"\"\"\n", " return self.weight > other\n", " def __et__(self, other):\n", " \"\"\"check of weight is equal to other\"\"\"\n", " return self.weight == other\n", " def __ne__(self, other):\n", " \"\"\"check if weight is not equal to other\"\"\"\n", " return not self.weight == other\n", " \n", " def __str__(self):\n", " \"\"\"name, weight and gender of specific creature instance\"\"\"\n", " return 'Name: {} is a {} with weight {:.2f}'.format(self.name, self.gender, self.weight)\n", " " ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## Create a few animals\n", "animal_a = Creature('001', 34.2,'Female')\n", "animal_b = Creature('002', 36.7, 'Male')\n", "animal_c = Creature('003', 32.4, 'Male')\n", " " ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## Check comparison is on weight\n", "\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## Now if we put these creatures in a list, we can sort them by weight\n", "\n", "\n", "## sort list by weight\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###Exercise:\n", "\n", "Inheritance can be tricky...So lets use a simple set of classes and try to figure out how they will behave.\n", "We define three classes below, with simple attributes.\n", "* **B** inherits from **A**\n", "* **C** inherits from **B** which inherits from **A**\n", "\n", "if **c_instance** is an instance of C:\n", "\n", "1. will it have an attribute **a**?\n", "2. What will be the value of **b**?\n", "3. If you create an instance of **A** called **a_instance**, will it have the **c** attribute?" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class A(object):\n", " a = 'a is a'\n", "\n", "class B(A):\n", " b = 'b is b'\n", " \n", "class C(B):\n", " b = 'b is 42'\n", " c = 'c is c'\n", "\n", "c_instance = C()\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# Is a an attribute of c_instance?\n", "\n", "#Is b an attribute of c_instance?\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# create a new instance of A, what is the value of the attribute c?\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Properties\n", "\n", "Normally, when you get or set attributes on an object the value that you are setting simply gets a new name. However, sometimes you run into the case where you want to do something extra depending on the actual value you are reciveing. For example, maybe you need to confirm that the value is actually correct or desired.\n", "\n", "\n", "* In our Creature class, we want weight to be a float, otherwise we will get unexpected behavior.\n", "\n", "\n", "Python provides a mechanism called **properties** to do this. Properties are methods which either get, set, or delete a given attribute. To implement this, use the built-in `property()` decorator:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "## example where our class fails\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "class Creature(object):\n", " def __init__(self, name, weight, gender):\n", " \"\"\"Create instance of creature with a name, weight, and gender\"\"\"\n", " self.name = name\n", " self._weight = 0\n", " self.weight = weight\n", " self.gender = gender\n", " \n", " @property\n", " def weight(self):\n", " # getter\n", " return self._weight\n", " \n", " @weight.setter\n", " def weight(self, val):\n", " # setter check val is float\n", " if isinstance(val, float):\n", " self._weight = val\n", " else:\n", " raise ValueError('Weight must be a float, not {}:{}'.format(type(val),val))\n", " \n", " \n", " def __lt__(self, other):\n", " \"\"\"check if weight is less than other\"\"\"\n", " return self.weight < other\n", " def __gt__(self, other):\n", " \"\"\"check if weight is greater than other\"\"\"\n", " return self.weight > other\n", " def __et__(self, other):\n", " \"\"\"check of weight is equal to other\"\"\"\n", " return self.weight == other\n", " def __ne__(self, other):\n", " \"\"\"check if weight is not equal to other\"\"\"\n", " return not self.weight == other\n", " \n", " def __str__(self):\n", " \"\"\"name, weight and gender of specific creature instance\"\"\"\n", " return 'Name: {} is a {} with weight {:.2f}'.format(self.name, self.gender, self.weight)\n", " \n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## Now what happens when we pass a bad weight value?\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "## create a good_creature named 'bird_001', with weight 32.5, and gender 'Female'\n", "## print result\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Bonus Exercise\n", "\n", "Can you think of how to check that gender is Male or Female?" ] }, { "cell_type": "code", "collapsed": false, "input": [ "## create Creature Class using property to check gender input\n", "\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": true, "input": [ "## Check gender\n", "good_gender = Creature('Bruce', 32.4, 'Male')\n", "print good_gender\n", "bad_gender = Creature('abby', 32.4, 'blue')" ], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] }