{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 3. Introduction to Object-Oriented Programming\n", "\n", "Object Oriented Programming is a way of programming (a programming paradigm), based on the concept of \"objects\". An object is what you expect an object to be. They have methods, as we learnt last year. In addition to methods, objects have properties. Properties are just variables that are associated with an object and are accesed with a similar syntax to methods.\n", "\n", "When we think about a real object like a water bottle, we don't think about all of its properties and methods as an individual. Most water bottles can do more or less the same actions, so we can define a Class called Bottle which defines all the properties every water bottle will have. See the mock class, Bottle, below:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.5 0.4\n" ] } ], "source": [ "import copy\n", "\n", "class Bottle(object):\n", " def __init__(self, size, content_amount):\n", " self.size = size\n", " if content_amount of_capacity:\n", " raise ValueError(\"This bottle doesn't fit so much liquid\")\n", " if amount < with_content_amount:\n", " with_content_amount -= amount\n", " return (with_content_amount, amount)\n", " else:\n", " return(0, with_content_amount)\n", "\n", "size = 1.0\n", "content_amount = 0.9\n", "content_amount,cup = pour_bottle(of_capacity=size,\n", " with_content_amount=content_amount,\n", " amount =0.5)\n", "content_amount,cup2 = pour_bottle(of_capacity=size,\n", " with_content_amount=content_amount,\n", " amount =0.5)\n", "print(cup, cup2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The syntax for creating a class is as follows (we will explain what a superclass is later):\n", "\n", " class ClassName(SuperClass):\n", " \n", " def __init__(self, parameters):\n", " Do something\n", " \n", " def method_name(self, parameters):\n", " Do something\n", " \n", " etc....\n", " \n", "The first parameter for any method in a class is self. Self refers to the object that any parameters are being accessed/modified from, and if you ever do object.method(), it is passed as the first argument.\n", "\n", "The \\_\\_init()\\_\\_ function is there to initialise the class. it is what's called if you ever do ClassName(arguments). An example of an initialiser you'll already have encountered is numpy.array(some_list).\n", "\n", "A great thing about Object Oriented Programming is that it allows for you to write code via subclassing. Let's imagine the case of a Person class, with methods to walk, eat and talk, and has a property called personality. Consider the case that we wanted to make a Student class. A Student would be able to do all the things a Person can do, but also has properties of studiousness, intuition, and methods to take exams and go out. It would be a waste to rewrite all the things that are already there in the Person class, so instead, one could subclass Person to create Student. (In this case, Person would be the superclass of Student) This is done in Python by putting the superclass in brackets after the declaration of the class, as shown above. All classes inherit from the base class that is object.\n", "\n", "Let's look at a more Physics-like example, where we will subclass the sphere class from vpython, and use it to create a simulation for orbits. The documentation included in the source code should suffice. Similar simulations can be made using the pycav.mechanics module, which uses Velocity Verlet algorithms instead of RK4 for more physical results. Play around with the below code." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [ { "data": { "application/javascript": [ "require.undef(\"nbextensions/jquery-ui.custom.min\");" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "require.undef(\"nbextensions/glow.2.1.min\");" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "require.undef(\"nbextensions/glowcomm\");" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "require.undef(\"nbextensions/pako.min\");" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "require.undef(\"nbextensions/pako_deflate.min\");" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "require.undef(\"nbextensions/pako_inflate.min\");" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "require([\"nbextensions/glowcomm\"], function(){console.log(\"glowcomm loaded\");})" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "window.__context = { glowscript_container: $(\"#glowscript\").removeAttr(\"id\")}" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "window.__context = { glowscript_container: $(\"#glowscript\").removeAttr(\"id\")}" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "ename": "KeyboardInterrupt", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyboardInterrupt\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 143\u001b[0m \u001b[0;31m# Step the system's time forwards 50 times a second.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 144\u001b[0m \u001b[0;32mwhile\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 145\u001b[0;31m \u001b[0mrate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m50\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 146\u001b[0m \u001b[0msystem\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrunge_kutta_move_time\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Applications/anaconda/lib/python3.5/site-packages/vpython/vpython.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, maxRate)\u001b[0m\n\u001b[1;32m 132\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrval\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mmaxRate\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mmaxRate\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0;36m1.0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 133\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrval\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmaxRate\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 134\u001b[0;31m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mRateKeeper2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmaxRate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 135\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 136\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0msys\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mversion\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;34m'3'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Applications/anaconda/lib/python3.5/site-packages/vpython/rate_control.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, maxRate)\u001b[0m\n\u001b[1;32m 204\u001b[0m \u001b[0mdt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlastSleep\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcalls\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muserTime\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallTime\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdelay\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 205\u001b[0m \u001b[0mrenders\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrenderTime\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msleeps\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minteractionPeriod\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0m_clock\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 206\u001b[0;31m \u001b[0m_sleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 207\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlastSleep\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_clock\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 208\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcalls\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Applications/anaconda/lib/python3.5/site-packages/vpython/rate_control.py\u001b[0m in \u001b[0;36m_sleep\u001b[0;34m(dt)\u001b[0m\n\u001b[1;32m 47\u001b[0m \u001b[0mdtsleep\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnticks\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0m_tick\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 48\u001b[0m \u001b[0mt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_clock\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 49\u001b[0;31m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdtsleep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 50\u001b[0m \u001b[0mt\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_clock\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0mt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 51\u001b[0m \u001b[0mdt\u001b[0m \u001b[0;34m-=\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mKeyboardInterrupt\u001b[0m: " ] } ], "source": [ "# coding: utf-8\n", "from __future__ import division, print_function\n", "from vpython import *\n", "import numpy as np\n", "import copy\n", "\n", "class PhysicsError (Exception):\n", " \"\"\"\n", " Error type defined for if two Particles get too close to simulate well\n", " \"\"\"\n", " def __init__(self, exception_type):\n", " self.exception_type = exception_type\n", " def __str__(self):\n", " return repr(self.exception_type)\n", "\n", "class Particle(sphere):\n", " \"\"\"\n", " Class which describes a Particle under the influence of some force. Subclasses vpython sphere so drawing is no effort.\n", " \"\"\"\n", "\n", " G = 1\n", " def __init__(self,pos = vector(0,0,0), velocity = vector(0,0,0), mass = 0.0, radius =0.0, color = color.red):\n", " \"\"\"\n", " Parameters\n", " ----------\n", " pos : vpython vector\n", " Initial position of Particle\n", " velocity : vpython vector\n", " Initial velocity of Particle\n", " mass: float\n", " Mass of Particle (default = 0)\n", " radius : float\n", " Radius of Particle\n", " color: vpython color\n", " Color of particle\n", " \"\"\"\n", " sphere.__init__(self,pos = pos, velocity = velocity, radius = radius, make_trail = True, color = color)\n", " self.velocity = velocity\n", " self.mass = mass\n", " def force_felt_by(self,other,if_at = None):\n", " '''\n", " Parameters\n", " ----------\n", " other: Particle\n", " The particle which feels the force\n", " if_at: vpython vector\n", " If this parameter is used, the function gives the force the 'other' particle would feel if it were at this position\n", "\n", " Subclass Particle and change this to implement custom forces, then everything else should work.\n", " Default(The one implemented here) is gravitational\n", " '''\n", " if not if_at:\n", " if_at = other.pos\n", " position_difference = if_at - self.pos\n", " determinant = position_difference.mag\n", " if determinant == 0:\n", " return vector(0,0,0)\n", " velocity_determinant = self.velocity.mag\n", " g_force_scalar = (-1*self.G*self.mass*other.mass)/(determinant**3)\n", " g_force_vector = g_force_scalar * position_difference\n", " if determinant < self.radius + other.radius:\n", " raise PhysicsError(\"Collision \")\n", " return g_force_vector\n", "\n", " def increment_by(self,pos_increment, velocity_increment):\n", " '''\n", " Function to increment coordinates and velocity at the same time.\n", " Parameters\n", " ----------\n", " pos_increment: vpython vector\n", " Increment for pos\n", " velocity_increment: vpython vector\n", " Increment for velocity\n", " '''\n", " self.pos += pos_increment\n", " self.velocity += velocity_increment\n", "\n", "class System (object):\n", " \"\"\"Class which describes a system composed of a number of Particles.\"\"\"\n", " def __init__(self,dt,G = 1):\n", " \"\"\"\n", " Parameters\n", " ----------\n", " dt: float\n", " Specifies time increments to take\n", " G: float\n", " Gravitational constant, set to 1 by default\n", " \"\"\"\n", " planets = []\n", " self.dt = dt\n", " self.G = G\n", "\n", " def runge_kutta_move_time(self):\n", " \"\"\"Move time forwards by one step using RK4. Assumes gravitational field doesn't change significantly with time during one time step.\"\"\"\n", " old_planets = self.planets\n", " try:\n", " for counter_1,planet_1 in enumerate(self.planets):\n", " k1 = vector(0,0,0)\n", " k2 = vector(0,0,0)\n", " k3 = vector(0,0,0)\n", " k4 = vector(0,0,0)\n", " for counter_2,planet_2 in enumerate(old_planets):\n", " if counter_2 != counter_1:\n", " k1 += planet_2.force_felt_by(planet_1, if_at = None)/planet_1.mass\n", " imagpos = planet_1.pos + (self.dt/2)*k1\n", " for counter_2,planet_2 in enumerate(old_planets):\n", " if counter_2 != counter_1:\n", " k2 += planet_2.force_felt_by(planet_1, if_at = imagpos)/planet_1.mass\n", " imagpos = planet_1.pos + (self.dt/2)*k2\n", " for counter_2,planet_2 in enumerate(old_planets):\n", " if counter_2 != counter_1:\n", " k3 += planet_2.force_felt_by(planet_1, if_at = imagpos)/planet_1.mass\n", " imagpos = planet_1.pos + (self.dt)*k3\n", " for counter_2,planet_2 in enumerate(old_planets):\n", " if counter_2 != counter_1:\n", " k3 += planet_2.force_felt_by(planet_1, if_at = imagpos)/planet_1.mass\n", " x_increment = planet_1.velocity*self.dt\n", " v_increment = (self.dt/6)*(k1 + 2*k2 + 2*k3 + k4)\n", " self.planets[counter_1].increment_by(x_increment,v_increment)\n", " except PhysicsError:\n", " print(\"Collision\")\n", " \n", "scene1 = canvas(title = \"Orbits!\")\n", "scene1.caption = \"\"\"Right button drag or Ctrl-drag to rotate \"camera\" to view scene.\n", "To zoom, drag with middle button or Alt/Option depressed, or use scroll wheel.\n", " On a two-button mouse, middle is left + right.\n", "Touch screen: pinch/extend to zoom, swipe or two-finger rotate.\"\"\"\n", "scene1.forward = vector(0,0,1)\n", "# Initialise 3 planets\n", "giant_planet = Particle(pos = vector(-10.,0.,0.),\n", " velocity = vector(0., 0., 0.), \n", " mass = 200, radius = 5, color = color.blue)\n", "dwarf_planet = Particle(pos = vector(15.,0.,0.), \n", " velocity = vector(0., 0., 3.25), \n", " mass = 10, radius = 5, color = color.green)\n", "really_big_planet = Particle(pos = vector(-100,-200,0), \n", " velocity = vector(3, 0, 0), \n", " mass = 2000, radius = 20)\n", "dt = 0.1\n", "system = System(dt)\n", "planets_array = [giant_planet, dwarf_planet, really_big_planet]\n", "system.planets = planets_array\n", "# Step the system's time forwards 50 times a second.\n", "while True:\n", " rate(50)\n", " system.runge_kutta_move_time()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When designing classes, the SOLID principles are good ones to follow, so that you don't end up with useless/ very complicated classes:\n", "\n", "* S stands for the Single Responsibility Principle:\n", " * A class should have only one responsibility, i.e. only one potential change in what you want to do should affect the design of the class\n", "* O stands for the Open/Closed Principle:\n", " * Classes should be open for extension, but closed for modification. This is hard to implement in Python, as anything can be modified, but you should try to make it clear that certain things should stay the way they are)\n", "* L stands for the Liskov Substitution Principle:\n", " * Objects should be replacable with instances of their subclasses without altering how the program works, i.e. subclasses shouldn't break the functionality of their superclasses.\n", "* I stands for the Interface Segregation Principle:\n", " * The way you use a class shouldn't depend on an interface (method or variable) that you don't use. This isn't as relevant in Python due to the way the language is designed\n", "* D stands for the Dependency Inversion Principle:\n", " * One should always depend upon abstractions, not upon concretions. i.e., the implementation of one class shouldn't depend upon the peculiarities of the implementation of another.\n", "\n", "If you are interested in other design principles/patterns, there are additional, optional notebooks on them.\n", "\n", "OOP (Object Oriented Programming) is a powerful way of thinking, but it can take a while for it to \"click\". If it doesn't make sense to you, don't worry; very advanced simulations can be written (and often are due to performance reasons) without OOP. Keep in mind, however, that it can be very helpful to know, especially as a lot of Python libraries are written in an object oriented manner." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "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.1" } }, "nbformat": 4, "nbformat_minor": 0 }