Classes and objects
Classes, objects, attributes, methods and constructors; encapsulation, access specifiers, getters and setters, and class diagrams.
Do this lesson in the simulatorA procedural program keeps its data in variables and passes it to subroutines. That works, but nothing ties the robot's odometer total to the code that is allowed to change it: any subroutine could add to it, reset it, or set it to nonsense. Object-oriented programming (OOP) bundles data together with the subroutines that act on it, and hides the data so the only way to change it is through those subroutines.
Class, object, attribute, method
- A class is a template, or blueprint, that defines what data an object holds and what it can do.
- An object is one instance of a class, with its own values. Making one is called instantiation.
- An attribute is a variable belonging to an object: its data, sometimes called its state.
- A method is a subroutine defined in a class: what the object can do.
- A constructor is the method that runs when an object is created, to give its attributes their starting values. In Python it is
__init__.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
class Horn:
def __init__(self, note): # the constructor
self.note = note # an attribute
self.times = 0
def sound(self): # a method
tone(self.note, 0.2)
self.times = self.times + 1
low = Horn(262) # instantiation: two objects of one class
high = Horn(784)
low.sound()
high.sound()
high.sound()
print("low sounded", low.times, "times; high sounded", high.times, "times")
Horn is the class; low and high are two objects of it, each with its own note and its own times. Inside a method, self is the object the method was called on: in high.sound(), self is high, so only high.times goes up.
Encapsulation and information hiding
Encapsulation means keeping an object's attributes and the methods that use them together in one class. It goes with information hiding: making attributes private, so code outside the class cannot read or change them directly, and must use the class's public methods. The class then controls every change and can refuse bad ones.
Access is set by access specifiers:
| Specifier | Class diagram symbol | Who can use it | Python convention |
|---|---|---|---|
| Public | + |
Any code | a plain name: speed |
| Private | - |
Only the class's own methods | two underscores: __speed |
| Protected | # |
The class and its subclasses | one underscore: _speed |
Python does not enforce these as strictly as Java or C#. A name starting with two underscores is renamed behind the scenes, so motor.__speed fails outside the class, which is close enough to private for our purposes. One underscore is only a convention that tells other programmers to keep out.
Methods that read or change a private attribute are called getters and setters, or accessor and mutator methods. A setter is where validation goes:
class Motor:
def __init__(self):
self.__speed = 0 # private
def get_speed(self): # getter
return self.__speed
def set_speed(self, speed): # setter: the only way to change it
if speed < 0 or speed > 100:
raise ValueError("speed must be 0 to 100")
self.__speed = speed
m = Motor()
m.set_speed(60)
print(m.get_speed())
try:
m.set_speed(250)
except ValueError as error:
print("refused:", error)
try:
print(m.__speed)
except AttributeError:
print("__speed is private")
print(m.get_speed())
The advantages of encapsulation are that an object cannot be put into an invalid state from outside, that the inside of a class can be changed (storing the speed differently, say) without breaking any code that uses it, and that each class can be written and tested as a unit.
Class diagrams
A class diagram shows a class as a box in three parts: the name, the attributes, and the methods, each marked +, - or #. Types may be shown after a colon.
In the exam languages
OCR's exam reference language writes a class with class ... endclass, marks attributes and methods public or private, calls the constructor new, and creates objects with the keyword new:
class Odometer
private name
private total
public procedure new(givenName)
name = givenName
total = 0
endprocedure
public function getTotal()
return total
endfunction
endclass
tripA = new Odometer("trip A")
print(tripA.getTotal())
Task: the odometer class
Write the class Odometer from the class diagram.
- The constructor
__init__(self, name)storesname(a string) in the private attributeself.__nameand sets the private attributeself.__totalto 0. drive(self, cm)takes a whole number of cm. Ifcmis 0 or more it drives forwardcm; if it is negative it drives backward by-cm. Either way it adds the size of the move,abs(cm), toself.__total.get_total(self)returns the total.report(self)returns the string<name>: <total> cm.
Then make two objects, trip_a = Odometer("trip A") and trip_b = Odometer("trip B"), and call trip_a.drive(20), trip_b.drive(-10) and trip_a.drive(15) in that order. Print trip_a.report() then trip_b.report(), which should show trip A: 35 cm and trip B: 10 cm.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
class Odometer:
def __init__(self, name):
self.__name = name
trip_a = Odometer("trip A")
Challenges
- Add
reset(self). Should it be public or private? Draw the new class diagram. - Try
print(trip_a.__total)outside the class. Then tryprint(trip_a._Odometer__total). What does that tell you about how private Python's privacy is? - Add a setter for the name that refuses an empty string with a
ValueError.