Overerving

Deze pagina bevat uitvoerbare code.

Voortbouwen op een klasse die er al is

Waar we waren

In week 5 schreef je de klasse Student, met een afgeschermd startjaar. Hier is ze weer, met één methode erbij: graduation_year, het jaar waarin de student naar verwachting afstudeert. Een voltijdstudent doet er vier jaar over.

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

    def graduation_year(self):
        """Geeft het jaar waarin de student naar verwachting afstudeert."""
        return self._year + 4
student_a = Student("Sanne de Wit", 2024)
print(student_a)
print(student_a.graduation_year())
naam: Sanne de Wit, startjaar: 2024
2028

Een deeltijdstudent

Een deeltijdstudent studeert naast zijn werk. Hij heeft een naam en een startjaar, hij kan zijn studie uitstellen, en hij wordt net zo afgedrukt. Alleen doet hij er langer over: zes jaar in plaats van vier.

Je zou de klasse Student kunnen kopiëren en er PartTimeStudent van maken. Dan staan __init__, delay en year twee keer in je programma. Zit er een fout in delay, dan moet je hem op twee plekken herstellen, en vergeet je er een, dan gedragen de twee soorten studenten zich ineens anders.

Een subklasse

Het kan ook zonder kopiëren. Je schrijft achter de naam van de nieuwe klasse, tussen haakjes, de klasse waarop ze voortbouwt:

class PartTimeStudent(Student):
    """Een deeltijdstudent, die naast zijn werk studeert."""

Deze klasse bevat alleen een docstring. Toch kan een deeltijdstudent alles wat een student kan:

student_b = PartTimeStudent("Ali Bakker", 2024)
print(student_b)
student_b.delay(1)
print(student_b.year)
naam: Ali Bakker, startjaar: 2024
2025

PartTimeStudent("Ali Bakker", 2024) roept de constructor van Student aan, print gebruikt __repr__ van Student, en delay en year komen ook uit Student. Een deeltijdstudent is een student, en krijgt daarom alles mee wat een student heeft.

De begrippen op een rij:

Begrip

Wat het is

Hier

overerving

een klasse krijgt de attributen en methoden van een andere klasse mee

PartTimeStudent erft van Student

subklasse

de klasse die erft

PartTimeStudent

superklasse

de klasse waarvan geërfd wordt

Student

Een methode overschrijven

Eén ding klopt nog niet:

print(student_b.graduation_year())
2029

Een deeltijdstudent doet er zes jaar over, geen vier. De subklasse krijgt daarom een eigen versie van graduation_year:

class PartTimeStudent(Student):
    """Een deeltijdstudent, die naast zijn werk studeert."""

    def graduation_year(self):
        """Geeft het verwachte afstudeerjaar: na zes jaar."""
        return self.year + 6
student_b = PartTimeStudent("Ali Bakker", 2024)
print(student_b.graduation_year())
print(student_a.graduation_year())
2030
2028

PartTimeStudent overschrijft de methode graduation_year van Student (in het Engels: override). Bij een deeltijdstudent draait nu de versie van PartTimeStudent, bij een gewone student nog steeds die van Student. Python zoekt een methode eerst in de klasse van het object zelf, en pas als ze daar niet staat in de superklasse.

super()

Zes jaar is “twee jaar langer dan voltijd”. Staat dat zo in de code, dan klopt een deeltijdstudent ook nog als de voltijdse studieduur ooit verandert. Met super() roep je de versie van de superklasse aan:

class PartTimeStudent(Student):
    """Een deeltijdstudent, die naast zijn werk studeert."""

    def graduation_year(self):
        """Geeft het verwachte afstudeerjaar: twee jaar later dan voltijd."""
        return super().graduation_year() + 2
student_b = PartTimeStudent("Ali Bakker", 2024)
print(student_b.graduation_year())
2030

super().graduation_year() draait graduation_year van Student, voor hetzelfde object: hier 2028. De subklasse zegt alleen wat er anders is: twee jaar erbij.

De constructor uitbreiden

Een uitwisselingsstudent komt een tijdje van een andere universiteit. Hij heeft alles wat een student heeft, en één attribuut erbij: zijn eigen universiteit. De subklasse krijgt daarom een eigen constructor, en die roept eerst de constructor van Student aan:

class ExchangeStudent(Student):
    """Een uitwisselingsstudent, die van een andere universiteit komt."""

    def __init__(self, name, year, university):
        """Maak een uitwisselingsstudent van de gegeven universiteit."""
        super().__init__(name, year)
        self.university = university

    def __repr__(self):
        """Geeft de student als string, met de universiteit erbij."""
        return super().__repr__() + ", van: " + self.university
student_c = ExchangeStudent("Lotte Smit", 2025, "Gent")
print(student_c)
print(student_c.graduation_year())
naam: Lotte Smit, startjaar: 2025, van: Gent
2029

super().__init__(name, year) laat Student doen wat Student al kan: name en _year een beginwaarde geven. Daarna zet de constructor van ExchangeStudent het nieuwe attribuut. Ook __repr__ bouwt voort op de versie van Student, via super().__repr__().

Als super().__init__ ontbreekt

Een constructor in een subklasse overschrijft de constructor van de superklasse. Roept hij super().__init__ niet aan, dan draait de constructor van Student dus helemaal niet:

class ForgetfulStudent(Student):
    """Een subklasse waarvan de constructor iets vergeet."""

    def __init__(self, name, year, university):
        """Maak een student, maar zonder de constructor van Student."""
        self.university = university


f = ForgetfulStudent("Tom Jansen", 2024, "Gent")
print(f)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[12], line 10
      6         self.university = university
      7 
      8 
      9 f = ForgetfulStudent("Tom Jansen", 2024, "Gent")
---> 10 print(f)

Cell In[1], line 11, in Student.__repr__(self)
      9     def __repr__(self):
     10         """Geeft de student als string, om af te drukken."""
---> 11         return "naam: " + self.name + ", startjaar: " + str(self._year)

AttributeError: 'ForgetfulStudent' object has no attribute 'name'

__repr__ van Student leest self.name, en dat attribuut heeft niemand een waarde gegeven. Python meldt een AttributeError, en pas op het moment dat het attribuut nodig is: het object maken ging nog goed.

Een standaardwaarde voor een parameter

Een parameter kan een standaardwaarde krijgen: een waarde die hij krijgt als je het argument bij de aanroep weglaat. Je schrijft haar met = in de signatuur:

def greet(name, greeting="Hallo"):
    """Geeft een begroeting voor name, standaard met Hallo."""
    return greeting + ", " + name + "!"


print(greet("Lotte"))
print(greet("Lotte", "Goedemorgen"))
Hallo, Lotte!
Goedemorgen, Lotte!

Geef je het argument mee, dan telt wat je meegeeft. Laat je het weg, dan geldt de standaardwaarde. Parameters met een standaardwaarde staan achteraan: eerst de parameters die je altijd moet meegeven, dan die je mag weglaten.

Een uitwisselingsstudent blijft meestal één semester, vijf maanden. Dat kan een standaardwaarde zijn:

class ExchangeStudent(Student):
    """Een uitwisselingsstudent, die van een andere universiteit komt."""

    def __init__(self, name, year, university, months=5):
        """Maak een uitwisselingsstudent; zonder months blijft hij één semester."""
        super().__init__(name, year)
        self.university = university
        self.months = months

    def __repr__(self):
        """Geeft de student als string, met de universiteit erbij."""
        return super().__repr__() + ", van: " + self.university
student_c = ExchangeStudent("Lotte Smit", 2025, "Gent")
student_d = ExchangeStudent("Noor Visser", 2025, "Lissabon", 10)
print(student_c.months)
print(student_d.months)
5
10

Op een rij

Begrip

Wat het is

Hier

overerving

een klasse krijgt de attributen en methoden van een andere klasse mee

class PartTimeStudent(Student):

subklasse

de klasse die erft

PartTimeStudent, ExchangeStudent

superklasse

de klasse waarvan geërfd wordt

Student

overschrijven

een subklasse geeft een methode een eigen versie

graduation_year in PartTimeStudent

super()

roept de versie van de superklasse aan

super().__init__(name, year)

standaardwaarde

de waarde van een parameter als het argument ontbreekt

months=5

Een subklasse schrijft alleen wat anders is. Wat ze niet zelf schrijft, erft ze.

Opdrachten

Opdracht 1

Schrijf een subklasse HonoursStudent van Student: een student die een extra honoursprogramma volgt. Haar constructor krijgt naast name en year een argument track, de naam van het programma, en slaat dat op in self.track. Gebruik super().__init__.

Opdracht 2

Geef HonoursStudent een eigen __repr__, die de tekst van Student gebruikt met het programma erachter, zoals naam: Sanne de Wit, startjaar: 2024, honours: data. Schrijf de tekst van Student niet opnieuw.

Opdracht 3

Geef de parameter track de standaardwaarde "algemeen". Controleer dat HonoursStudent("Sanne de Wit", 2024).track dan "algemeen" is.

Opdracht 4

Voorspel eerst, en controleer daarna: verander in Student de 4 in graduation_year in een 3, en voer alle cellen vanaf de klasse Student opnieuw uit, in volgorde: een subklasse bouwt voort op de klasse Student die er stond toen de subklasse werd gemaakt. Welk afstudeerjaar geeft PartTimeStudent("Ali Bakker", 2024) nu, met de versie die super() gebruikt? En wat had de versie met self.year + 6 gegeven?