Operatoren en exceptions¶
Je eigen klassen laten meedoen met Python
Waar we waren¶
De klasse Student uit week 5 en 6, met een naam en een afgeschermd startjaar:
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
Twee dingen aan deze klasse gaan we vandaag veranderen. Je kunt twee studenten
niet op waarde vergelijken met ==, en delay doet stilletjes niets als je het
een getal geeft dat niet kan.
Wat == doet bij je eigen objecten¶
Twee keer dezelfde student:
a = Student("Sanne de Wit", 2024)
b = Student("Sanne de Wit", 2024)
print(a == b)
print(a is b)
False
False
Allebei False. In week 5 zag je waarom: bij je eigen objecten vergelijkt ==
de identiteit, net als is. Wilde je de waarde vergelijken, dan had je een eigen
methode nodig, zoals equals bij Date.
Die eigen methode kan ook == zelf zijn. Python vertaalt a == b naar de
aanroep a.__eq__(b). Schrijf je __eq__ in je klasse, dan bepaal jij wat ==
betekent.
__eq__¶
Twee studenten zijn gelijk als ze dezelfde naam en hetzelfde startjaar hebben:
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 __eq__(self, other):
"""Geeft True als other een student is met dezelfde naam en hetzelfde jaar."""
if not isinstance(other, Student):
return False
return self.name == other.name and self._year == other._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
a = Student("Sanne de Wit", 2024)
b = Student("Sanne de Wit", 2024)
print(a == b)
print(a is b)
print(a != Student("Ali Bakker", 2024))
print(a == "Sanne de Wit")
True
False
True
False
Nu vergelijkt == de waarde, en is nog steeds de identiteit: a en b zijn
gelijk, maar het blijven twee objecten. != hoef je niet te schrijven; Python
keert het antwoord van __eq__ om. En een string is geen student, dus daar is
het antwoord False.
Magische methoden¶
__eq__ roep je nooit zelf aan: Python doet dat, zodra je == schrijft. Zo’n
methode heet een magische methode. Je kent er al twee: __init__, die Python
aanroept als je een object maakt, en __repr__, bij print. De naam met twee
underscores ervoor en erachter heet een dunder, van double underscore: dat
zegt iets over de vorm van de naam, magisch zegt wat Python ermee doet.
Een operator eigen gedrag geven voor je eigen klasse heet operator overloading.
Je schrijft |
Python roept aan |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__lt__ en sorteren¶
Met __lt__ bepaal je wat < betekent. Hier: een student is kleiner dan een
andere als hij eerder begon. Omdat sorted met < vergelijkt, kun je dan een
lijst studenten sorteren.
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 __eq__(self, other):
"""Geeft True als other een student is met dezelfde naam en hetzelfde jaar."""
if not isinstance(other, Student):
return False
return self.name == other.name and self._year == other._year
def __lt__(self, other):
"""Geeft True als deze student eerder begon dan other."""
if not isinstance(other, Student):
raise TypeError("een student is alleen met een student te vergelijken")
return self._year < other._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
students = [
Student("Lotte Smit", 2025),
Student("Sanne de Wit", 2023),
Student("Ali Bakker", 2024),
]
for student in sorted(students):
print(student)
naam: Sanne de Wit, startjaar: 2023
naam: Ali Bakker, startjaar: 2024
naam: Lotte Smit, startjaar: 2025
En >? Daar is geen methode voor geschreven, en toch werkt het:
lotte = Student("Lotte Smit", 2025)
sanne = Student("Sanne de Wit", 2023)
print(lotte > sanne)
True
Student heeft geen methode voor >. Python geeft dan niet op, maar draait de
vergelijking om: lotte > sanne betekent hetzelfde als sanne < lotte, en
daarvoor is er __lt__.
Een argument van een ander type¶
Wat doet een operator als het andere object geen student is? Dat hangt af van de vraag.
Methode |
Bij een ander type |
Waarom |
|---|---|---|
|
geeft |
een student en een string zijn gewoon niet gelijk |
|
gooit een |
of een student kleiner is dan een string, is een vraag zonder antwoord |
Of het andere object het goede type heeft, vraag je met isinstance, uit week 6.
In week 6 vroeg starting_in juist niet naar de klasse: elk object met een year
mocht meedoen. Bij een operator ligt dat anders. __lt__ leest other._year, en
een vergelijking met iets anders heeft geen betekenis. Dan is een duidelijke
TypeError beter dan een onduidelijke AttributeError.
print(sanne < "Sanne")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 1
----> 1 print(sanne < "Sanne")
Cell In[5], line 22, in Student.__lt__(self, other)
19 def __lt__(self, other):
20 """Geeft True als deze student eerder begon dan other."""
21 if not isinstance(other, Student):
---> 22 raise TypeError("een student is alleen met een student te vergelijken")
23 return self._year < other._year
TypeError: een student is alleen met een student te vergelijken
raise in __lt__ gooit de TypeError. Wat dat precies doet, zie je
hierna.
Een fout melden met raise¶
Terug naar delay. Een negatief aantal jaren uitstellen kan niet, en delay
doet dan stilletjes niets:
ali = Student("Ali Bakker", 2024)
ali.delay(-2)
print(ali.year)
2024
Wie delay(-2) schrijft, heeft zich waarschijnlijk vergist. Maar niemand hoort
het: het programma gaat door alsof er niets is gebeurd, en de fout komt pas veel
later aan het licht, of nooit. Beter is het als delay meteen meldt dat dit niet
kan. Dat doet raise:
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 __eq__(self, other):
"""Geeft True als other een student is met dezelfde naam en hetzelfde jaar."""
if not isinstance(other, Student):
return False
return self.name == other.name and self._year == other._year
def __lt__(self, other):
"""Geeft True als deze student eerder begon dan other."""
if not isinstance(other, Student):
raise TypeError("een student is alleen met een student te vergelijken")
return self._year < other._year
def delay(self, num_years):
"""Stel de start num_years jaar uit; gooit een ValueError bij een negatief aantal."""
if num_years < 0:
raise ValueError("uitstellen kan alleen met 0 of meer jaar")
self._year += num_years
@property
def year(self):
"""Het startjaar, alleen om te lezen."""
return self._year
ali = Student("Ali Bakker", 2024)
ali.delay(-2)
print(ali.year)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[11], line 2
1 ali = Student("Ali Bakker", 2024)
----> 2 ali.delay(-2)
3 print(ali.year)
Cell In[10], line 28, in Student.delay(self, num_years)
25 def delay(self, num_years):
26 """Stel de start num_years jaar uit; gooit een ValueError bij een negatief aantal."""
27 if num_years < 0:
---> 28 raise ValueError("uitstellen kan alleen met 0 of meer jaar")
29 self._year += num_years
ValueError: uitstellen kan alleen met 0 of meer jaar
raise ValueError(...) maakt een exception en gooit haar. Python stopt
meteen met de methode, en ook met de code die de methode aanriep: print komt
niet meer aan de beurt. Je ziet een foutmelding, met op de laatste regel de soort
fout en de tekst die je meegaf.
ValueError is de soort fout voor een waarde van het goede type die toch niet
kan. Een paar soorten ken je al uit foutmeldingen:
Exception |
Wanneer |
|---|---|
|
het type klopt, de waarde niet: |
|
het type klopt niet: |
|
een attribuut of methode die het object niet heeft |
|
een methode die een subklasse zelf moet schrijven; zie verderop |
Een exception afvangen met try en except¶
Een exception hoeft je programma niet te laten stoppen. Met try en except
vang je haar af:
ali = Student("Ali Bakker", 2024)
try:
ali.delay(-2)
print("uitgesteld")
except ValueError:
print("niet uitgesteld: dat kan niet")
print(ali.year)
niet uitgesteld: dat kan niet
2024
Python voert het blok onder try uit. Gooit een regel daarin een ValueError,
dan slaat Python de rest van dat blok over en gaat verder met het blok onder
except. Daarna loopt het programma gewoon door. Gooit er niets, dan wordt het
except-blok overgeslagen.
Afhandelen¶
Het blok onder except handelt de fout af: het programma doet iets
zinnigs in plaats van te stoppen. Hier komen verzoeken om uitstel binnen, en een
ongeldig verzoek wordt gemeld en overgeslagen:
ali = Student("Ali Bakker", 2024)
requests = [1, -2, 0, 1]
for num_years in requests:
try:
ali.delay(num_years)
except ValueError:
print("verzoek overgeslagen:", num_years)
print(ali.year)
verzoek overgeslagen: -2
2026
De try staat binnen de lus, om één verzoek. Een ongeldig verzoek slaat dan
alleen dat verzoek over, en de lus gaat verder met het volgende.
Wie gooit en wie afvangt, zijn vaak verschillende plekken. delay weet dat -2
niet kan, maar niet wat er dan moet gebeuren. De code die delay aanroept, weet
dat wel: hier melden en doorgaan.
Afdwingen dat een subklasse een methode schrijft¶
In week 6 viel een subklasse die een methode niet overschreef, stilletjes terug
op de versie van de superklasse. Soms is er voor de superklasse geen zinnige
versie. Dan gooit haar methode een NotImplementedError:
class Person:
"""Iemand op de hogeschool. Elke subklasse moet role overschrijven."""
def __init__(self, name):
"""Maak een persoon met de gegeven naam."""
self.name = name
def role(self):
"""Gooit een NotImplementedError: een subklasse schrijft haar eigen versie."""
name = self.__class__.__name__
raise NotImplementedError(f"{name} moet role overschrijven")
class Teacher(Person):
"""Een docent."""
def role(self):
"""Geeft de rol van een docent."""
return "docent"
class Visitor(Person):
"""Een bezoeker, die vergeet role te overschrijven."""
print(Teacher("Jan Mulder").role())
print(Visitor("Eva de Jong").role())
docent
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[15], line 2
1 print(Teacher("Jan Mulder").role())
----> 2 print(Visitor("Eva de Jong").role())
Cell In[14], line 11, in Person.role(self)
8 def role(self):
9 """Gooit een NotImplementedError: een subklasse schrijft haar eigen versie."""
10 name = self.__class__.__name__
---> 11 raise NotImplementedError(f"{name} moet role overschrijven")
NotImplementedError: Visitor moet role overschrijven
Bij de docent draait role van Teacher. Visitor heeft geen eigen versie, dus
draait die van Person, en die gooit. De foutmelding noemt de klasse die het
vergat, dankzij self.__class__.__name__ uit week 6. De fout valt op zodra
role wordt aangeroepen, in plaats van dat het programma stilletjes iets
verkeerds doet.
Op een rij¶
Begrip |
Wat het is |
Hier |
|---|---|---|
magische methode |
een methode die Python zelf aanroept, bijvoorbeeld bij een operator |
|
operator overloading |
een operator eigen gedrag geven voor je eigen klasse |
|
exception |
het mechanisme waarmee Python een fout meldt |
|
een exception gooien |
met |
|
een exception afvangen |
met |
|
een fout afhandelen |
wat het |
het verzoek overslaan |
Opdrachten¶
Opdracht 1¶
Geef Student een methode __le__ voor <=, naast __lt__. Controleer met twee
studenten uit hetzelfde jaar dat a <= b True is en a < b False. Werkt
a >= b nu ook? Leg uit waarom.
Opdracht 2¶
Laat de constructor van Student een ValueError gooien als het startjaar vóór
1900 ligt. Schrijf daarna een lus die van een lijst jaren studenten maakt, en de
ongeldige jaren meldt en overslaat.
Opdracht 3¶
Na __eq__ geven a == b en a is b bij twee studenten met dezelfde naam en
hetzelfde jaar een ander antwoord. Wanneer wil je in een programma ==, en wanneer
is? Geef van beide een voorbeeld uit de studiegroep van week 5.