Object-oriented programming

There are many different ways to program in Python. In this class we will be (mostly) programing in what is called the Procedural style. However, at its heart, Python is an Objected-Oriented programming language. The Objected-Oriented style of programming has many advantages, but is less straightforward than Procedural programming

As you progress in your programming life, there will come a time when you find that moving to the Objected-Oriented paradigm will make your life easier.

I would like to say I created this introduction to aid in your exploration of Objected-Oriented Python, but I did not. I just shamelessly stole and modified Brett Morris' awesome work.

Example - Asteroids

In our introduction to Python, we used a dataset of main belt asteroids.

This dataset contained the following data for the asteroids:

  • Names
  • Absolute Magnitudes (H)
  • Albedo (A)

Let us work with this data from an Objected-Oriented point of view.

Defining a new object

To create a new object, you use the class command, rather than the def command that you would use for functions,

class SpaceRock(object):

We've named the new object SpaceRock - object names in python should be uppercase without underscores separating words (whereas functions are usually all lowercase and words are separated by underscores).

The __init__ method

Now we will define how you call the SpaceRock constructor (the call that creates new SpaceRock objects). Let's say you want to be able to create a asteroid like this...

new_asteroid = SpaceRock(name=my_name, ab_mag=my_ab_mag, albedo=my_albedo)

All Python objects get initialized with a function called __init__ defined within the class, like this:

class SpaceRock(object):
    def __init__(self, name=None, ab_mag=None, albedo=None):

You define the __init__ function like all other functions, except that the first argument is always called self. This self is the shorthand variable that you use to refer to the SpaceRock object within the __init__ method.

Attributes

Objects have attributes, which are like variables stored on an object. We'll want to store the values above into the SpaceRock object, each with their own attribute, like this:

class SpaceRock(object):
    def __init__(self, name=None, ab_mag=None, albedo=None):
        self.name = name
        self.ab_mag = ab_mag
        self.albedo = albedo

Each attribute is defined by setting self.<attribute name> = <value>. All attributes should be defined within the __init__ method.

A Working Example

Let's now create an instance of the SpaceRock object, and see how it works:


In [ ]:
import numpy as np
from astropy import units as u

In [ ]:
class SpaceRock(object):
    
    def __init__(self, name=None, ab_mag=None, albedo=None):
        self.name = name
        self.ab_mag = ab_mag
        self.albedo = albedo

In [ ]:
# Create some fake data:

my_name = "Geralt of Rivia"
my_ab_mag = 5.13
my_albedo = 0.131

# Initialize a SpaceRock object:

new_asteroid = SpaceRock(name=my_name, ab_mag=my_ab_mag, albedo=my_albedo)

We can see what values are stored in each attribute like this:


In [ ]:
new_asteroid.name

In [ ]:
new_asteroid.albedo

Methods

So far this just looks like another way to store your data. It becomes more powerful when you write methods for your object.

Methods can be thought of as functions associated with an object.

You can now access the attributes of the object within methods by calling self.<attribute name>.

Let's make a simple method for the SpaceRock object, which determines the size of the asteroid.


In [ ]:
class SpaceRock(object):
    
    def __init__(self, name=None, ab_mag=None, albedo=None):
        self.name = name
        self.ab_mag = ab_mag
        self.albedo = albedo
        
    def diameter(self):
        result = (1329.0 / np.sqrt(self.albedo)) * (10 ** (-0.2 * self.ab_mag))
        return result * u.km

To use a method you need to add () to the end of the method


In [ ]:
new_asteroid = SpaceRock(name=my_name, ab_mag=my_ab_mag, albedo=my_albedo)

new_asteroid.diameter()

Real data

Lets use some real data. A short version of the MainBelt.csv dataset from last week


In [ ]:
from astropy.table import QTable

In [ ]:
rock_table = QTable.read('MainBelt_small.csv', format='ascii.csv')

In [ ]:
print(rock_table)

In [ ]:
my_name = rock_table['Name']
my_ab_mag = rock_table['H']
my_albedo = rock_table['A']

rocks = SpaceRock(name=my_name, ab_mag=my_ab_mag, albedo=my_albedo)

In [ ]:
rocks.diameter()

One of the nice things about creating a Class is that all of the methods within the Class know about each other.

For example: I want to create a new method that uses the results of the diameter method I already defined.

Easy, just use the variable self.diameter() in my new method


In [ ]:
class SpaceRock(object):
    
    def __init__(self, name=None, ab_mag=None, albedo=None):
        self.name = name
        self.ab_mag = ab_mag
        self.albedo = albedo
        
    def diameter(self):
        result = (1329.0 / np.sqrt(self.albedo)) * (10 ** (-0.2 * self.ab_mag))
        return result * u.km
    
    def two_diameter(self):
        result = 2.0 * self.diameter()
        return result

In [ ]:
rocks = SpaceRock(name=my_name, ab_mag=my_ab_mag, albedo=my_albedo)

In [ ]:
rocks.diameter()

In [ ]:
rocks.two_diameter()

As you modify your Class all of the methods within the Class know about the modifications.

For example: Let us add some more attributes to our Asteroid data.


In [ ]:
class SpaceRock(object):
    
    def __init__(self, name = None, ab_mag = None, albedo = None, 
                 semi_major= None, ecc = None):
        
        self.name = name
        self.ab_mag = ab_mag
        self.albedo = albedo
        self.semi_major = semi_major
        self.ecc = ecc
        
    def diameter(self):
        result = (1329.0 / np.sqrt(self.albedo)) * (10 ** (-0.2 * self.ab_mag))
        return result * u.km
    
    def two_diameter(self):
        result = 2.0 * self.diameter()
        return result
    
    def find_perihelion(self):
        result = self.semi_major * ( 1.0 - self.ecc )
        return result * u.AU

In [ ]:
my_name = rock_table['Name']
my_ab_mag = rock_table['H']
my_albedo = rock_table['A']
my_semi_major = rock_table['a']
my_ecc = rock_table['ecc']

more_rocks = (SpaceRock(name=my_name, ecc = my_ecc, semi_major=my_semi_major, 
                        ab_mag=my_ab_mag, albedo=my_albedo))

In [ ]:
more_rocks.diameter()

In [ ]:
more_rocks.find_perihelion()

In [ ]:
for idx,value in enumerate(more_rocks.find_perihelion()):
    
    rock_name = more_rocks.name[idx]
    
    my_string = "The Asteroid {0} has a perihelion distance of {1:.2f}".format(rock_name, value)
    
    print(my_string)

Documentation

If you want to share your code with collaborators or with your future self, you should include documentation. We've neglected that above, so let's add in some docstrings!


In [ ]:
class SpaceRock(object):
    
    """Container for Asteroids"""
    
    def __init__(self, name = None, ab_mag = None, albedo = None, 
                 semi_major= None, ecc = None):
        
        """
        Parameters
        ----------
        name : string
            Name of the target
        ab_mag : array-like
            Absolute Magnitude of each Asteroid
        albedo : array-like
            Albedo of each Asteroid
        semi_major : array-like
            Semi Major Axis of each Asteroid in AU
        ecc : array-like
            Eccentricity of each Asteroid
        """
        
        self.name = name
        self.ab_mag = ab_mag
        self.albedo = albedo
        self.semi_major = semi_major
        self.ecc = ecc
        
    def diameter(self):
        """
        Determine the diameter (in km) of the Asteroids
        """
        result = (1329.0 / np.sqrt(self.albedo)) * (10 ** (-0.2 * self.ab_mag))
        return result * u.km
    
    def two_diameter(self):
        """
        Determine twice the diameter (in km) of the Asteroids
        """
        result = 2.0 * self.diameter()
        return result
    
    def find_perihelion(self):
        """
        Determine the perihelion distance of the Asteroids in AU
        """
        result = self.semi_major * ( 1.0 - self.ecc )
        return result * u.AU

In [ ]:
rocks = (SpaceRock(name=my_name, ecc = my_ecc, semi_major=my_semi_major, 
                   ab_mag=my_ab_mag, albedo=my_albedo))

Now you can see the documentation on the module within the Notebooks by typing

rocks?

...you can see the documentation for each method by typing

rocks.diameter?

In [ ]:
rocks?

In [ ]:
rocks.diameter?