XClose

An introduction to research programming with Python

Home
Menu

Defining your own classes

User defined types

A class is a user-programmed Python type.

A minimal class can be defined like this:

In [1]:
class Room:
    pass

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

In [2]:
zero = int()
In [3]:
zero
Out[3]:
0
In [4]:
print(type(zero))
<class 'int'>
In [5]:
my_room = Room()
In [6]:
my_room
Out[6]:
<__main__.Room at 0x7f8db0600a90>
In [7]:
print(type(my_room))
<class '__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:

In [8]:
my_room.name = "Living"
In [9]:
print(my_room.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.

In [10]:
my_room.capacity = 3
my_room.occupants = ["Alice", "Bob"]
In [11]:
my_string = "Hello"
my_string.upper()
Out[11]:
'HELLO'

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.

In [12]:
class Room:

    def overfull(self):
        """Whether room is currently full beyond its capacity."""
        return len(self.occupants) > self.capacity
In [13]:
my_room = Room()
my_room.capacity = 3
my_room.occupants = ["Alice", "Bob"]
In [14]:
my_room.overfull()
Out[14]:
False
In [15]:
my_room.occupants.append(['Carol'])
In [16]:
my_room.occupants.append(['Dan'])
In [17]:
my_room.overfull()
Out[17]:
True

When we write methods, we always write the first function argument as self, to refer to the object instance itself.

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.

In [18]:
class Room:

    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):
        """Whether room is currently full beyond its capacity."""
        return len(self.occupants) > self.capacity
In [19]:
living_room = Room("Living room", {'north': 'garden'}, 3)
In [20]:
living_room.capacity
Out[20]:
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:

  • classes for each kind of thing in our system
  • methods for each capability of that kind
  • properties (defined in a constructor) for each piece of information describing that kind

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

We define a Maze class which can hold rooms:

In [21]:
class Maze:
    """A maze containing zero or more rooms."""
    
    def __init__(self, name):
        self.name = name
        self.rooms = {}
        
    def add_room(self, room):
        """Add a room to the maze."""
        room.maze = self  # the room needs to know which maze it is a part of
        self.rooms[room.name] = room
    
    def occupants(self):
        """Get list of occupants of all rooms in maze."""
        return  [
            occupant 
            for room in self.rooms.values()
            for occupant in room.occupants.values()
        ]
    
    def wander(self):
        """Move all room occupants in a random direction."""
        for occupant in self.occupants():
            occupant.wander()
                
    def describe(self):
        """Print a summary of the current state of the maze."""
        for room in self.rooms.values():
            room.describe()
            
    def step(self):
        """Print summary of current maze state and then update."""
        print('=' * 80)
        self.describe()
        print('-' * 80)
        self.wander()
        
    def simulate(self, steps):
        """Simulate maze by iteratively randomly moving occupants."""
        for _ in range(steps):
            self.step()

And a Room class with exits, and people:

In [22]:
import random

class Room:
    """A room within a maze containing zero or more occupants."""
    
    def __init__(self, name, exits, capacity, maze=None):
        self.maze = maze
        self.name = name
        self.occupants = {} # Room occupants starts empty
        self.exits = exits  # Should be a dictionary from directions to room names
        self.capacity = capacity
        
    def has_space(self):
        """Whether the room has space for more occupants."""
        return len(self.occupants) < self.capacity
    
    def available_exits(self):
        """Get list of exits to rooms with space for more occupants."""
        return [
            exit for exit, target in self.exits.items()
            if self.maze.rooms[target].has_space()
        ]
            
    def random_valid_exit(self):
        """Choose a random exit from those with space for more occupants."""
        if not self.available_exits():
            return None
        return random.choice(self.available_exits())

    def destination(self, exit):
        """Get room destination corresponding to specified exit."""
        return self.maze.rooms[self.exits[exit]]
    
    def add_occupant(self, occupant):
        """Add an occupant to room."""
        occupant.room = self # The person needs to know which room it is in
        self.occupants[occupant.name] = occupant
        
    def delete_occupant(self, occupant):
        """Remove an occupant from room."""
        self.occupants.pop(occupant.name)
        
    def describe(self):
        """Print a description of the current occupants of the room."""
        if self.occupants:
            print(f"{self.name.capitalize()}: {', '.join(self.occupants.keys())}")

We define a Person class for room occupants:

In [23]:
class Person:
    """A person within a room in maze."""
    
    def __init__(self, name):
        self.name = name
    
    def use(self, exit):
        """Use the specified exit to move to another room."""
        self.room.delete_occupant(self)
        destination = self.room.destination(exit)
        destination.add_occupant(self)
        print(f"{self.name} goes {exit} to the {destination.name}.")
    
    def wander(self):
        """Try to move to another room via a random exit."""
        exit = self.room.random_valid_exit()
        if exit:
            self.use(exit)

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

In [24]:
alice = Person('Alice')
bob = Person('Bob')
carol = Person('Carol')
dan = Person('Dan')
In [25]:
living_room = Room('living room', {'outside': 'garden', 'upstairs': 'bedroom', 'north': 'kitchen'}, 2)
kitchen = Room('kitchen', {'south': 'living room'}, 1)
garden = Room('garden', {'inside': 'living room'}, 3)
bedroom = Room('bedroom', {'jump': 'garden', 'downstairs': 'living room'}, 1)
In [26]:
house = Maze('house')
for room in [living_room, kitchen, garden, bedroom]:
    house.add_room(room)
In [27]:
living_room.add_occupant(alice)
In [28]:
garden.add_occupant(bob)
garden.add_occupant(carol)
In [29]:
bedroom.add_occupant(dan)

And we can run a "simulation" of our model:

In [30]:
house.simulate(3)
================================================================================
Living room: Alice
Garden: Bob, Carol
Bedroom: Dan
--------------------------------------------------------------------------------
Alice goes outside to the garden.
Bob goes inside to the living room.
Carol goes inside to the living room.
Dan goes jump to the garden.
================================================================================
Living room: Bob, Carol
Garden: Alice, Dan
--------------------------------------------------------------------------------
Bob goes outside to the garden.
Carol goes north to the kitchen.
Alice goes inside to the living room.
Dan goes inside to the living room.
================================================================================
Living room: Alice, Dan
Kitchen: Carol
Garden: Bob
--------------------------------------------------------------------------------
Alice goes upstairs to the bedroom.
Dan goes outside to the garden.
Carol goes south to the living room.
Bob goes inside to the living room.

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.

In [31]:
class Maze:
    """A maze containing zero or more rooms."""
    
    def __init__(self, name):
        self.name = name
        self.rooms = []
        self.occupants = []
        
    def add_room(self, name, capacity):
        """Add room to maze with specified name and capacity."""
        result = Room(name, capacity)
        self.rooms.append(result)
        return result
        
    def add_exit(self, name, source, target, reverse=None):
        """Add exit from source to target room and optionally the reverse exit."""
        source.add_exit(name, target)
        if reverse:
            target.add_exit(reverse, source)
            
    def add_occupant(self, name, room):
        """Add an occupant to the specified room."""
        self.occupants.append(Person(name, room))
        room.occupancy += 1
    
    def wander(self):
        """Move all room occupants in a random direction."""
        for occupant in self.occupants:
            occupant.wander()
                
    def describe(self):
        """Print a summary of the current state of the maze."""
        for occupant in self.occupants:
            occupant.describe()
            
    def step(self):
        """Print summary of current maze state and then update."""
        print('=' * 80)
        self.describe()
        print('-' * 80)
        self.wander()
        
    def simulate(self, steps):
        """Simulate maze by iteratively randomly moving occupants."""
        for _ in range(steps):
            self.step()
In [32]:
import random

class Room:
    """A room within a maze containing zero or more occupants."""
    
    def __init__(self, name, capacity):
        self.name = name
        self.capacity = capacity
        self.occupancy = 0
        self.exits = []
        
    def has_space(self):
        """Whether the room has space for more occupants."""
        return self.occupancy < self.capacity
    
    def available_exits(self):
        """Get list of exits to rooms with space for more occupants."""
        return [exit for exit in self.exits if exit.valid()]
            
    def random_valid_exit(self):
        """Choose a random exit from those with space for more occupants."""
        if not self.available_exits():
            return None
        return random.choice(self.available_exits())
    
    def add_exit(self, name, target):
        """Add an exit to the room."""
        self.exits.append(Exit(name, target))
    
In [33]:
class Person:
    """A person within a room in maze."""
    
    def __init__(self, name, room=None):
        self.name = name
        self.room = room
    
    def use(self, exit):
        """Use the specified exit to move to another room."""
        self.room.occupancy -= 1
        destination = exit.target
        destination.occupancy += 1
        self.room = destination
        print(f"{self.name} goes {exit.name} to the {destination.name}.")
    
    def wander(self):
        """Try to move to another room via a random exit."""
        exit = self.room.random_valid_exit()
        if exit:
            self.use(exit)
            
    def describe(self):
        """Print a description of current location in maze."""
        print(f"{self.name} is in the {self.room.name}.")
In [34]:
class Exit:
    """An exit from one room in maze to another."""
    
    def __init__(self, name, target):
        self.name = name
        self.target = target
    
    def valid(self):
        return self.target.has_space()
In [35]:
house = Maze('house')
In [36]:
living_room = house.add_room('living room', 2)
bed = house.add_room('bedroom', 1)
garden = house.add_room('garden', 3)
kitchen = house.add_room('kitchen', 1)
In [37]:
house.add_exit('north', living_room, kitchen, 'south')
In [38]:
house.add_exit('upstairs', living_room, bed, 'downstairs')
In [39]:
house.add_exit('outside', living_room, garden, 'inside')
In [40]:
house.add_exit('jump', bed, garden)
In [41]:
house.add_occupant('Alice', living_room)
house.add_occupant('Bob', garden)
house.add_occupant('Carol', bed)
house.add_occupant('Dan', garden)
In [42]:
house.simulate(3)
================================================================================
Alice is in the living room.
Bob is in the garden.
Carol is in the bedroom.
Dan is in the garden.
--------------------------------------------------------------------------------
Alice goes north to the kitchen.
Bob goes inside to the living room.
Carol goes jump to the garden.
Dan goes inside to the living room.
================================================================================
Alice is in the kitchen.
Bob is in the living room.
Carol is in the garden.
Dan is in the living room.
--------------------------------------------------------------------------------
Bob goes outside to the garden.
Carol goes inside to the living room.
Dan goes upstairs to the bedroom.
================================================================================
Alice is in the kitchen.
Bob is in the garden.
Carol is in the living room.
Dan is in the bedroom.
--------------------------------------------------------------------------------
Alice goes south to the living room.
Carol goes outside to the garden.
Dan goes downstairs to the living room.

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, and is where this course ends, and future courses begin!

Exercise: your own solution

Compare the two solutions above. Which do you like better, and why? Sarting from scratch, design your own. What choices did you make that are different?