In this module you will learn to write programs where objects can interact with each other.
In object-oriented programming a program is composed of classes. The classes are used to create instances, or objects, during runtime. The objects can interact with each other: an object can process other objects and call their methods.
This relationship between objects is called association. The power of object-oriented programming is achieved by programming these associative relationships: the program breaks up into small, easily understandable pieces and the programmer can write code in small portions, focusing in one feature at a time. When the associations between objects are designed well, even a large program is easy to build with these small parts.
In the last module we wrote a Dog
class that defines the properties of a dog (name, birth year and distinctive barking
sound). Furthermore, the class has a single method: bark
. The Dog
class looks like this:
class Dog:
def __init__(self, name, birth_year, sound="Woof woof"):
self.name = name
self.birth_year = birth_year
self.sound = sound
def bark(self, times):
for i in range(times):
print(self.name + " barks: " + self.sound)
return
Let’s extend the example by adding a dog hotel. A dog hotel is defined as follows: a dog can be taken to a dog hotel and later be picked up from the hotel. Occasionally, a member of the dog hotel staff takes a round around the hotel: they greet all the dogs and each dog barks back.
First, we will need to think about what we need to implement the dog hotel.
First of all, the dog hotel should be implemented as a separate class. The functionality of the dog hotel has nothing to do with a single dog, so it should not be written inside the Dog class. Therefore, we will add a second class called Hotel to our program.
Next, we need to think about what properties relate to a dog hotel. We notice that a dog hotel must know which dogs are in its care at any given time. This can be done by using a list: let’s add a list of dogs as a property of the Hotel class.
What about activities of a dog hotel that should be written into methods? From the definition earlier we can identify three methods we should write for a dog hotel:
Now we have defined and designed the program and we can go ahead to implement it.
There are two classes in our example program: Dog
and Hotel
. In Python, it is common to write multiple classes into
a single source file. The classes could also be in separate files. If the classes are in placed in
separate files, referencing another class is only possible if an import
statement for the other file (or module) is added
at the beginning of the program.
It is handy to write classes of a small program into one file, and this is what we will do now. We will create a file called doghotel.py where we will program all the required functionality:
class Dog:
def __init__(self, name, birth_year, sound="Woof woof"):
self.name = name
self.birth_year = birth_year
self.sound = sound
def bark(self, times):
for i in range(times):
print(self.name + " barks: " + self.sound)
return
class Hotel:
def __init__(self):
self.dogs = []
def dog_checkin(self, dog):
self.dogs.append(dog)
print(dog.name + " checked in")
return
def dog_checkout(self, dog):
self.dogs.remove(dog)
print(dog.name + " checked out")
return
def greet_dogs(self):
for dog in self.dogs:
dog.bark(1)
# Main program
dog1 = Dog("Rascal", 2018)
dog2 = Dog("Boi", 2022, "Yip yip yip")
hotel = Hotel()
hotel.dog_checkin(dog1)
hotel.dog_checkin(dog2)
hotel.greet_dogs()
hotel.dog_checkout(dog1)
hotel.greet_dogs()
The example program consists of three parts:
Dog
classHotel
classThe execution of the program starts at the beginning of the main program. First two dogs, Rascal and Boi, are created. Then a new hotel is created:
hotel = Hotel()
Now the execution moves to the initializer of the Hotel class where an empty dogs list is added as a property of the hotel. The newly created hotel does not have any guests yet, but it has an empty list for storing dogs later.
Next, the first dog (Rascal) is checked in to the hotel:
hotel.dog_checkin(dog1)
This is a method provided by the hotel: the check-in is clearly an activity of the hotel, which is why it has been
programmed into the Hotel
class. It is necessary to know which dog is going to be checked in, so the corresponding
Dog object (or actually a reference to the object) is passed as an argument in the method call. When the method is called,
the execution moves to the dog_checkin
method where the dog received as a parameter is added to the dog list of the hotel.
The second dog Boi is checked in to the hotel the same way.
Now it is time for the caretaker to do a round in the hotel and greet all the dogs. To do this the corresponding method written
in the Hotel
class is called:
hotel.greet_dogs()
This method was implemented without parameters. The greeting is targeted to all dogs that are currently in the hotel and the hotel itself knows which dogs its taking care of at any given time. The method iterates through the list of dogs and tells each dog to bark once.
Finally, one dog, Boi, is checked out from the hotel. For this, the corresponding method that removes a dog from the list
in the Hotel
class is called. Then the dogs are greeted again, but this time only Rascal is there to answer.
The operation of the program can be seen from its output:
Rascal checked in
Boi checked in
Rascal barks: Woof woof
Boi barks: Yip yip yip
Boi checked out
Rascal barks: Woof woof
Now we have written a program that has instances (or objects) from two different classes. We can say that there is a
permanent association between the Hotel
class and the Dog
class: A Hotel
object has an instance variable that
holds the references to Dog
objects.
Here the associative relationship is unidirectional: A Hotel
object knows which dogs are currently staying in the hotel.
On the other hand, a Dog
object has no knowledge of the hotel it might currently be staying in. An associative relationship
can be implemented either unidirectionally or bidirectionally. A bidirectional association should only be used when absolutely
needed. Bidirectional association brings an extra burden to the programmer because the contents of the object references to
different directions must be in sync.
As was mentioned above, the Hotel
and Dog
classes in the example shared a static association: the dogs in the hotel
were stored as a list into the property of the hotel.
The Hotel
and Dog
classes also share another type of a dependency: The Hotel
class provides to methods that
have a reference to a Dog
object as a parameter. An associative relationship can also be valid only during a method call
when an instance of the other class is listed as a parameter of a method. When the method call finishes, the associative
relationship used during the method call would vanish if the information of the relationship wasn’t stored as a property like
we did in our example.
Let’s look at an example of a situation where an associative relationship is purely temporary: the relationship between a car and a car paint shop. In this example we will create a blue car and pass it to a paint shop to be painted red:
class Car:
def __init__(self, plate_number, colour):
self.plate_number = plate_number
self.colour = colour
class PaintShop:
def paint(self, car, colour):
car.colour = colour
paint_shop = PaintShop()
car = Car("ABC-123", "blue")
print("The car is " + car.colour)
paint_shop.paint(car, "red")
print("The car is now " + car.colour)
The program prints out the colour of the car before and after the paint job:
The car is blue
The car is now red
In this example the paint shop knows the car it needs to paint only for time of the execution of the paint
method as
the reference to the Car
object was received as a parameter of the method call. Once the execution of method is finished,
the value of the parameter variable can no longer be accessed. The car has no knowledge of the paint shop either. In this
example the associative relationship between the paint shop and the car is only temporary.