Basics of object-oriented programming
Object-oriented programming is a way of programming in which you bundle related variables and functions into objects, and then manipulate and use these objects as a whole. We already saw many examples of objects on Python, e.g. lists, dictionaries, numpy arrays, that have both variables and methods inside them. In this section we will learn how to create our own objects.
You can think of:
- variables as properties / attributes of an object
- functions as some operations / methods you can perform on this object
Let’s define out first class:
class Planet:
# internally we store all numbers in cgs units
hostObject = "Sun" # class attribute (the same value for every class instance)
def __init__(self, radius, mass): # "constructor" sets the initial state of a newly created object
self.radius = radius*1e5 # instance attribute, convert km -> cm
self.mass = mass*1.e3 # instance attribute, convert kg->g
Let’s define some instances of this class:
mercury = Planet(radius=2439.7, mass=3.285e23) # enter km and kg
venus = Planet(6051.8, 4.867e24) # enter km and kg
venus.radius, venus.mass, venus.hostObject
Instances are guaranteed to have the attributes that we expect.
Question 23
How can we define an instance without passing the values? E.g., I would like to say earth = Planet()
and then pass the
attribute values separately like this:
earth = Planet()
earth.radius = 6371 # these are dynamic variables that we can redefine
earth.mass = 5.972e24
Let’s add inside our class an instance method (with proper indentation):
def density(self): # it acts on the class instance
return self.mass/(4./3.*math.pi*self.radius**3) # [g/cm^3]
Redefine the class, and now:
earth = Planet()
earth.radius = 6371*1e5 # here we need to convert manually
earth.mass = 5.972e24*1e3
import math
earth.density() # 5.51 g/cm^3
Let’s add another method (remember the indentation!):
def g(self): # free fall acceleration
return 6.67259e-8*self.mass/self.radius**2 # G in [cm^3/g/s^2]
and now
earth = Planet(6371,5.972e24)
mars = Planet(3389.5,6.39e23)
earth.g() # 981.7 cm/s^2
mars.g() / earth.g() # 0.378
Let’s add another method (remember the indentation!):
def describe(self):
print('density =', self.density(), 'g/cm^3')
print('free fall =', self.g(), 'cm/s^2')
Redefine the class, and now:
jupyter = Planet(radius=69911, mass=1.898e27)
jupyter.describe() # should print 1.32 g/cm^3 and 2591 cm/s^2
print(jupyter) # says it is an object at this memory location (not very descriptive)
Let’s add our last method (remember the indentation!):
def __str__(self): # special method to redefine the output of print(self)
return f"My radius is {self.radius/1e5}km and my mass is {self.mass/1e3}kg"
Redefine the class, and now:
jupyter = Planet(radius=69911, mass=1.898e27)
print(jupyter) # prints the full sentence
Important: As with any complex object in Python, assigning an instance to a new variable will simply create a pointer, i.e. if you modify one in place, you’ll see the change through the other one too:
new = jupyter
jupyter.mass = -1
new.mass # also -1
If you want a separate copy:
import copy
new = copy.deepcopy(jupyter)
jupyter.mass = -2
new.mass # still -1
Inherit from parent classes
Let’s create a child class Moon
that would inherit the attributes and methods of Planet
class:
class Moon(Planet): # it inherits all the attributes and methods of the parent process
pass
phobos = Moon(radius=22.2, mass=1.08e16)
deimos = Moon(radius=12.6, mass=2.0e15)
phobos.g() / earth.g() # 0.0001489
isinstance(phobos, Moon) # True
isinstance(phobos, Planet) # True - all objects of a child class are instances of the parent class
isinstance(jupyter, Planet) # True
isinstance(jupyter, Moon) # False
issubclass(Moon,Planet) # True
Child classes can have their own attributes and methods that are distinct from (i.e. override) the parent class:
class Moon(Planet):
hostObject = 'Mars'
def g(self):
return 'too small to compute accurately'
phobos = Moon(radius=22.2, mass=1.08e16)
deimos = Moon(radius=12.6, mass=2.0e15)
mars = Planet(3389.5,6.39e23)
phobos.hostObject, mars.hostObject # ('Mars', 'Sun')
phobos.g(), mars.g() # ('too small to compute accurately', 371.1282569773226)
One thing to keep in mind about class inheritance is that changes to the parent class automatically propagate to child classes (when you follow the sequence of definitions), unless overridden in the child class:
class Parent:
...
def __str__(self):
return "Changed in the parent class"
...
class Moon(Planet):
hostObject = 'Mars'
def g(self):
return 'too small to compute accurately'
deimos = Moon(radius=12.6, mass=2.0e15)
print(deimos) # prints "Changed in the parent class"
You can access the parent class namespace from inside a method of a child class by using super():
class Moon(Planet):
hostObject = 'Mars'
def parentHost(self):
return super().hostObject # will return hostObject of the parent class
deimos = Moon(radius=12.6, mass=2.0e15)
deimos.hostObject, deimos.parentHost() # ('Mars', 'Sun')
Generators
We already saw that in Python you can loop over a collection using for
:
for i in 'weather':
print(i)
for j in [5,6,7]:
print(j)
Behind the scenes Python creates an iterator out of a collection. This iterator has a __next__()
method, i.e. it does
something like:
a = iter('weather')
a.__next__() # 'w'
a.__next__() # 'e'
a.__next__() # 'a'
You can build your own iterator as if you were defining a function. Such function is called a generator in Python:
def cycle():
yield 1
yield 'hello'
yield [1,2,3]
[i for i in cycle()] # [1, 'hello', [1, 2, 3]]
def square(x): # `x` is an input string in this generator
for letter in x:
yield int(letter)**2 # yields a sequence of numbers that you can cycle through
[i for i in square('12345')] # [1, 4, 9, 16, 25]
a = square('12345')
[a.__next__() for i in range(3)] # [1, 4, 9]
Programming Style and Wrap-Up
- comment your code as much as possible
- use meaningful variable names
- very good idea to break complex programs into blocks using functions
- change one thing at a time, then test
- use revision control
- use docstrings to provide online help
def average(values):
"Return average of values, or None if no values are supplied."
if len(values) > 0:
return(sum(values)/len(values))
print(average([1,2,3,4]))
print(average([]))
help(average)
def moreComplexFunction(values):
"""This string spans
multiple lines.
Blank lines are allowed."""
help(moreComplexFunction)
- very good idea to add assertions to your code to check things
assert n > 0., 'Data should only contain positive values'
is the same as
import sys
if n <= 0.:
print('Data should only contain positive values')
sys.exit(1)
- Python 3 documentation https://docs.python.org/3
- Matplotlib gallery http://matplotlib.org/gallery.html
- NumPy is a scientific computing package http://www.numpy.org
- SciPy is a rich collection of scientific utilities http://www.scipy.org/scipylib
- Python Data Analysis Library http://pandas.pydata.org