Klassen en objecten¶
Het ontwerpen van data
Een geheel nieuwe klasse class van programmeren!
Bouwstenen¶
functies
functiecompositie
Een probleem opdelen in functies, waar elke functie verantwoordelijk is voor een deel van de oplossing. Je hebt niet alleen eigen functies geschreven, maar bedenk ook hoe vaak je ingebouwde functies als len, min, max hebt gebruikt als onderdeel van jouw functiecomposities!
Data¶
De representatie van informatie: typen
string
int / float
list
En combinaties van typen als LoL’s!
Een Student ontwerpen¶
Een student gaat studeren…
Van een student willen we informatie opslaan, laten we beginnen met naam en jaar van inschrijven.
student_naam = "Jan Janssen"
student_start_jaar = 2023
Binnen de opleiding ICT hebben we veel meer studenten dan 1. Het moet mogelijk zijn om informatie van meerdere studenten te gebruiken.
student1_naam = "Jan Janssen"
student1_start_jaar = 2023
student2_naam = "John Smith"
student2_start_jaar = 2023
Dit is onhandig, misschien is het mogelijk het in een lijst te plaatsen?
students = ["Jan Janssen", 2023, "John Smith", 2023]
Zodra we meerdere studenten hebben en ook andere informatie willen opslaan wordt het al snel onoverzichtelijk. Het wordt ook lastig om informatie te vinden van een specifieke student.
Elke student heeft een studentnummer, wat handig kan zijn om een student te vinden. Bijvoorbeeld het vinden van de naam van de student met nummer 463726.
students = [463726, "Jan Janssen", 2023, 482638, "John Smith", 2023]
student_index = students.index(463726)
print(students[student_index + 1])
Jan Janssen
In een lijst moet je eerst de index van een element weten voordat je het uit de lijst kunt halen. Het zou handig zijn als we het studentnummer als index konden gebruiken. Daarvoor ken je uit week 1 de dictionary.
Dictionary¶
In een dictionary hoort bij elke sleutel een waarde
d = {47: 2, 42: 1}
Een waarde haal je op met haar sleutel, niet met een positie zoals in een lijst
d[47] == 2
d[42] == 1
students = {463726: ["Jan Janssen", 2023], 482638: ["John Smith", 2023]}
print(students[463726])
['Jan Janssen', 2023]
students = {463726: ["Jan Janssen", 2023], 482638: ["John Smith", 2023]}
print(students[463726][0])
Jan Janssen
Goed nieuws, sleutels kunnen ook andere typen dan int zijn!
student1 = {"naam": "Jan Janssen", "start_jaar": 2023}
student2 = {"naam": "John Smith", "start_jaar": 2023}
print(student1["naam"])
Jan Janssen
En net als een lijst lijsten kan bevatten, de zogenaamde LoL, kan een dictionary uit dictionaries bestaan.
students = {
463726: {"naam": "Jan Janssen", "start_jaar": 2023},
482638: {"naam": "John Smith", "start_jaar": 2023},
}
print(students[463726]["naam"])
Jan Janssen
Zo kun je gegevens over studenten opslaan, maar het blijft een dictionary. Wat als we een eigen type Student konden maken?
Klassen en objecten¶
Een klasse is een type
Een object is een specifieke instantie van dat type
Alles in Python is een object: elke waarde die je in een variabele kunt stoppen. Wat een object kan, zijn methoden, wordt bepaald door zijn klasse.
s = str("Astronaut wordt snel oud tijdens een reis naar Mars")
De variabele s verwijst naar een object, en dat object is een instantie van de klasse str. In de klasse str staat bijvoorbeeld de methode count(). Die telt hoe vaak een gegeven teken in de string voorkomt.
s = str("Astronaut wordt snel oud tijdens een reis naar Mars")
print(s.count("e"))
5

Zie een klasse als een mal: er is er maar één van, maar je kunt er véél objecten mee maken. De klasse legt de “vorm” vast, een object is de concrete uitwerking van die vorm (koekjes!).

Een klasse kun je ook zien als een blauwdruk of bouwtekening (een gedetailleerde mal). Ze legt vast wat een object is en wat het kan (een raket kan vliegen, in een huis met keuken kan worden gekookt, …). Wat een object is, staat in zijn attributen; wat het kan, in zijn methoden.
Een klasse Student ontwerpen¶
Je kunt ook zelf een klasse schrijven, en daarmee een nieuw type: Student.
class Student:
"""Een student met een naam en een startjaar."""
def __init__(self, name, year):
"""Maak een student met de gegeven naam en het gegeven startjaar."""
self.name = name
self.year = year
def __repr__(self):
"""Geeft de student als string, om af te drukken."""
return "naam: " + self.name + ", startjaar: " + str(self.year)
def delay(self, num_years):
"""Stel de start van de studie num_years jaar uit."""
self.year += num_years
Klasse en object¶
De klasse Student hierboven is een ontwerp. Er bestaat nog geen enkele student: de klasse beschrijft alleen wat elke student heeft en wat hij kan. Pas als je de klasse aanroept, zoals Student("Jasper Klein", 2021), ontstaat er een object. Van één klasse kun je zoveel objecten maken als je wilt, en elk object heeft zijn eigen waarden.
De begrippen op een rij:
Begrip |
Wat het is |
In |
|---|---|---|
klasse |
het ontwerp: welke attributen en methoden elk object krijgt |
|
object |
één exemplaar, gemaakt volgens dat ontwerp, met eigen waarden |
|
attribuut |
een gegeven dat bij één object hoort |
|
methode |
een functie die in de klasse staat en werkt op het object waarvoor je haar aanroept |
|
constructor |
de methode |
|
|
het object waarop een methode op dat moment werkt |
|
Een object is een instantie van zijn klasse: een student die je met Student(...) maakt, is een instantie van Student.
De methode __repr__ geeft een object als string, zodat Python weet hoe het object afgedrukt moet worden.
Methoden met dubbele underscores heten ook wel dunder-methoden. Het is een conventie in Python om speciale methoden zo te schrijven. Handig, want zo zijn ze goed te onderscheiden van andere methoden (die jij gaat schrijven!).
Nu kun je studenten aanmaken: vier objecten van één klasse.
student_a = Student("Peter Been", 2021)
student_b = Student("Jasper Klein", 2021)
student_c = Student("Linda Buitendijk", 2022)
student_d = Student("Myrthe Zomer", 2021)
Zodra je een student aanmaakt, roept Python de constructor aan. __init__(self, name, year) heeft drie parameters, maar Student("Jasper Klein", 2021) geeft er twee mee: de eerste, self, vult Python zelf in met het nieuwe object. print verwacht een string. Een student moet dus eerst een string worden voordat hij afgedrukt kan worden, en daarvoor is de methode __repr__().
student_b.__repr__()
'naam: Jasper Klein, startjaar: 2021'
__repr__() is een dunder-methode, en je roept hem normaal niet zelf aan. Python gebruikt hem op de achtergrond wanneer dat nodig is. Zolang de klasse __repr__() heeft, kun je het object rechtstreeks afdrukken.
print(student_b)
naam: Jasper Klein, startjaar: 2021
Je kunt een attribuut van een object ook rechtstreeks opvragen.
student_b.name
'Jasper Klein'
En je kunt een methode van een object aanroepen.
student_b.delay(1)
student_b.year
2022
print(student_b)
naam: Jasper Klein, startjaar: 2022
Encapsulatie¶
Hierboven kwam je op twee manieren bij het startjaar: via de methode delay, en rechtstreeks, met student_b.year. Dat tweede werkt, maar het is ook het probleem. Wie rechtstreeks bij een attribuut kan, kan het ook veranderen:
student_b.year = 1850
print(student_b)
naam: Jasper Klein, startjaar: 1850
Python protesteert niet. Toch hoort een startjaar alleen naar achteren te schuiven, met delay: je kunt je studie uitstellen, maar niet terugdraaien naar een jaar waarin de opleiding nog niet bestond. Die regel staat nergens, dus niemand bewaakt haar.
Bij de dictionaries aan het begin van dit college was het niet anders. Met student1["start_jaar"] = 1850 kan elke regel code in het programma elk gegeven veranderen. Daar is niets verborgen.
Encapsulatie is het principe dat de staat van een object verborgen is voor de buitenwereld. Wie het object gebruikt, komt er alleen via zijn methoden aan. Zo kan de klasse zelf bewaken dat die staat klopt.
Een attribuut afschermen¶
Python geeft je twee middelen.
De _-conventie. Een attribuut waarvan de naam met een _ begint, zoals self._year, is voor de klasse zelf. Van buiten de klasse gebruik je het niet. Python houdt je niet tegen, maar wie de code leest, weet dat hij er niet aan hoort te komen.
@property. Soms mag de buitenwereld een waarde wel lezen, maar niet veranderen. Dan schrijf je een methode die de waarde teruggeeft, met @property erboven. Zo’n regel met een @ boven een def heet een decorator: hij verandert hoe de methode werkt. Door @property roep je de methode aan zonder haakjes, alsof het een attribuut is: student_b.year in plaats van student_b.year().
class Student:
"""Een student met een naam en een startjaar."""
def __init__(self, name, year):
"""Maak een student met de gegeven naam en het gegeven startjaar."""
self.name = name
self._year = year
def __repr__(self):
"""Geeft de student als string, om af te drukken."""
return "naam: " + self.name + ", startjaar: " + str(self._year)
def delay(self, num_years):
"""Stel de start van de studie num_years jaar uit, en nooit terug."""
if num_years > 0:
self._year += num_years
@property
def year(self):
"""Het startjaar, alleen om te lezen."""
return self._year
Het startjaar lezen werkt zoals eerst:
student_b = Student("Jasper Klein", 2021)
student_b.year
2021
Uitstellen werkt ook, maar terugdraaien niet:
student_b.delay(1)
student_b.delay(-200)
print(student_b)
naam: Jasper Klein, startjaar: 2022
En rechtstreeks een ander startjaar toekennen weigert Python nu, met een foutmelding:
student_b.year = 1850
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[20], line 1
----> 1 student_b.year = 1850
AttributeError: property 'year' of 'Student' object has no setter
De regel staat nu op één plek, in de klasse, en elke verandering van het startjaar gaat erlangs. Dat levert encapsulatie op: de klasse bewaakt haar eigen staat, en wie haar gebruikt hoeft die regels niet te kennen om ze niet te breken.
Let op wat de _ níet doet: student_b._year = 1850 werkt nog steeds. Python dwingt het niet af; de _ is een afspraak. Daarom gebruik je van buiten een klasse nooit een naam die met een _ begint.
Compositie¶
Een attribuut van een student bevat een string of een getal. Een attribuut kan ook objecten van een eigen klasse bevatten. Een studiegroep bestaat uit studenten, dus de klasse StudyGroup hieronder bewaart Student-objecten in een lijst. Die lijst begint met een _: van buiten de klasse kom je er alleen bij via de methoden add en starting_in.
class StudyGroup:
"""Een groep studenten die samen aan een project werken."""
def __init__(self):
"""Maak een lege studiegroep."""
self._students = []
def add(self, student):
"""Voeg student toe aan de groep."""
self._students.append(student)
def starting_in(self, start_year):
"""Geeft een lijst van de leden met startjaar start_year."""
result = []
for student in self._students:
if student.year == start_year:
result.append(student)
return result
starting_in komt niet aan _year. Ze vraagt elke student naar zijn startjaar, via de property year. Wat een student bijhoudt en welke regels daarvoor gelden, blijft zo in de klasse Student.
Maak een groep met twee studenten:
group = StudyGroup()
student_a = Student("Peter Been", 2021)
student_b = Student("Jasper Klein", 2021)
group.add(student_a)
group.add(student_b)
print("Startjaar 2021:")
for student in group.starting_in(2021):
print(student)
Startjaar 2021:
naam: Peter Been, startjaar: 2021
naam: Jasper Klein, startjaar: 2021
Stel nu de studie van Jasper een jaar uit. Dat gebeurt buiten de groep om, via student_b:
student_b.delay(1)
print("Startjaar 2021:")
for student in group.starting_in(2021):
print(student)
print("Startjaar 2022:")
for student in group.starting_in(2022):
print(student)
Startjaar 2021:
naam: Peter Been, startjaar: 2021
Startjaar 2022:
naam: Jasper Klein, startjaar: 2022
De groep heeft zelf niets gedaan, en toch geeft ze een ander antwoord. add heeft geen kopie van Jasper in de groep gezet, maar Jasper zelf. student_b en het tweede lid van de groep zijn hetzelfde object, dus wat je via student_b verandert, ziet de groep ook.
Compositie is het principe dat een object andere objecten als onderdeel heeft. De studiegroep bestaat uit studenten, en ze gebruikt die studenten alleen via hun methoden. In het practicum bouw je op dezelfde manier een Party, die uit Creature-objecten bestaat.
Opdrachten¶
Opdracht 1¶
Werk verder met de afgeschermde klasse Student, de versie met _year. Geef haar een attribuut voor het studentnummer. Pas de constructor aan zodat een student bij het aanmaken ook een studentnummer krijgt, en pas __repr__() aan zodat het studentnummer wordt afgedrukt.
Opdracht 2¶
Een studentnummer verandert nooit. Zorg dat het van buiten wel te lezen is, maar niet te veranderen: sla het op met een _ voor de naam en geef de klasse een property student_number. Voer de klasse opnieuw uit en maak student_b opnieuw aan, nu met een studentnummer: de student_b van hierboven is nog gemaakt zonder. Controleer dan dat student_b.student_number het nummer geeft, en dat student_b.student_number = 1 een foutmelding oplevert.
Opdracht 3¶
Maak een studiegroep en voeg er drie nieuwe studenten aan toe, alle drie met startjaar 2023. Gebruik je klasse
Studentuit opdracht 2, dus geef elke student ook een studentnummer.Stel de studie van één van hen een jaar uit. Doe dat via de variabele waarin je die student hebt bewaard, niet via de groep.
Voorspel wat
starting_in(2023)enstarting_in(2024)nu teruggeven. Controleer je voorspelling metprint, en druk elke student op een eigen regel af, zoals in het voorbeeld.Leg in één zin uit waarom de groep een ander antwoord geeft, terwijl je de groep na
addniet meer hebt aangeroepen.