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:
s = 'hello'
s.upper()
l = [4,2,1]
l.sort()
l
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 are most easily explained by example, so let's dive right in and look at a class, which is used to define an object:
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:
tom = Person('Tom')
tom.say_hello()
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:
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:
tom = Person('Tom')
print(tom.name)
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:
tom.say_hello()
is equivalent to:
Person.say_hello(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:
alice = Person("Alice")
bob = Person("Bob")
alice.say_hello()
bob.say_hello()
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:
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)
alice = Person("Alice")
bob = Person("Bob")
alice.say_hello_to(bob)
As we will see below, you can actually pass any object to say_hello_to
as long as it has a name
attribute.
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:
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:
alice = Scientist("Alice")
alice.integrate([1,2,3], [4,5,6])
The say_hello_to
method takes any object that has a name
attribute, so it can say hello to another person:
bob = Person("Bob")
alice.say_hello_to(bob)
or scientist:
eve = Scientist("Eve")
alice.say_hello_to(eve)
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:
p = Person('Tom')
p.say_hello()
p.name = 'Alice'
p.say_hello()
and it is also possible to create new attributes from the outside:
p.age = 97
p.name
p.age
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:
class Point(object):
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
pt = Point(1, 2, 3)
pt.x
After which if you want to pass a point to a function, you can pass it in a single variable instead of three:
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:
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!
Write a Particle
class that can be used to represent a particle with a mass, a 3-d position, and a 3-d velocity.
Write a method that can be used to compute the kinetic energy of the particle
Write a method that takes another particle as an argument and finds the distance between the two particles
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.
Write a ChargedParticle
class that inherits from the Particle
class, but also has an attribute for the charge of the particle.
Write examples of using these classes to test that the methods are working correctly.
# your solution here