Classes and Objects

Materials by Tommy Guy and Anthony Scopatz

Object Orientation

Object oriented programming is a way of thinking about and defining how different pieces of software and ideas work together. In objected oriented programming, there are two main interfaces: classes and objects.

  • Classes are types of things such as int, float, person, or square.
  • Objects are instances of those types such a 1, 42.0, me, and a square with side length 2.

Unlike functional or procedural paradigms, there are three main features that classes provide.

  • Encapsulation: Classes are container which may have any kind of other programming element living on them: variables, functions, and even other classes. In Python, members of a class are known as attributes for normal variables and methods for functions.
  • Inheritence: A class may automatically gain all of the attributes and methods from another class it is related to. The new class is called a subclass or sometimes a subtype. Multiple levels of inheritance sets up a class heirarchy. For example:

    • Shape is a class with an area attribute.
    • Rectangle is a subclass of Shape.
    • Square is a subclass of Rectangle (which also makes it a subclass of Shape).
  • Polymorphism: Subclasses may override methods and attributes of their parents in a way that suitable to them. For example:.

    • Shape is a class with an area method.
    • Square is a subclass of Shape which computes area by $x^2$.
    • Circle is a subclass of Shape which computes area by $\pi r^2$

If this seems more complicated than writing functions and calling them in sequence that is because it is! However, obeject orientation enables authors to cleanly separate out ideas into independent classes. It is also good to know because in many languages - Python included - it is the way that you modify the type system.

Basic Classes

Object oriented programming revolves around the creation and manipulation of objects that have attributes and can do things. They can be as simple as a coordinate with x and y values or as complicated as a dynamic webpage framework. Here is the code for making a very simple class that sets an attribute.


In [ ]:
class MyClass(object):
    def me(self, name):
        self.name = name

In [ ]:
my_object = MyClass()

In [ ]:
type(my_object)

In [ ]:
my_object.name = 'Anthony'
print my_object.name

In [ ]:
my_object.me('Jan')
print my_object.name

In the object oriented terminology above:

  • MyClass - a user defined type.
  • my_object - a MyClass instance.
  • me() - a MyClass method (member function)
  • self - a refernce to the object that is used to define method. This must be the first agument of any method.
  • name - an attribute (member variable) of MyClass.
  • object - a special class which should be the parent of all classes.

You write a class and you create and object.

Hands on Example

Write an Atom class with mass and velocity attributes and an energy() method.


In [ ]:

Here is a more complex and realisitic example of a matrix class:


In [ ]:
# Matrix defines a real, 2-d matrix.

class Matrix(object):
    # I am a matrix of real numbers

    def __init__(self, h, w):
        self._nrows = h
        self._ncols = w
        self._data = [0] * (self._nrows * self._ncols)

    def __str__(self):
        return "Matrix: " + str(self._nrows) + " by " + str(self._ncols)

    def setnrows(self, w):
        self._nrows = w
        self.reinit()

    def getnrows(self):
        return self._nrows

    def getncols(self):
        return self._ncols

    def reinit(self):
        self._data = [0] * (self._nrows * self._ncols)

    def setncols(self, h):
        self._ncols = h
        self.reinit()

    def setvalue(self, i, j, value):
        if i < self._nrows and j < self._ncols:
            self._data[i * self._nrows + j] = value
        else:
            raise Exception("Out of range")

    def multiply(self, other):
        # Perform matrix multiplication and return a new matrix.
        # The new matrix is on the left.
        result = Matrix(self._nrows, other.getncols())
        # Do multiplication...
        return result

    def inv(self):
        # Invert matrix 
        if self._ncols != self._nrows:
            raise Exception("Only square matrices are invertible")
        inverted = Matrix(self._ncols, self._nrows)
        inverted.setncols(self._ncols)
        inverted.setnrows(self._ncols)
        return inverted

Programatic Attribute Access

Python provides three built-in functions, getattr(), setattr(), and delattr() to programticly access an object's members. These all take the object and the string name of the attribute to be accessed. This is instead of using dot-access.


In [ ]:
class A(object):
    a = 1

In [ ]:
avar = A()
getattr(avar, 'a')

In [ ]:
setattr(avar, 'q', 'mon')
print avar.q

Interface vs. Implementation

Users shouldn't have to know how your program works in order to use it.

The interface is a contract saying what a class knows how to do. The code above defines matrix multiplication, which means that mat1.multiply(mat2) should always return the right answer. It turns out there are many ways to multiply matrices, and there are whole Ph.Ds written on performing efficient matrix inversion. The implementation is the way in which the contract is carried out.

Constructors

Usually you want to create an object with a set of initial or default values for things. Perhaps an object needs certain information to be created. For this you write a constructor. In python, constructors are just methods with the special name __init__():


In [ ]:
class Person(object):
    def __init__(self):
        self.name = "Anthony"

In [ ]:
person = Person()
print person.name

Constructors may take arguements just like any other method or function.


In [ ]:
class Person(object):
    def __init__(self, name, title="The Best"):
        self.name = name
        self.title = title

In [ ]:
anthony = Person("Anthony")
print anthony.name, anthony.title

In [ ]:
jan = Person("Jan", "The Greatest")
print jan.name, jan.title

Subclassing

If you want a to create a class that behaves mostly like another class, you should not have to copy code. What you do is subclass and change the things that need changing. When we created classes we were already subclassing the built in python class "object."

For example, let's say you want to write a sparse matrix class, which means that you don't explicitly store zero elements. You can create a subclass of the Matrix class that redefines the matrix operations.


In [ ]:
class SparseMatrix(Matrix):
    # I am a matrix of real numbers

    def __str__(self):
        return "SparseMatrix: " + str(self._nrows) + " by " + str(self._ncols)

    def reinit(self):
        self._data = {}

    def setValue(self, i, j, value):
        self._data[(i,j)] = value

    def multiply(self, other):
        # Perform matrix multiplication and return a new matrix.
        # The new matrix is on the left.
        result = SparseMatrix(self._nrows, other.getncols())
        # Do multiplication...
        return result

    def inv(self):
        # Invert matrix
        if self._nrows != self._rcols: 
            raise Exception("Only square matrices are invertible")
        inverted = SparseMatrix(self._ncols, self._nrows)

The SparseMatrix object is a Matrix but some methods are defined in the superclass Matrix. You can see this by looking at the dir of the SparseMatrix and noting that it gets attributes from Matrix.


In [ ]:
dir(SparseMatrix)

A more minimal and more abstact version of inheritence may be seen here:


In [ ]:
class A(object):
    a = 1

class B(A):
    b = 2
    
class C(B):
    b = 42
    c = 3

x = C()
print x.a, x.b, x.c

Properties

Normally, when you get or set attributes on an object the value that you are setting simply gets a new name. However, sometimes you run into the case where you want to do something extra depending on the actual value you are reciveing. For example, maybe you need to confirm that the value is actually correct or desired.

Python provides a mechanism called properties to do this. Properties are methods which either get, set, or delete a given attribute. To implement this, use the built-in property() decorator:


In [ ]:
class EvenNum(object):
    def __init__(self, value):
        self._value = 0
        self.value = value

    @property
    def value(self):
        # getter
        return self._value

    @value.setter
    def value(self, val):
        # setter
        if val % 2 == 0:
            self._value = val
        else:
            print "number not even"

In [ ]:
en = EvenNum(42)
print en.value

In [ ]:
en.value = 65
print en.value

In [ ]:
en.value = 16
print en.value

Data Model

Since classes are user-defined types in Python, they should interact normally with literal operators such + - * / == < > <= >= and other Python language constructs. However, Python doesn't know how to do these operations until the user tells it. Take the EvenNum example above:


In [ ]:
x = EvenNum(42)
y = 65
x + y

We need to let Python know that EvenNum addition should be addition on the value. We should also let it know that it should return an EvenNum if it can.

To do this we have to implmenet part of the Python Data Model. Python has a list of special - or sometimes known as magic - method names that you can override to implement support for many language operations. All of these method names start and end with a double underscore __. This is because no regual method would ever use such an obtuse name. It also lets the user and other developers know that something special is happening in those methods and that they aren't meant to be called directly. Many of these has a predefined interface they must follow.

We have already seen an example of this with the __init__() constructor method. Now let's try to make addition work for EvenNum. From the documentation, there is an __add__() method with the following API:

object.__add__(self, other)

In [ ]:
class EvenNum(object):
    def __init__(self, value):
        self._value = 0
        self.value = value

    @property
    def value(self):
        # getter
        return self._value

    @value.setter
    def value(self, val):
        # setter
        if val % 2 == 0:
            self._value = val
        else:
            print "number not even"  
            
    def __add__(self, other):
        if isinstance(other, EvenNum):
            newval = self.value + other.value
        else:
            newval = self.value + other
        return EvenNum(newval)

In [ ]:
x = EvenNum(42)
y = 65
x + y

One of the most useful of these special methods is the __str__() method, which allows you to provide a string representation of the object.


In [ ]:
class EvenNum(object):
    def __init__(self, value):
        self._value = 0
        self.value = value
        
    def __str__(self):
        return str(self.val)

    @property
    def value(self):
        # getter
        return self._value

    @value.setter
    def value(self, val):
        # setter
        if val % 2 == 0:
            self._value = val
        else:
            print "number not even"  
            
    def __add__(self, other):
        if isinstance(other, EvenNum):
            newval = self.value + other.value
        else:
            newval = self.value + other
        return EvenNum(newval)

In [ ]:
x = EvenNum(42)
y = 16
print x + y

Lastly, the most important magic part of classes and objects is the __dict__ attribute. This is dictionary where all of the the method and attributes of an object are stored. Modifying the __dict__ directly affects the object and vice versa.


In [ ]:
class A(object):
    def __init__(self, a):
        self.a = a

avar = A(42)

In [ ]:
print avar.__dict__

In [ ]:
avar.__dict__['b'] = 'yourself'
print avar.b

In [ ]:
avar.c = "me now"
print avar.__dict__

It is because of this that dictionaries are the most important container in Python. Under the covers, all types ae just dicts.