Classes and objects
Objectives
By the end of this chapter, you will be able to:
- define a class with attributes and methods,
- create objects from a class,
- use the objects in a Python program,
- handle visibility (public, private),
- define subclasses of a class,
- define abstract classes,
- use polymorphism.
Documentation
Official documentation about classes
Define a class
As we touched on earlier, Python includes facilities for functional programming, as well as object-oriented programming. Thus, we can define classes in Python and instantiate objects from those classes.
Let us take the example of a class Car with two attributes: brand, color.
In the example below, we define the class Car and instantiate those classes into a number of objects.
class Car:
def __init__(self, brand, color):
self.brand = brand
self.color = color
def __str__(self):
return f"{self.color} {self.brand}"
if __name__ == '__main__':
car1 = Car("Mercedes", "green")
car2 = Car("Bmw", "blue")
car3 = Car("Audi", "orange")
for car in [car1, car2, car3]:
print(car)
What do we see here:
- The name of the class is preceded by the keyword
class. - By convention, we give the class a name with a capital letter.
- The class name is followed by a colon.
- The function
__init__is the constructor for the objects of the class. - For each function, we first pass
selfas an argument. - You define an attribute in by prefixing the attribute with
self.This is usually done in the constructor. - Instantiating a class, i.e. creating an object of a class, is done by giving the name of the class, with the arguments of the constructor (without
self). - A method or attribute is used with dot notations: the name of the object, followed by a dot, followed by the method or attribute.
- With the method
__str__, we define what the result when we print a car object.
Encapsulation and data hiding
As we know, an advantage of object-oriented programming is that we hide the implementation of a method from the outside world. We can hide methods and attributes when we make them private rather than public. By default, methods and attributes of a Python class are public. You can make them private by preceding the attribute with one or two underscores. To make private attributes accessible, we need to define getters and setters.
There is a difference between one and two underscores. With one underscore, there is no real encapsulation. It is a signal to other programmers that you should not access this attribute directly from outside the class, but it is not prevented by the language. Python considers programmers to be adults who know what they are doing.
Double underscores gives more protection. You can access the attribute from outside the class, but only via a work-around: the attribute is renamed as _Class__attribute. If, for instance, you have an attribute self__color in a class Car, you can access the attribute in the object myCar as follows: myCar._Car__color. Of course you should never do so, except in special cases such as debugging.
You can use both options.
class Car:
def __init__(self, brand, color):
self.__brand = brand # private
self.__color = color
def get_brand(self): # getter voor brand
return self.__brand
def get_color(self): # getter voor color
return self.__color
def set_color(self, color): # setter voor color
self.__color = color
def __str__(self):
return f"{self.__color} {self.__brand}"
if __name__ == '__main__':
car = Car("Ferrari", "red")
print(f"You have a {car}!")
car.set_color("black")
print(f"Now you have a {car}!")
Inheritance
To create a subclass of a class, we put the name of the parent class in brackets after the name of the class. The subclass adopts the methods and attributes of the parent class, but can also override them or the subclass can define additional attributes or methods.
class Robot:
"""
Class representing a talking robot.
"""
def __init__(self):
"""
We set the default name of the Robot
Because we define a subclass Robot, we set the attribute as protected
by adding one underscore before it.
This is not a enforced. It is just a convention.
"""
self._name = "Nameless" # start with one underscore, is merely a convention
def say_hello(self):
print("Hello, I am " + self._name + "!")
def rename(self, name):
self._name = name
def get_name(self):
return self._name
# TerminatorRobot inherits from Robot
class TerminatorRobot(Robot):
"""
Class representing a robot in the future
"""
def __init__(self):
super().__init__() # call the constructor of the super class
#Robot.__init__(self) # alternative for super().__init__()
self.eyes = "laser eyes" # new attribute. _name is inherited
# Overrides the method say_hello from the super class
def say_hello(self):
super().say_hello() # We first call the method of the super class
#Robot.say_hello(self) # alternative for super().say_hello()
print("I have " + self.eyes + "...") # we add an element to the method
if __name__ == '__main__':
my_robot = Robot()
my_robot.say_hello()
my_robot.rename("Kirk")
my_robot.say_hello()
naam = my_robot.get_name()
print("The robot is called " + naam)
print("The robot is called " + my_robot._name) # the attribute is not really protected
terminator = TerminatorRobot()
terminator.rename("Arnold")
terminator.say_hello()
Class attributes and class methods
Class attributes and class methods do not belong to objects of the class, but to the class itself. We know that we have to use these sparingly, but Python does provide them. We also call these static attributes or static methods.
We simply define a static attribute in the class without preceding it with the keyword self.
class Dog:
kind = 'canine' # a class variable shared by all instances of the class
def __init__(self, name):
self.name = name # object attribute unique for each object of the class
if __name__ == '__main__':
d = Dog('Fido')
e = Dog('Buddy')
print(d.kind) # 'canine', shared by all dogs
print(e.kind) # 'canine, shared by all dogs
print(d.name) # unique for d, thus 'Fido'
print(e.name) # unique for e, thus 'Buddy'
We do not give a class method the argument self and we precede the method by the decorator @classmethod. We do not call the methods to the object (although this can be done), but to the class itself.
class Person:
_population = 0 # class attribute
def __init__(self, name):
self.name = name
Person._population += 1 # the population gets larger when creating a new person
@classmethod
def total_population(cls):
return cls._population
if __name__ == '__main__':
john = Person("John")
pete = Person("Pete")
print(Person.total_population()) # should be two
Abstract classes
To define an abtract class, you must use the library abc. The class must inherit from the class ABC. For the abstract methods, we use the decorator @abstractmethod.
from abc import ABC, abstractmethod
class Vehicle(ABC): # abstract inherits from class AB
def __init__(self, speed):
self.speed = speed
@abstractmethod # decorator showing that the method is abstract
def drive(self):
pass # pass because an abstract method had no implementation
class Bike(Vehicle):
def drive(self):
print(f"I am driving my bike. My speed is {self.speed} km/h.")
class SportsCar(Vehicle):
def drive(self):
print(f"I am racing and I am going really fast! {self.speed} km/h!")
if __name__ == '__main__':
my_bike = Bike(25)
my_bike.drive()
my_porsche = SportsCar(185)
my_porsche.drive()
We cannot create an object from the class Vehicle but we kan inherit from this class.
Data classes
In many programs, we create classes that mainly store data — for example, to represent a Student, a Book, or a Point in 2D space. Such classes often contain only attributes and very little logic. In Python, there is a convenient shortcut for writing these data containers: the data class.
A data class automatically generates several useful methods for you, such as __init__(), __repr__() and __eq__(). This makes your code shorter and clearer.
To use data classes, import the dataclass decorator from the dataclasses module:
from dataclasses import dataclass
@dataclass
class Student:
name: str
number: int
email: str
This automatically creates an initializer like:
def __init__(self, name: str, number: int, email: str):
self.name = name
self.number = number
self.email = email
You can now create and print students easily:
s1 = Student("Alice", 123, "alice@example.com")
s2 = Student("Bob", 456, "bob@example.com")
print(s1)
# Output: Student(name='Alice', number=123, email='alice@example.com')
Data classes are especially useful for:
- keeping your code concise,
- comparing objects (they implement
__eq__automatically), - or converting to and from dictionaries (using
dataclasses.asdict()).
Example:
from dataclasses import asdict
print(asdict(s1))
# {'name': 'Alice', 'number': 123, 'email': 'alice@example.com'}