Objects and Classes

Objects and Classes

You have now learned how to package up your code into re-usable, documented functions, and how to then package up those functions into re-usable, documented modules (libraries). It is great to package up your code so that it is easy for other people to understand and re-use. However, one problem is that other people have a habit of re-using your code in the wrong, or in unexpected ways…

As an example, lets pretend to be someone who is using your morse.py module in the wrong way… Start a new ipython session and type;

import morse
morse.letter_to_morse = "c"
print( morse.encodeToMorse("Hello world") )

You should see that the following error is printed;

TypeError                                 Traceback (most recent call last)
<ipython-input-5-8f98527ab404> in <module>()
----> 1 morse.encodeToMorse("Hello world")
  
     25     for letter in message:
     26         letter = letter.lower()
---> 27         morse.append(letter_to_morse[letter])
     28 
     29     return string.join(morse," ")

What happened here???

The problem is that the letter_to_morse variable is visible, and that we can change its value whenever we want. Above, we changed letter_to_morse so that equalled the string c. This breaks the encodeToMorse function, as this was written assuming that letter_to_morse was a dictionary.

As another example, lets pretend to be a user who is trying to edit the Morse code dictionary… Exit ipython and then start a new ipython session. Type;

import morse
morse.morse_to_letter["...."] = "K"
print( morse.encodeToMorse("help") )

This should have printed the Morse code '.... . .-.. .--.'. All so well and good. Now let’s try to decode this same message back to English. Type;

print( morse.decodeFromMorse(".... . .-.. .--.") )

Now, instead of seeing the expected message (help), you should see that the message has been decoded to Kelp. Why has this happened?

As you can see, allowing a user of your code to mess with the data on which it relies can lead to subtle, and difficult to find bugs!

Encapsulation - Hiding Data

The problem you have found is that your functions depend on their associated data. The encodeToMorse and decodeFromMorse functions depend on the letter_to_morse and morse_to_letter dictionaries. Changing these dictionaries changes the behaviour of these functions, and risks breaking them or introducing subtle bugs.

Object orientated programming solves this problem by packaging functions and their associated data together into something that is called a class. A class defines the type of data, together with functions that manipulate that data. Encapsulation is a key idea of object orientated programming, and means to hide the data in a class, such that only the functions defined as part of the class can read or write (change) that data.

For example, here is a Python class that implements a simple guessing game.

"""A simple guessing game"""
    
class GuessGame:
    """A simple guess the secret game"""
    def __init__(self, secret):
        """Construct a game with the passed secret"""
        self.__secret = secret
        self.__nguesses = 0   # the number of guesses
    
    def guess(self, value):
        """See if the passed value is equal to the secret"""
    
        if self.__nguesses >= 3:
            print( "Sorry, you have run out of guesses." )
    
        elif value == self.__secret:
            print( "Well done - you have won the game!" )
            return True
        else:
            print( "Wrong answer. Try again!" )
            self.__nguesses += 1  # increase the number of wrong guesses
            return False

This piece of Python contains lots of new ideas. Before we explore them, lets try and play the game. Exit ipython, and then use nano to copy and paste the above script into the file guessgame.py. Now start a new ipython session in the same directory as your copy of guessgame.py. Into ipython type;

import guessgame
game = guessgame.GuessGame("cat")
game.guess("dog")

This will print;

Wrong answer. Try again!
Out[3]: False

Now try typing;

game.guess("fish")

This will print;

Wrong answer. Try again!
Out[3]: False

Now try typing;

game.guess("cat")

This will print;

Well done - you have won the game!
Out[5]: True

Lets take a look at the help for GuessGame. Type;

help(game)

You will now see printed;

Help on instance of GuessGame in module guessgame:
    
class GuessGame
 |  A simple guess the secret game
 |  
 |  Methods defined here:
 |  
 |  __init__(self, secret)
 |      Construct a game with the passed secret
 |  
 |  guess(self, value)
 |      See if the passed value is equal to the secret

“GuessGame”, defined in this module is a example of a class. Classes are used to package up functions with associated data. As you can see in the help(), we can only see the functions defined in the class. There are two functions, __init__, which is used to construct a new object of type GuessGame, and guess which is used to guess the secret. As you can see, the first argument to each of these functions is self. self is a special variable that is used by the class to gain access to the data hidden within.

Lets look again at the source for GuessGame.

"""A simple guessing game"""
    
class GuessGame:
    """A simple guess the secret game"""
    def __init__(self, secret):
        """Construct a game with the passed secret"""
        self.__secret = secret
        self.__nguesses = 0   # the number of guesses
    
    def guess(self, value):
        """See if the passed value is equal to the secret"""
    
        if self.__nguesses >= 3:
            print( "Sorry, you have run out of guesses." )
    
        elif value == self.__secret:
            print( "Well done - you have won the game!" )
            return True
        else:
            print( "Wrong answer. Try again!" )
            self.__nguesses += 1  # increase the number of wrong guesses
            return False

Here you can see that the keyword class is used to define a new class (in this case, called GuessGame). Within the class you can see defined the two functions, __init__ and guess. The __init__ function is special, and is called the constructor. It must be present in all classes, and constructors are used in all object orientated programming languages. The job of the constructor is to define how to create an object of the class, i.e. how to initialise the data contained within an object instance of the class. In this case, you can see that the constructor specifies two variables, _secret, which will hold the secret to be guessed, and _nguesses, which holds the number of wrong guesses made to date. Note that these variables start with an underscore. This is the way you tell Python that the variables are private to the class. It is good programming practice to ensure that all class variable names in python are private, and start with an underscore.

Note that the variables are defined as attached to self, via the full stop, e.g. self._secret. self is a special variable that is only available within the functions of the class, and provides access to the hidden data of the class. You can see that self is used by the guess function to check the passed guess against self._secret, and to increase the value of self._nguesses if the guess is wrong.

Note that we don’t need to pass self ourselves to the class functions. self is passed implicitly by Python when we construct an object of the class, or when we call a function of the object.

We can construct as many instances (objects) of a class as we want, and each will have its own self and its own set of hidden variables. For example, start a new ipython session and type;

from guessgame import GuessGame
game1 = GuessGame("orange")
game2 = GuessGame("carrot")
game3 = GuessGame("apricot")

game1.guess("apricot")

This will print;

Wrong answer. Try again!
Out[4]: False

while now typing;

game3.guess("apricot")

will result in you winning the game in game3, and seeing

Well done - you have won the game!
Out[6]: True

printed to the screen.

(Note that we have used the from X import Y syntax in Python to import only GuessGame from guessgame.py. This allows us to write game1 = GuessGame("orange") instead of game1 = guessgame.GuessGame("orange").)


Exercise

Edit your morse.py script and change it so that instead of function, you create a class called MorseTranslator. Package together the functions encodeToMorse and decodeFromMorse with the variables letter_to_morse and morse_to_letter. Rename the function encodeToMorse to encode and rename the function decodeFromMorse to decode.

Make sure that you document your class, e.g. by documenting the __init__ function you will have to write, and also by documenting the class, as in the above GuessGame class..

When you have finished, test that the Morse code produced by your class is correctly translated back to English, e.g. in a new ipython session type;

from morse import MorseTranslator
translator = MorseTranslator()
message = "hello world"
print( translator.decode( translator.encode(message) ) == message )

This should print True, showing that your MorseTranslator class can encode and decode the message.

If you want, you can take a look at this completed example.

Previous Next