Klassen en objecten
Doelstellingen
Op het einde van dit hoofdstuk kan je:
- een klasse definiëren met attributen en methoden,
- van een klasse objecten creëren,
- de objecten gebruiken in een Python-programma,
- omgaat met de zichtbaarheid (public, private),
- subklassen van een klasse definiëren,
- abstracte klassen definiëren,
- gebruik maken van polymorfisme.
Documentatie
Officiële documentatie over klassen
Een klasse definiëren
Zoals we al eerder aanhaalden, bevat Python voorzieningen voor functioneel programmeren, maar ook voor objectgeoriënteerd programmeren. We kunnen dus in Python klassen definiëren en van die klassen objecten instantiëren.
Nemen we het voorbeeld van een klasse Car met twee attributen: brand, color.
In onderstaand voorbeeld definiëren we de klasse Car en instantiëren we die klassen in een aantal objecten.
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)
Wat zien we hier:
- De naam van de klasse wordt voorafgegaan door het keyword
class. - Bij conventie geven we de klasse een naam met een hoofdlettter.
- De klassenaam wordt gevolgd door een dubbelpunt.
- De functie
__init__is de constructor voor de objecten van de klasse. - Bij elke functie geven we eerst
selfmee als argument. - Een attribuut definieer je in door het attribuut te laten voorafgaan door
self.Dat gebeurt meestal in de constructor, maar dat is niet verplicht. - Een klasse instantiëren, m.a.w. een object creëren van een klasse gebeurt door de naam van de klasse te geven, met de argumenten van de constructor (zonder
self). - Een methode of attribuut gebruik je met de dot-notaties: de naam vna het object, gevolgd door een punt, gevolgd door de methode of het attribuut.
- De methode
__str__bepaalt wat er gebeurt als we aan object afdrukken.
Encapsulation en data hiding
Zoals we weten, is een voordeel van objectgeoriënteerd programmeren dat we de implementatie van een methode verbergen voor de buitenwereld. We kunnen methodes en attributen verbergen wanneer we ze niet public, maar private maken. Standaard zijn methodes en attributen van een Pythonklasse publiek. Je kan ze private maken door het attribuut te laten voorafgaan door één of twee lage streepjes (underscores). Om de attributen toegankelijk te maken, moeten we getters en setters definiëren.
Er is aan verschil tussen één en twee lage streepjes bij aan objectattribuut. Met één laag streepje is het attribuut eigenlijk toch gewoon toegankelijk van buiten de klasse. Het lage streepje is een indicatie aan andere programmeurs dat ze dit attribuut normaal gezien niet van buiten de klasse mogen benaderen, maar de taal Python voorkomt het niet. Python beschouwt programmeurs als volwassenen die weten waar ze mee bezig zijn.
Een dubbel laag streepje geeft meer bescherming. Je kan het attribuut nog van buiten de klasse benaderen, maar alleen via een speciale weg: het attribuut wordt intern hernoemd als _Class__attribute. Als je, bijvoorbeeld, bij een klasse Car een attribuut self.__color hebt, dan kan je dit attribuut bij een object myCar als volgt benaderen: myCar._Car_color. Het spreekt vanzelf dat je beter nooit doet, tenzij in heel specifieke gevallen, zoals debugging.
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}!")
Overerving
Om een subklasse van een klasse te maken, zetten we de naam van de moederklasse tussen haakjes na de naam van de klasse. De subklasse neemt de methodes en attributen van de moederklasse over, maar kan deze ook overschrijven (override) of de subklasse kan extra attributen of methodes definiëren.
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()
Klasseattributen en klassemethodes
Klasseattributten en klassemethodes horen niet bij objecten van de klasse, maar bij de klasse zelf. We weten dat we hiermee zuinig moeten omgaan, maar Python voorziet deze mogelijkheid wel. We noemen deze ook statische attributen of statische methodes.
Een statisch attribuut definiëren we gewoon in de klasse zonder deze te laten voorafgaan door het sleutelwoord 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'
Een klassemethode geven we niet het argument self en we laten de methode voorafgaan door de decorator @classmethod. We roepen de methoden niet aan bij het object (alhoewel dit wel kan), maar bij de klasse zelf.
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
Abstracte klassen
Om een abtracte klasse te definiëren moet je gebruik maken van de library abc. De klasse moet ervan van de klasse ABC. Voor de abstracte methodes gebruiken we de 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 kunnen hier geen object van de klasse Vehicle definiëren, maar we kunnen wel overerven van die klasse.
Dataklassen
In veel programma’s maken we klassen die vooral bedoeld zijn om gegevens bij te houden — bijvoorbeeld een Student, een Boek of een Punt in een vlak. Zulke klassen bevatten meestal enkel attributen en weinig logica. In Python bestaat er een handige manier om dit eenvoudiger te schrijven: de dataclass.
Een dataclass maakt automatisch een aantal nuttige methodes aan, zoals __init__(), __repr__() en __eq__(). Daardoor wordt je code korter en overzichtelijker.
Om een dataclass te gebruiken, importeer je de dataclass-decorator uit de module dataclasses:
from dataclasses import dataclass
@dataclass
class Student:
naam: str
nummer: int
email: str
Hiermee wordt automatisch een constructor gegenereerd zoals:
def __init__(self, naam: str, nummer: int, email: str):
self.naam = naam
self.nummer = nummer
self.email = email
Je kunt nu eenvoudig objecten aanmaken en afdrukken:
s1 = Student("Alice", 123, "alice@example.com")
s2 = Student("Bob", 456, "bob@example.com")
print(s1)
# Output: Student(naam='Alice', nummer=123, email='alice@example.com')
Dataclasses zijn vooral handig omdat ze:
- je code korter en leesbaarder maken,
- objecten automatisch kunnen vergelijken (__eq__),
- en eenvoudig kunnen worden omgezet naar een dictionary (met dataclasses.asdict()).
Voorbeeld:
from dataclasses import asdict
print(asdict(s1))
# {'naam': 'Alice', 'nummer': 123, 'email': 'alice@example.com'}