Operatoren en exceptions

Deze pagina bevat uitvoerbare code.

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

a == b

a.__eq__(b)

a < b

a.__lt__(b)

a <= b

a.__le__(b)

a + b

a.__add__(b)

a - b

a.__sub__(b)

a * b

a.__mul__(b)

a += b

a = a.__iadd__(b)

__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

__eq__

geeft False

een student en een string zijn gewoon niet gelijk

__lt__ en de andere

gooit een TypeError

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

ValueError

het type klopt, de waarde niet: int("hallo"), of delay(-2)

TypeError

het type klopt niet: "a" + 1, of een student vergelijken met een string

AttributeError

een attribuut of methode die het object niet heeft

NotImplementedError

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

__eq__ bij ==

operator overloading

een operator eigen gedrag geven voor je eigen klasse

== en < bij Student

exception

het mechanisme waarmee Python een fout meldt

ValueError, TypeError

een exception gooien

met raise een fout melden en de methode stoppen

raise ValueError(...) in delay

een exception afvangen

met try en except zorgen dat een gegooide exception het programma niet stopt

except ValueError:

een fout afhandelen

wat het except-blok doet: iets zinnigs in plaats van stoppen

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.