Python 6: Classes and Objects

Contents

6.1 Why classes and objects ?

Classes extend the concept of module packaging. So far we have used modules to define functions shared between programs. In some cases modules usefully include constants related to the purpose of the module e.g. math.pi . A class defines a template for a self-contained bundle of behaviour (functions) which can be combined with data any number of times. Just as a ball may have a size and a colour, a speed and a direction, if you have a ball class you can have any number of ball objects each having their own sizes, colours, speeds and directions. Imagine we were to simulate a game of snooker. Our ball class would have methods to govern the relevant physics and would construct ball objects each of which would know the attributes for each ball.

All of the data types we have dealt with so far in Python have already been objects, including float, string and integer primitives and combinations of these in lists, tuples and dictionaries. We have not had to think about classes to use these objects because their types were built into the language. By defining our own classes we can define our own data types to meet application requirements, and construct any number of objects of our newly defined types. A class is a specialised kind of object factory. The class acts as a blueprint and the object is a thing made using the blueprint. Names of things in Python are all references to objects. We call objects "instances" of the class which made them. Objects are brought to life by the class methods. A method is simply a function that knows its object.

6.2 Using a trivial class and made up attributes

>>> class null:            # a do nothing much class
... 	pass               # do nothing statement
...
>>> a=null()               # a is an object created by null class.
>>> b=null()               # b is another object
>>> a.c=2                  # give object a an attribute c with value 2
>>> b.d=4                  # same kind of deal
>>> a.c+b.d                # add the value attributes and print
6

Most of the time we're going to be putting classes into modules and importing them rather than coding them transiently (as above) or non-reusably (within the client script). OO (Object Oriented) programming purists might be offended by the above code, which violates many of their cherished ideals of encapsulation, as if classes and objects had to be secured inside software lockers so that only access authorised by the class is possible. If you really want to you can design Python classes as if they were in a state of siege, but this is slightly more complicated. In practice the language optimisation for rapid prototyping allows you to start as simply as fits the immediate need and make things more containerised as requirements develops and time permits. If you have good reasons for object style names in your code rather than a proliferation of names and don't want to go any further into OO, the above coding style lets you do this without further overhead.

6.3 Methods

A class method is a function that knows its object.

>>> class rectangle:
... 	def area(self):
... 		return self.width*self.height
...
>>> a=rectangle()
>>> a.width=2
>>> a.height=3
>>> a.area()
6

In the above example the method: area() returns the area as the multiple of the object a's width and height attributes. The method area() is called without any parameters, as an attribute of the object a. However, when the method is run the object reference is handled as the first paramter. The name of this parameter: self is not a special word in Python, it is the name used to refer to the current object by convention.

6.4 Constructor methods

Classes such as the ones above only constructed empty objects, to which we added data attributes later. When an object is constructed by calling the class e.g: a=rectangle() the class will run a method (if it has one) called __init__() . (There are 2 underscores before and after the name: init .) We're going to encounter other special class attribute names (i.e. methods and data) which catch particular operations. These have reserved names which both begin and end with double underscores.

# geometry module: geom.py
class rectangle: # rectangle class
    # make a rectangle using top left and bottom right coordinates
    def __init__(self,tl,br):
        self.tl=tl
        self.br=br
        self.width=abs(tl.x-br.x)  # width
        self.height=abs(tl.y-br.y) # height
    def area(self): # gets area of rectangle
        return self.width*self.height

class coordinate: # coordinate class
     def __init__(self,x,y): # make a coordinate object with a reference (self), an x and a y
        self.x=x
        self.y=y
     def distance(self,another): # distance between 2 coordinates
        import math
        xdist=abs(self.x-another.x)
        ydist=abs(self.y-another.y)
        return math.sqrt(xdist**2+ydist**2) # pythagoras theorem

The above geom package contains 2 classes, coordinate and rectangle. The following commands import this package, construct 2 coordinates and a rectangle and then calculate the area of the rectangle and the distance between the 2 coordinates:

>>> import geom
>>> a=geom.coordinate(2,3)
>>> b=geom.coordinate(5,7)
>>> c=geom.rectangle(a,b)
>>> c.area()
12
>>> a.distance(b)
5.0

6.5 Class data attributes

Object data attributes are either constructed with or added to each object of the class. However, consider the case of a washing machine factory. If each washing machine manufactured from a production line has its own unique serial number, how does the factory know which serial number to give to the next washing machine off the production line ? If serial numbers start with 1 and go up by 1 each time, the last issued is the same as the count of machines manufactured. For this we use a class attribute. Here is a class which simulates a washing machine, with class attribute: no_made.

# washing module file: washing.py
class machine:
    no_made=0
    def __init__(self):
        machine.no_made+=1
        self.serial=machine.no_made
    def spin(self):
      print "wheeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee!!"
    def wash(self):
      print "slosh    slosh    slosh    slosh   slosh"
    def label(self):
      print "washing machine: %d" % self.serial

The following commands were used to test this class:

>>> import washing
>>> a=washing.machine()
>>> a.wash()
slosh    slosh    slosh    slosh   slosh
>>> a.spin()
wheeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee!!
>>> a.serial
1
>>> a.no_made
1
>>> b=washing.machine()
>>> washing.machine.no_made
2
>>> c=washing.machine()
>>> a.no_made
3
>>> b.serial
2
>>> c.serial
3
>>> c.label()
washing machine: 3
>>> a.label()
washing machine: 1

Notice that only one copy of the class attribute: no_made exists, but every object has its own serial number.

6.6 Inheritance

In our example ball class there probably are many games which have balls with similar physics or functionality. We could code behaviour relevant to balls used in all sorts of ball games in a parent ball class, and have specialised behaviour relevant to particular games, e.g. billiards, pinball and bowls in subclasses.

Objects inherit behaviour from the classes that construct them through method objects, and class data attributes shared with other objects, as well as having object data of their own. Classes can also inherit from superclasses or parent classes. Again both data and functional attributes are inherited.

A Python class can inherit from one parent, as in Java, or from any number as in C++ . Multiple inheritance can lead to complexities to do with where attributes come from (remember the analogy of opening multiple jigsaw puzzles onto the same table). These are probably best avoided unless you know why you want to do this.

Inheritance allows you to have a general class and to create a number of specialised versions of it. The child or specialised classes reuse code within the generalised parent class, and add some of their own, to override attributes within the parent, and by adding new ones.

Inheritance is probably best illustrated through an example. Here we have a Suprex Deluxe washing machine which does all that our generalised washing machine does, but it has a model attribute, a tumble dry cycle and overides the label method within its parent.

#  file: suprex.py
from washing import machine
class deluxe(machine): # deluxe subclasses parent class machine
    model="Suprex Deluxe" # adds an attribute
    def tumble_dry(self): # adds a method
      print "tumble tumble chug tumble tumble chug"
    def label(self): # overrides a method in parent class
      print "Model: %s Serial No: %d" % (deluxe.model,self.serial)

Here is a test run, with comments after # symbols

>>> import suprex
>>> a=suprex.deluxe()	# make a suprex deluxe using parent constructor
>>> b=suprex.machine()	# suprex module can construct object of parent class
>>> a.tumble_dry()	# a suprex can tumble dry
tumble tumble chug tumble tumble chug
>>> a.wash()		# suprex knows how to wash from parent
slosh    slosh    slosh    slosh   slosh
>>> b.wash()		# so can an ordinary machine
slosh    slosh    slosh    slosh   slosh
>>> b.tumble_dry()	# ordinary machines can't tumble dry
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
AttributeError: machine instance has no attribute 'tumble_dry'
>>> a.serial		# a got this object attribute from parent class
1
>>> b.serial		# can the parent also count instances of children ?
2
>>> a.label()		# suprex has its own method for this
Model: Suprex Deluxe Serial No. 1
>>> b.label()		# ordinary machines have other code
washing machine: 2
>>> b.no_made		# no_made attribute is accessible through both classes
2
>>> a.no_made
2

6.7 Polymorphism

This word means something having many forms. This idea is built into the Python language from the ground up so as to make life easier for you the programmer. For example you can have data of many kinds in a list, and if an operation (e.g. adding) makes different sense in different contexts Python usually helps. E.G. adding numbers does what you would expect and adding strings joins them together, with both operations using the same + operator.

You can override built in names in Python, e.g. by defining your own len() function and localising the override to the scope where this is needed. You can override class methods by subclassing if this is useful. Python classes also allow you to define methods with special names e.g: __add__(), __del__(), so that you can define what happens when you use + and - operators between your objects. Many Python operators can be overriden for class objects. In the following example we use the __getitem__ method to override what happens when we index an object:

use of __getitem__ to intercept indexing operations

>>> class mystring:
... 	def __getitem__(self,index):
... 		import string
... 		capital=string.upper(self.contents[index])
... 		return capital
...
>>> a=mystring()
>>> a.contents="abcdefghijklmnopqrstuvwxyz"
>>> a[0]		# __getitem__ method overides indexing operator
'A'
>>> a[25]
'Z'
>>> a.data[25]		# a.data was and still is lower case
'z'
>>>

use of  __repr__ to intercept print operations

>>> class printmachine(suprex.deluxe):
... 	def __repr__(self):
... 		return "Instance of model: %s Serial Number: %d" %
			(self.model,self.serial)
...
>>> a=printmachine()
>>> print a
Instance of model: Suprex Deluxe Serial Number: 4

6.8 Controlling access to class and object attributes

Up to a point people won't go into houses where they're not welcome. In earlier examples adding attributes to objects outside the class and changing anything inside was allowed by default. Most of the time programmers who mess inside modules and classes other than through the published interfaces only have themselves to blame if they don't like the consequences.

If the nature of your project is such that the security needs of your classes go beyond the assumption that unintended forms of access is someone else's problem, Python does allow you to code methods called __getattr__ and __setattr__ to intercept read and write access to your class attributes. These methods can force consistent attribute behaviour when unknown attributes are referenced or inappropriate access is made to values which should be managed inside the class.

""" A class which controls access to its attributes """
class locked_data:
    max=100 # constant
    def __init__(self,module="WPA4"):
        self.module=module
    def __getattr__(self,attrib):
        if attrib == "title":
            return "Website Programming Applications IV"
        else:  # redirect access to unknown attributes
            return self.module
    def __setattr__(self,attrib,value):
        if attrib in ["module","title"]: # List the attributes
                                         # which can be written to here.
            self.__dict__[attrib]=value  # Have to access through __dict__
                                         # to avoid infinite regression
        else:
            raise AttributeError

This test run demonstrates attribute read redirections and write locking-mechanisms :

>>> from locked import locked_data
>>> a=locked_data()
>>> a.max		# constant class attribute
100
>>> a.title		# default values
'Website Programming Applications IV'
>>> a.module
'WPA4'
>>> a.thing		# __getattr__ returns module for unknown attribute
'WPA4'
>>> a.max=50		# __setattr__ prevents write to class constant
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "locked.py", line 15, in __setattr__
    raise AttributeError
AttributeError
>>> a.max		# a.max stays the same
100
>>> a.thing=42		# can't write to non-existent attribute
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "locked.py", line 15, in __setattr__
    raise AttributeError
AttributeError
>>> a.title="Another"	# can change module and/or title
>>> a.module="new"
>>> a.title
'Another'
>>> a.module
'new'