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.
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:
import copy
class Bottle(object):
def __init__(self, size, content_amount):
self.size = size
if content_amount<size:
self.content_amount = content_amount
else:
raise ValueError("This bottle doesn't fit so much liquid")
def pour(self, amount):
if amount < self.content_amount:
self.content_amount -= amount
return amount
else:
amount_pourable = self.content_amount
self.content_amount = 0.
return amount_pourable
bottle = Bottle(size=1.0, content_amount=0.9)
cup = bottle.pour(0.5)
cup2 = bottle.pour(0.6)
print(cup, cup2)
0.5 0.4
AS you can see, Object Oriented Programming is a nice paradigm to use when you want to combine a state and actions on that state at a fundamental level. We could have written the same code in a way we're more familiar with, but it ends up being more cumbersome (and implementing the ValueError for that the bottle doesnt fit so much liquid seems very unnatural, with the check coming in at a weird place):
import copy
def pour_bottle(of_capacity, with_content_amount, amount):
if with_content_amount > of_capacity:
raise ValueError("This bottle doesn't fit so much liquid")
if amount < with_content_amount:
with_content_amount -= amount
return (with_content_amount, amount)
else:
return(0, with_content_amount)
size = 1.0
content_amount = 0.9
content_amount,cup = pour_bottle(of_capacity=size,
with_content_amount=content_amount,
amount =0.5)
content_amount,cup2 = pour_bottle(of_capacity=size,
with_content_amount=content_amount,
amount =0.5)
print(cup, cup2)
0.5 0.4
The syntax for creating a class is as follows (we will explain what a superclass is later):
class ClassName(SuperClass):
def __init__(self, parameters):
Do something
def method_name(self, parameters):
Do something
etc....
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.
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).
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.
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.
# coding: utf-8
from __future__ import division, print_function
from vpython import *
import numpy as np
import copy
class PhysicsError (Exception):
"""
Error type defined for if two Particles get too close to simulate well
"""
def __init__(self, exception_type):
self.exception_type = exception_type
def __str__(self):
return repr(self.exception_type)
class Particle(sphere):
"""
Class which describes a Particle under the influence of some force. Subclasses vpython sphere so drawing is no effort.
"""
G = 1
def __init__(self,pos = vector(0,0,0), velocity = vector(0,0,0), mass = 0.0, radius =0.0, color = color.red):
"""
Parameters
----------
pos : vpython vector
Initial position of Particle
velocity : vpython vector
Initial velocity of Particle
mass: float
Mass of Particle (default = 0)
radius : float
Radius of Particle
color: vpython color
Color of particle
"""
sphere.__init__(self,pos = pos, velocity = velocity, radius = radius, make_trail = True, color = color)
self.velocity = velocity
self.mass = mass
def force_felt_by(self,other,if_at = None):
'''
Parameters
----------
other: Particle
The particle which feels the force
if_at: vpython vector
If this parameter is used, the function gives the force the 'other' particle would feel if it were at this position
Subclass Particle and change this to implement custom forces, then everything else should work.
Default(The one implemented here) is gravitational
'''
if not if_at:
if_at = other.pos
position_difference = if_at - self.pos
determinant = position_difference.mag
if determinant == 0:
return vector(0,0,0)
velocity_determinant = self.velocity.mag
g_force_scalar = (-1*self.G*self.mass*other.mass)/(determinant**3)
g_force_vector = g_force_scalar * position_difference
if determinant < self.radius + other.radius:
raise PhysicsError("Collision ")
return g_force_vector
def increment_by(self,pos_increment, velocity_increment):
'''
Function to increment coordinates and velocity at the same time.
Parameters
----------
pos_increment: vpython vector
Increment for pos
velocity_increment: vpython vector
Increment for velocity
'''
self.pos += pos_increment
self.velocity += velocity_increment
class System (object):
"""Class which describes a system composed of a number of Particles."""
def __init__(self,dt,G = 1):
"""
Parameters
----------
dt: float
Specifies time increments to take
G: float
Gravitational constant, set to 1 by default
"""
planets = []
self.dt = dt
self.G = G
def runge_kutta_move_time(self):
"""Move time forwards by one step using RK4. Assumes gravitational field doesn't change significantly with time during one time step."""
old_planets = self.planets
try:
for counter_1,planet_1 in enumerate(self.planets):
k1 = vector(0,0,0)
k2 = vector(0,0,0)
k3 = vector(0,0,0)
k4 = vector(0,0,0)
for counter_2,planet_2 in enumerate(old_planets):
if counter_2 != counter_1:
k1 += planet_2.force_felt_by(planet_1, if_at = None)/planet_1.mass
imagpos = planet_1.pos + (self.dt/2)*k1
for counter_2,planet_2 in enumerate(old_planets):
if counter_2 != counter_1:
k2 += planet_2.force_felt_by(planet_1, if_at = imagpos)/planet_1.mass
imagpos = planet_1.pos + (self.dt/2)*k2
for counter_2,planet_2 in enumerate(old_planets):
if counter_2 != counter_1:
k3 += planet_2.force_felt_by(planet_1, if_at = imagpos)/planet_1.mass
imagpos = planet_1.pos + (self.dt)*k3
for counter_2,planet_2 in enumerate(old_planets):
if counter_2 != counter_1:
k3 += planet_2.force_felt_by(planet_1, if_at = imagpos)/planet_1.mass
x_increment = planet_1.velocity*self.dt
v_increment = (self.dt/6)*(k1 + 2*k2 + 2*k3 + k4)
self.planets[counter_1].increment_by(x_increment,v_increment)
except PhysicsError:
print("Collision")
scene1 = canvas(title = "Orbits!")
scene1.caption = """Right button drag or Ctrl-drag to rotate "camera" to view scene.
To zoom, drag with middle button or Alt/Option depressed, or use scroll wheel.
On a two-button mouse, middle is left + right.
Touch screen: pinch/extend to zoom, swipe or two-finger rotate."""
scene1.forward = vector(0,0,1)
# Initialise 3 planets
giant_planet = Particle(pos = vector(-10.,0.,0.),
velocity = vector(0., 0., 0.),
mass = 200, radius = 5, color = color.blue)
dwarf_planet = Particle(pos = vector(15.,0.,0.),
velocity = vector(0., 0., 3.25),
mass = 10, radius = 5, color = color.green)
really_big_planet = Particle(pos = vector(-100,-200,0),
velocity = vector(3, 0, 0),
mass = 2000, radius = 20)
dt = 0.1
system = System(dt)
planets_array = [giant_planet, dwarf_planet, really_big_planet]
system.planets = planets_array
# Step the system's time forwards 50 times a second.
while True:
rate(50)
system.runge_kutta_move_time()
--------------------------------------------------------------------------- KeyboardInterrupt Traceback (most recent call last) <ipython-input-1-6f9e44700268> in <module>() 143 # Step the system's time forwards 50 times a second. 144 while True: --> 145 rate(50) 146 system.runge_kutta_move_time() /Applications/anaconda/lib/python3.5/site-packages/vpython/vpython.py in __call__(self, maxRate) 132 if (self.rval != maxRate) and (maxRate >= 1.0): 133 self.rval = maxRate --> 134 super(RateKeeper2, self).__call__(maxRate) 135 136 if sys.version > '3': /Applications/anaconda/lib/python3.5/site-packages/vpython/rate_control.py in __call__(self, maxRate) 204 dt = self.lastSleep + self.calls*(self.userTime + self.callTime + self.delay) + \ 205 renders*self.renderTime + sleeps*self.interactionPeriod - _clock() --> 206 _sleep(dt) 207 self.lastSleep = _clock() 208 self.calls = 0 /Applications/anaconda/lib/python3.5/site-packages/vpython/rate_control.py in _sleep(dt) 47 dtsleep = nticks*_tick 48 t = _clock() ---> 49 time.sleep(dtsleep) 50 t = _clock()-t 51 dt -= t KeyboardInterrupt:
When designing classes, the SOLID principles are good ones to follow, so that you don't end up with useless/ very complicated classes:
If you are interested in other design principles/patterns, there are additional, optional notebooks on them.
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.