{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from __future__ import print_function" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Classes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Classes are the fundamental concept for object oriented programming. A class defines a data type with both data and functions that can operate on the data. An object is an instance of a class. Each object will have its own namespace (separate from other instances of the class and other functions, etc. in your program).\n", "\n", "We use the dot operator, `.`, to access members of the class (data or functions). We've already been doing this a lot, strings, ints, lists, ... are all objects in python." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "simplest example: just a container (like a struct in C)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 2 3\n", "1 2\n" ] } ], "source": [ "class Container(object):\n", " pass\n", " \n", "a = Container()\n", "a.x = 1\n", "a.y = 2\n", "a.z = 3\n", "\n", "b = Container()\n", "b.xyz = 1\n", "b.uvw = 2\n", "\n", "print(a.x, a.y, a.z)\n", "print(b.xyz, b.uvw)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "notice that you don't have to declare what variables are members of the class ahead of time (although, usually that's good practice).\n", "\n", "Here, we give the class name an argument, `object`. This is an example of inheritance. For a general class, we inherit from the base python `object` class." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## More useful class" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's a class that holds some student info" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class Student(object):\n", " def __init__(self, name, grade=None):\n", " self.name = name\n", " self.grade = grade" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mike\n", "None\n" ] } ], "source": [ "a = Student(\"Mike\")\n", "print(a.name)\n", "print(a.grade)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's create a bunch of them, stored in a list" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "students = []\n", "students.append(Student(\"fry\", \"F-\"))\n", "students.append(Student(\"leela\", \"A\"))\n", "students.append(Student(\"zoidberg\", \"F\"))\n", "students.append(Student(\"hubert\", \"C+\"))\n", "students.append(Student(\"bender\", \"B\"))\n", "students.append(Student(\"calculon\", \"C\"))\n", "students.append(Student(\"amy\", \"A\"))\n", "students.append(Student(\"hermes\", \"A\"))\n", "students.append(Student(\"scruffy\", \"D\"))\n", "students.append(Student(\"flexo\", \"F\"))\n", "students.append(Student(\"morbo\", \"D\"))\n", "students.append(Student(\"hypnotoad\", \"A+\"))\n", "students.append(Student(\"zapp\", \"Q\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Quick Exercise:

\n", "\n", "Loop over the students in the `students` list and print out the name and grade of each student, one per line.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fry F-\n", "leela A\n", "zoidberg F\n", "hubert C+\n", "bender B\n", "calculon C\n", "amy A\n", "hermes A\n", "scruffy D\n", "flexo F\n", "morbo D\n", "hypnotoad A+\n", "zapp Q\n" ] } ], "source": [ "for s in students:\n", " print(s.name, s.grade)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use list comprehensions with our list of objects. For example, let's find all the students who have A's" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['leela', 'amy', 'hermes', 'hypnotoad']" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "As = [q.name for q in students if q.grade.startswith(\"A\")]\n", "As" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Playing Cards" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "here's a more complicated class that represents a playing card. Notice that we are using unicode to represent the suits.\n", "\n", "unicode support in python is also one of the major differences between python 2 and 3. In python 3, every string is unicode." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "class Card(object):\n", " \n", " def __init__(self, suit=1, rank=2):\n", " if suit < 1 or suit > 4:\n", " print(\"invalid suit, setting to 1\")\n", " suit = 1\n", " \n", " \n", " self.suit = suit\n", " self.rank = rank\n", " \n", "\n", " def value(self):\n", " \"\"\" we want things order primarily by rank then suit \"\"\"\n", " return self.suit + (self.rank-1)*14\n", " \n", " # we include this to allow for comparisons with < and > between cards \n", " def __lt__(self, other):\n", " return self.value() < other.value()\n", "\n", " def __unicode__(self):\n", " suits = [u\"\\u2660\", # spade\n", " u\"\\u2665\", # heart\n", " u\"\\u2666\", # diamond\n", " u\"\\u2663\"] # club\n", " \n", " r = str(self.rank)\n", " if self.rank == 11:\n", " r = \"J\"\n", " elif self.rank == 12:\n", " r = \"Q\"\n", " elif self.rank == 13:\n", " r = \"K\"\n", " elif self.rank == 14:\n", " r = \"A\"\n", " \n", " return r +':'+suits[self.suit-1]\n", " \n", " def __str__(self):\n", " return self.__unicode__() #.encode('utf-8')\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When you instantiate a class, the `__init__` method is called. Note that all method in a class always have \"`self`\" as the first argument -- this refers to the object that is invoking the method.\n", "\n", "we can create a card easily." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2:♠\n" ] } ], "source": [ "c1 = Card()\n", "print(c1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can pass arguments to `__init__` in when we setup the class:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "K:♠\n" ] } ], "source": [ "c2 = Card(suit=1, rank=13)\n", "print(c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we have our object, we can access any of the functions in the class using the `dot` operator" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "15\n" ] } ], "source": [ "print(c1.value())" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "invalid suit, setting to 1\n" ] } ], "source": [ "c3 = Card(suit=0, rank=4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `__str__` method converts the object into a string that can be printed. The `__unicode__` method is actually for python 2." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2:♠\n", "K:♠\n" ] } ], "source": [ "print(c1)\n", "print(c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "the value method assigns a value to the object that can be used in comparisons, and the `__lt__` method is what does the actual comparing" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "False\n", "True\n" ] } ], "source": [ "print(c1 > c2)\n", "print(c1 < c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that not every operator is defined for our class, so, for instance, we cannot add two cards together:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for +: 'Card' and 'Card'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mc1\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mc2\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for +: 'Card' and 'Card'" ] } ], "source": [ "c1 + c2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Quick Exercise:

\n", "\n", " * Create a \"hand\" corresponding to a straight (5 cards of any suite, but in sequence of rank)\n", " * Create another hand corresponding to a flush (5 cards all of the same suit, of any rank)\n", " * Finally create a hand with one of the cards duplicated—this should not be allowed in a standard deck of cards. How would you check for this?\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "invalid suit, setting to 1\n", "9:♠\n", "10:♠\n", "J:♠\n", "Q:♦\n", "K:♣\n" ] } ], "source": [ "c3 = Card(suit=0, rank=4)\n", "def hand(name):\n", " \"\"\"creates a hand corresponding to the input name\n", " input: name (str) hand type\n", " output: hand[] list of 5 cards with ordering depending on hand type\"\"\"\n", " import random\n", " H=[]\n", " \n", " if name == \"straight\":\n", " rank0 = random.randint(1,9)\n", " for i in range(5):\n", " H.append(Card(suit=random.randint(1,4), rank=rank0+i)) \n", " print(H[i]) \n", " return H \n", " \n", "\n", "a = hand(\"straight\")\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Deck of Cards" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "classes can use other include other classes as data objects—here's a deck of cards. Note that we are using the python random module here." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import random\n", "\n", "class Deck(object):\n", " \"\"\" the deck is a collection of cards \"\"\"\n", "\n", " def __init__(self):\n", "\n", " self.nsuits = 4\n", " self.nranks = 13\n", " self.minrank = 2\n", " self.maxrank = self.minrank + self.nranks - 1\n", "\n", " self.cards = []\n", "\n", " for rank in range(self.minrank,self.maxrank+1):\n", " for suit in range(1, self.nsuits+1):\n", " self.cards.append(Card(rank=rank, suit=suit))\n", "\n", " def shuffle(self):\n", " random.shuffle(self.cards)\n", "\n", " def get_cards(self, num=1):\n", " hand = []\n", " for n in range(num):\n", " hand.append(self.cards.pop())\n", "\n", " return hand\n", " \n", " def __str__(self):\n", " string = \"\"\n", " for c in self.cards:\n", " string += str(c) + \" \"\n", " return string" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "let's create a deck, shuffle, and deal a hand (for a poker game)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mydeck = Deck()\n", "print(mydeck)\n", "print(len(mydeck.cards))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "notice that there is no error handling in this class. The get_cards() will deal cards from the deck, removing them in the process. Eventually we'll run out of cards." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mydeck.shuffle()\n", "\n", "hand = mydeck.get_cards(5)\n", "for c in sorted(hand): print(c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Operators" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can define operations like `+` and `-` that work on our objects. Here's a simple example of currency—we keep track of the country and the amount" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Currency(object):\n", " \"\"\" a simple class to hold foreign currency \"\"\"\n", " \n", " def __init__(self, amount, country=\"US\"):\n", " self.amount = amount\n", " self.country = country\n", " \n", " def __add__(self, other):\n", " \n", " return Currency(self.amount + other.amount, country=self.country)\n", " \n", " def __str__(self):\n", " return \"{} {}\".format(self.amount, self.country)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now create some monetary amounts for different countries" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d1 = Currency(10, \"US\")\n", "d2 = Currency(15, \"Euro\")\n", "print(d1 + d2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Quick Exercise:

\n", "\n", "As written, our Currency class has a bug—it does not check whether the amounts are in the same country before adding. Modify the `__add__` method to first check if the countries are the same. If they are, return the new `Currency` object with the sum, otherwise, return `None`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "## Vectors Example\n", "\n", "Here we write a class to represent 2-d vectors. Vectors have a direction and a magnitude. We can represent them as a pair of numbers, representing the x and y lengths. We'll use a tuple internally for this\n", "\n", "We want our class to do all the basic operations we do with vectors: add them, multiply by a scalar, cross product, dot product, return the magnitude, etc.\n", "\n", "We'll use the math module to provide some basic functions we might need (like sqrt)\n", "\n", "This example will show us how to overload the standard operations in python. Here's a list of the builtin methods:\n", "\n", "https://docs.python.org/3/reference/datamodel.html\n", "\n", "To make it really clear what's being called when, I've added prints in each of the functions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import math" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class Vector(object):\n", " \"\"\" a general two-dimensional vector.\n", " Notice the print() statements are for teaching\n", " purpose only.\"\"\"\n", " \n", " def __init__(self, x, y):\n", " print(\"in __init__\")\n", " self.x = x\n", " self.y = y\n", " \n", " def __str__(self):\n", " print(\"in __str__\") \n", " return \"({} î + {} ĵ)\".format(self.x, self.y)\n", " \n", " def __repr__(self):\n", " print(\"in __repr__\") \n", " return \"Vector({}, {})\".format(self.x, self.y)\n", "\n", " def __add__(self, other):\n", " print(\"in __add__\") \n", " if isinstance(other, Vector):\n", " return Vector(self.x + other.x, self.y + other.y)\n", " else:\n", " # it doesn't make sense to add anything but two vectors\n", " print(\"we don't know how to add a {} to a Vector\".format(type(other)))\n", " raise NotImplementedError\n", "\n", " def __sub__(self, other):\n", " print(\"in __sub__\") \n", " if isinstance(other, Vector):\n", " return Vector(self.x - other.x, self.y - other.y)\n", " else:\n", " # it doesn't make sense to add anything but two vectors\n", " print(\"we don't know how to add a {} to a Vector\".format(type(other)))\n", " raise NotImplementedError\n", "\n", " def __mul__(self, other):\n", " print(\"in __mul__\") \n", " if isinstance(other, int) or isinstance(other, float):\n", " # scalar multiplication changes the magnitude\n", " return Vector(other*self.x, other*self.y)\n", " else:\n", " print(\"we don't know how to multiply two Vectors\")\n", " raise NotImplementedError\n", "\n", " def __matmul__(self, other):\n", " print(\"in __matmul__\")\n", " # a dot product\n", " if isinstance(other, Vector):\n", " return self.x*other.x + self.y*other.y\n", " else:\n", " print(\"matrix multiplication not defined\")\n", " raise NotImplementedError\n", "\n", " def __rmul__(self, other):\n", " print(\"in __rmul__\") \n", " return self.__mul__(other)\n", "\n", " def __truediv__(self, other):\n", " print(\"in __truediv__\") \n", " # we only know how to multiply by a scalar\n", " if isinstance(other, int) or isinstance(other, float):\n", " return Vector(self.x/other, self.y/other)\n", "\n", " def __abs__(self):\n", " print(\"in __abs__\") \n", " return math.sqrt(self.x**2 + self.y**2)\n", "\n", " def __neg__(self):\n", " print(\"in __neg__\") \n", " return Vector(-self.x, -self.y)\n", "\n", " def cross(self, other):\n", " # a vector cross product -- we return the magnitude, since it will\n", " # be in the z-direction, but we are only 2-d \n", " return abs(self.x*other.y - self.y*other.x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is a basic class that provides two methods `__str__` and `__repr__` to show a representation of it. There was some discussion of this on slack. These two functions provide a readable version of our object.\n", "\n", "The convection is what `__str__` is human readable while `__repr__` should be a form that can be used to recreate the object (e.g., via `eval()`). See:\n", "\n", "http://stackoverflow.com/questions/1436703/difference-between-str-and-repr-in-python" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "v = Vector(1,2)\n", "v" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Vectors have a length, and we'll use the `abs()` builtin to provide the magnitude. For a vector:\n", "$$\n", "\\vec{v} = \\alpha \\hat{i} + \\beta \\hat{j}\n", "$$\n", "we have\n", "$$\n", "|\\vec{v}| = \\sqrt{\\alpha^2 + \\beta^2}\n", "$$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "abs(v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's look at mathematical operations on vectors now. We want to be able to add and subtract two vectors as well as multiply and divide by a scalar." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u = Vector(3,5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "w = u + v\n", "print(w)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u - v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It doesn't make sense to add a scalar to a vector, so we didn't implement this -- what happens?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u + 2.0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now multiplication. It makes sense to multiply by a scalar, but there are multiple ways to define multiplication of two vectors. \n", "\n", "Note that python provides both a `__mul__` and a `__rmul__` function to define what happens when we multiply a vector by a quantity and what happens when we multiply something else by a vector." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u*2.0" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "2.0*u" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and division: `__truediv__` is the python 3 way of division `/`, while `__floordiv__` is the old python 2 way, also enabled via `//`.\n", "\n", "Dividing a scalar by a vector doesn't make sense:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u/5.0" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "5.0/u" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Python 3.5 introduced a new matrix multiplication operator, `@` -- we'll use this to implement a dot product between two vectors:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u @ v" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For a cross product, we don't have an obvious operator, so we'll use a function. For 2-d vectors, this will result in a scalar" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u.cross(v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, negation is a separate operation:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "-u" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "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.7.3" } }, "nbformat": 4, "nbformat_minor": 1 }