Object-oriented programming

Introduction

Object-oriented programming is a way of programming that groups together related data and functions into objects, and makes it easier to design more complex programs. In traditional (procedural) programming, you can typically think of a program as an set of instructions that is read from top to bottom, with the exception of functions, which allow you to avoid repetitive code. In object-oriented programming, one instead thinks of a program mainly as setting up a number of 'types' of objects, and doing operations on them.

Note that you have already been using objects! Every Python object is an object:

In [1]:
s = 'hello'
s.upper()
Out[1]:
'HELLO'
In [2]:
l = [4,2,1]
l.sort()
l
Out[2]:
[1, 2, 4]

In these cases, upper and sort are both methods, and s and l are instances of the str and list class respectively (we will discuss these terms below).

Classes, instances, and methods

Classes are most easily explained by example, so let's dive right in and look at a class, which is used to define an object:

In [3]:
class Person(object):
    
    def __init__(self, name):
        self.name = name
        
    def say_hello(self):
        print("Hello, my name is " + self.name)

and let's try and use it:

In [4]:
tom = Person('Tom')
tom.say_hello()
Hello, my name is Tom

There is a lot of new syntax here which you won't have likely seen before, but also some syntax which will look more familiar. First, let's have a look at the class definition. The class is defined using the following syntax:

class Person(object):
    ...

This looks similar to the definition for a function, except that it doesn't directly contain code, but it then contains a series of functions:

class Person(object):

    def __init__(self, ...):
        ...

    def say_hello(self, ...):
        ...

So in effect, a class is a collection of functions. What is special about these functions? If you look at the full class definition above, you will see that the first argument to all the functions is self, which is the object itself. Why is this useful?

At this point, we need to clarify some more terminology: an instance of a class is a particular object represented by the class - that is, while Person is the class (i.e. the general definition of the idea of a 'person'), an instance is a particular person, e.g. Tom:

In [5]:
tom = Person('Tom')

Here tom is an instance of the Person class. Now let's look at the first defined method, called __init__:

def __init__(self, name):
    self.name = name

The term method is basically used for a function that is attached to a class. The __init__ method is a method that is automatically called when creating an instance of the class (for those familiar with the terminology, it is equivalent to a constructor). What is basically happening here is that self is the actual instance that is being created, and the method then takes the name of the person and assigns it to the name attribute of the instance:

In [6]:
tom = Person('Tom')
print(tom.name)
Tom

As you can see, the tom instance has an attribute name that has been set to the name that was passed. Now let's look at the second method:

def say_hello(self):
    print "Hello, my name is " + self.name

This looks more like a normal function, but takes the same self argument. It then prints out a string containing the value of the name attribute of this instance. Note that while all methods should take the self argument, this argument doesn't need to be passed because when calling a method, this is automatically done:

In [7]:
tom.say_hello()
Hello, my name is Tom

is equivalent to:

In [8]:
Person.say_hello(tom)
Hello, my name is Tom

In the last example, we passed the instance explicitly to the function, but of course the first notation is much cleaner and simpler. Now let's look at the following example:

In [9]:
alice = Person("Alice")
bob = Person("Bob")
alice.say_hello()
bob.say_hello()
Hello, my name is Alice
Hello, my name is Bob

as you can see, when calling say_hello, the result will depend to the actual object that the method is attached to.

Since they are essentially functions, methods can of course take arguments, which can be any Python object(s). For example, we can make a new say_hello_to method that will take another Person instance and say hello to them:

In [10]:
class Person(object):
    
    def __init__(self, name):
        self.name = name
        
    def say_hello(self):
        print("Hello, my name is " + self.name)
        
    def say_hello_to(self, other):
        print("Hello " + other.name + ", my name is " + self.name)
In [11]:
alice = Person("Alice")
bob = Person("Bob")
alice.say_hello_to(bob)
Hello Bob, my name is Alice

As we will see below, you can actually pass any object to say_hello_to as long as it has a name attribute.

Inheritance

One of the powerful features of object-oriented programming is inheritance, which means that it is possible to define classes based on other classes. For example, we can define:

In [12]:
from scipy.integrate import simps

class Scientist(Person):
    
    def integrate(self, x, y):
        return simps(y, x=x)

This looks similar to before, but this time when defining the class, we've used Person instead of object. This means that by default, Scientist will behave like a Person instance, but it then has some additional methods defined:

In [13]:
alice = Scientist("Alice")
alice.integrate([1,2,3], [4,5,6])
Out[13]:
10.0

The say_hello_to method takes any object that has a name attribute, so it can say hello to another person:

In [14]:
bob = Person("Bob")
alice.say_hello_to(bob)
Hello Bob, my name is Alice

or scientist:

In [15]:
eve = Scientist("Eve")
alice.say_hello_to(eve)
Hello Eve, my name is Alice

Attributes

As mentioned above, attributes are variables attached to the object. It is worth mentioning that attributes are not static, so they can be changed from outside the class:

In [16]:
p = Person('Tom')
p.say_hello()
Hello, my name is Tom
In [17]:
p.name = 'Alice'
p.say_hello()
Hello, my name is Alice

and it is also possible to create new attributes from the outside:

In [18]:
p.age = 97
p.name
p.age
Out[18]:
97

Why use objects?

So far, the programs you have needed to write have not been very complex, but as you write more and more complex analysis, classes may come in useful in some situations. For example, if you are doing particle physics, you might want to use a class to represent a Particle, which then has common operations and calculations you might want to do with a particle. If you are doing Astronomy, you could use a Star or Galaxy class that would be used to represent these objects. You can even use objects without defining actual methods, but just as a convenience to contain several variables - if you need to always handle cartesian point in your code, you could define:

In [19]:
class Point(object):
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
In [20]:
pt = Point(1, 2, 3)
pt.x
Out[20]:
1

After which if you want to pass a point to a function, you can pass it in a single variable instead of three:

In [21]:
def find_separation(p1, p2):
    return np.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2 + (p1.z - p2.z)**2)

as opposed to:

In [22]:
def find_separation(x1, y1, z1, x2, y2, z2):
    return np.sqrt((x1 - x2)**2 + (y1 - y2)**2 + (z1 - z2)**2)

This might not look like a big difference, but now imagine that you also wanted to pass 3-d velocities, then you would need to call the function with 12 arguments!

This is not to say that you should always use objects, but if you start to think of your program instead of what objects and basic operations are being represented, you may be able to write it more simply than if you were using only functions and procedural code. You may also be able to re-use classes for different projects if they are general enough!

Exercise

  1. Write a Particle class that can be used to represent a particle with a mass, a 3-d position, and a 3-d velocity.

  2. Write a method that can be used to compute the kinetic energy of the particle

  3. Write a method that takes another particle as an argument and finds the distance between the two particles

  4. Write a method that given a time interval dt will update the position of the particle to the new position based on the current position and velocity.

  5. Write a ChargedParticle class that inherits from the Particle class, but also has an attribute for the charge of the particle.

  6. Write examples of using these classes to test that the methods are working correctly.

In [23]:
# your solution here