Defining your own classes

User Defined Types

A class is a user-programmed Python type (since Python 2.2!)

It can be defined like:

class Room(object):
    pass

Or:

class Room():
    pass

Or:

class Room:
    pass

What’s the difference? Before Python 2.2 a class was distinct from all other Python types, which caused some odd behaviour. To fix this, classes were redefined as user programmed types by extending object, e.g., class room(object).

So most Python 2 code will use this syntax as very few people want to use old style python classes. Python 3 has formalised this by removing old-style classes, so they can be defined without extending object, or indeed without braces. But this will make code incompatible with Python 2!

Just as with other python types, you use the name of the type as a function to make a variable of that type:

zero = int()
type(zero)

int

myroom = Room()
type(myroom)

main.Room

In the jargon, we say that an object is an instance of a particular class.

Once we have an object with a type of our own devising, we can add properties at will:

myroom.name = "Living"
myroom.name

‘Living’

The most common use of a class is to allow us to group data into an object in a way that is easier to read and understand than organising data into lists and dictionaries.

myroom.capacity = 3
myroom.occupants = ["Jamilla", "Suleman"]

Methods

So far, our class doesn’t do much!

We define functions inside the definition of a class, in order to give them capabilities, just like the methods on built-in types.

class Room(object):
    def overfull(self):
        return len(self.occupants) > self.capacity
myroom = Room()
myroom.capacity = 3
myroom.occupants = ["Jamilla", "Suleman"]
myroom.overfull()

True

myroom.occupants.append(['Clare'])
myroom.occupants.append(['Bob'])
myroom.overfull()

True

When we write methods, we always write the first function argument as self, to refer to the object instance itself, the argument that goes “before the dot”.

This is just a convention for this variable name, not a keyword. You could call it something else if you wanted.

Constructors

Normally, though, we don’t want to add data to the class attributes on the fly like that.

Instead, we define a constructor that converts input data into an object.

class Room(object):
    def __init__(self, name, exits, capacity, occupants=[]):
        self.name = name
        self.occupants = occupants # Note the default argument, occupants start empty
        self.exits = exits
        self.capacity = capacity
    def overfull(self):
        return len(self.occupants) > self.capacity
living = Room("Living Room", {'north': 'garden'}, 3)
living.capacity

3

Methods which begin and end with two underscores in their names fulfil special capabilities in Python, such as constructors.

Object-oriented design

In building a computer system to model a problem, therefore, we often want to make:

For example, the below program might describe our “Maze of Rooms” system:

We define a “Maze” class which can hold rooms:

class Maze(object):
    def __init__(self, name):
        self.name = name
        self.rooms = {}

    def add_room(self, room):
        room.maze = self # The Room needs to know which Maze it is a part of
        self.rooms[room.name] = room

    def occupants(self):
        return  [occupant for room in self.rooms.values()
                 for occupant in room.occupants.values()]

    def wander(self):
        """Move all the people in a random direction"""
        for occupant in self.occupants():
            occupant.wander()

    def describe(self):
        for room in self.rooms.values():
            room.describe()

    def step(self):
        self.describe()
        print("")
        self.wander()
        print("")

    def simulate(self, steps):
        for _ in range(steps):
            self.step()

And a “Room” class with exits, and people:

class Room(object):
    def __init__(self, name, exits, capacity, maze = None):
        self.maze = maze
        self.name = name
        self.occupants = {} # Note the default argument, occupants start empty
        self.exits = exits # Should be a dictionary from directions to room names
        self.capacity = capacity

    def has_space(self):
        return len(self.occupants) < self.capacity

    def available_exits(self):
        return [exit for exit, target in self.exits.items()
                if self.maze.rooms[target].has_space() ]

    def random_valid_exit(self):
        import random
        if not self.available_exits():
            return None
        return random.choice(self.available_exits())

    def destination(self, exit):
        return self.maze.rooms[ self.exits[exit] ]

    def add_occupant(self, occupant):
        occupant.room = self # The person needs to know which room it is in
        self.occupants[occupant.name] = occupant

    def delete_occupant(self, occupant):
        del self.occupants[occupant.name]

    def describe(self):
        if self.occupants:
            print(self.name, ": ", " ".join(self.occupants.keys()))

We define a “Person” class for room occupants:

class Person(object):
    def __init__(self, name, room = None):
        self.name=name

    def use(self, exit):
        self.room.delete_occupant(self)
        destination=self.room.destination(exit)
        destination.add_occupant(self)
        print(self.name, "goes", exit, "to the", destination.name)

    def wander(self):
        exit = self.room.random_valid_exit()
        if exit:
            self.use(exit)

And we use these classes to define our people, rooms, and their relationships:

jamilla=Person('Jamilla')
suleman=Person('Suleman')
bob=Person('Bob')
clare=Person('Clare')
living=Room('livingroom', {'outside':'garden', 'upstairs':'bedroom', 'north':'kitchen'}, 2)
kitchen=Room('kitchen', {'south':'livingroom'}, 1)
garden=Room('garden', {'inside':'livingroom'}, 3)
bedroom=Room('bedroom', {'jump':'garden', 'downstairs': 'livingroom'}, 1)
house=Maze('My House')
for room in [living, kitchen, garden, bedroom]:
    house.add_room(room)
living.add_occupant(jamilla)
garden.add_occupant(suleman)
garden.add_occupant(clare)
bedroom.add_occupant(bob)

And we can run a “simulation” of our model:

house.simulate(3)

livingroom : Jamilla

garden : Suleman Clare

bedroom : Bob

Jamilla goes outside to the garden

Suleman goes inside to the livingroom

Clare goes inside to the livingroom

Bob goes jump to the garden

livingroom : Suleman Clare

garden : Jamilla Bob

Suleman goes upstairs to the bedroom

Clare goes outside to the garden

Jamilla goes inside to the livingroom

Bob goes inside to the livingroom

livingroom : Jamilla Bob

garden : Clare

bedroom : Suleman

Jamilla goes outside to the garden

Bob goes north to the kitchen

Clare goes inside to the livingroom

Suleman goes downstairs to the livingroom

Object oriented design

There are many choices for how to design programs to do this. Another choice would be to separately define exits as a different class from rooms.

This way, we can use arrays instead of dictionaries, but we have to first define all our rooms, then define all our exits.

class Maze(object):
    def __init__(self, name):
        self.name = name
        self.rooms = []
        self.occupants = []

    def add_room(self, name, capacity):
        result = Room(name, capacity)
        self.rooms.append(result)
        return result

    def add_exit(self, name, source, target, reverse= None):
        source.add_exit(name, target)
        if reverse:
            target.add_exit(reverse, source)

    def add_occupant(self, name, room):
        self.occupants.append(Person(name, room))
        room.occupancy += 1

    def wander(self):
        "Move all the people in a random direction"
        for occupant in self.occupants:
            occupant.wander()

    def describe(self):
        for occupant in self.occupants:
            occupant.describe()

    def step(self):
        house.describe()
        print("")
        house.wander()
        print("")

    def simulate(self, steps):
        for _ in range(steps):
            self.step()
class Room(object):
    def __init__(self, name, capacity):
        self.name = name
        self.capacity = capacity
        self.occupancy = 0
        self.exits = []

    def has_space(self):
        return self.occupancy < self.capacity

    def available_exits(self):
        return [exit for exit in self.exits if exit.valid() ]

    def random_valid_exit(self):
        import random
        if not self.available_exits():
            return None
        return random.choice(self.available_exits())

    def add_exit(self, name, target):
        self.exits.append(Exit(name, target))

class Person(object):
    def __init__(self, name, room = None):
        self.name=name
        self.room=room

    def use(self, exit):
        self.room.occupancy -= 1
        destination=exit.target
        destination.occupancy +=1
        self.room=destination
        print(self.name, "goes", exit.name, "to the", destination.name)

    def wander(self):
        exit = self.room.random_valid_exit()
        if exit:
            self.use(exit)

    def describe(self):
        print(self.name, "is in the", self.room.name)
class Exit(object):
    def __init__(self, name, target):
        self.name = name
        self.target = target

    def valid(self):
        return self.target.has_space()
house=Maze('My New House')
living=house.add_room('livingroom', 2)
bed = house.add_room('bedroom', 1)
garden = house.add_room('garden', 3)
kitchen = house.add_room('kitchen', 1)
house.add_exit('north', living, kitchen, 'south')
house.add_exit('upstairs', living, bed, 'downstairs')
house.add_exit('outside', living, garden, 'inside')
house.add_exit('jump',bed, garden)
house.add_occupant('Jamilla', living)
house.add_occupant('Suleman', garden)
house.add_occupant('Bob', bed)
house.add_occupant('Clare', garden)
house.simulate(3)

Jamilla is in the livingroom

Suleman is in the garden

Bob is in the bedroom

Clare is in the garden

Jamilla goes north to the kitchen

Suleman goes inside to the livingroom

Bob goes downstairs to the livingroom

Jamilla is in the kitchen

Suleman is in the livingroom

Bob is in the livingroom

Clare is in the garden

Suleman goes outside to the garden

Bob goes upstairs to the bedroom

Clare goes inside to the livingroom

Jamilla is in the kitchen

Suleman is in the garden

Bob is in the bedroom

Clare is in the livingroom

Jamilla goes south to the livingroom

Bob goes jump to the garden

Clare goes north to the kitchen

This is a huge topic, about which many books have been written.

The differences between these two designs are important, and will have long-term consequences for the project.

That is the how we start to think about software engineering, as opposed to learning to program.