Polymorfisme en duck typing

Deze pagina bevat uitvoerbare code.

Eén aanroep, ander gedrag

Waar we waren

De klassen uit het vorige college: Student, de deeltijdstudent en de uitwisselingsstudent. Daarnaast de studiegroep uit week 5, die Student-objecten als onderdeel heeft.

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


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


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


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

Eén aanroep, ander gedrag

Zet drie soorten studenten in één lijst, en vraag elk naar zijn afstudeerjaar:

students = [
    Student("Sanne de Wit", 2024),
    PartTimeStudent("Ali Bakker", 2024),
    ExchangeStudent("Lotte Smit", 2024, "Gent"),
]
for student in students:
    print(student)
    print("  afstuderen in", student.graduation_year())
naam: Sanne de Wit, startjaar: 2024
  afstuderen in 2028
naam: Ali Bakker, startjaar: 2024
  afstuderen in 2030
naam: Lotte Smit, startjaar: 2024, van: Gent
  afstuderen in 2028

In de lus staan twee aanroepen: print(student) en student.graduation_year(). Welke versie er draait, hangt af van het object. Bij de deeltijdstudent draait graduation_year van PartTimeStudent, bij de uitwisselingsstudent __repr__ van ExchangeStudent, en bij de rest die van Student.

Dat heet polymorfisme: dezelfde aanroep geeft ander gedrag, afhankelijk van het object. De lus hoeft niet te weten welke soort student ze voor zich heeft.

De studiegroep hoeft niets te weten

Aan StudyGroup is niets veranderd sinds week 5. Toch kan ze elke soort student bevatten:

group = StudyGroup()
for student in students:
    group.add(student)
for student in group.starting_in(2024):
    print(student)
naam: Sanne de Wit, startjaar: 2024
naam: Ali Bakker, startjaar: 2024
naam: Lotte Smit, startjaar: 2024, van: Gent

starting_in vraagt elk lid naar zijn year. Elke subklasse van Student heeft year geërfd, dus elk lid kan die vraag beantwoorden. Een nieuwe soort student schrijven kan dus zonder StudyGroup aan te passen.

Duck typing

Bij de colleges zit soms een toehoorder: iemand die meeluistert zonder student te zijn. Een toehoorder heeft geen afstudeerjaar en kan zijn studie niet uitstellen, dus een subklasse van Student is hij niet. Hij heeft wel een naam en een jaar waarin hij begon:

class Auditor:
    """Een toehoorder: volgt de colleges, maar is geen student."""

    def __init__(self, name, year):
        """Maak een toehoorder die in het gegeven jaar begon."""
        self.name = name
        self._year = year

    @property
    def year(self):
        """Het jaar waarin de toehoorder begon, alleen om te lezen."""
        return self._year

    def __repr__(self):
        """Geeft de toehoorder als string, om af te drukken."""
        return "toehoorder: " + self.name + ", sinds: " + str(self._year)

Auditor erft van niets. Kan een toehoorder toch in een studiegroep?

group.add(Auditor("Mila de Boer", 2024))
for member in group.starting_in(2024):
    print(member)
naam: Sanne de Wit, startjaar: 2024
naam: Ali Bakker, startjaar: 2024
naam: Lotte Smit, startjaar: 2024, van: Gent
toehoorder: Mila de Boer, sinds: 2024

Het werkt. starting_in gebruikt van elk lid alleen year, en een toehoorder heeft year. Van welke klasse een object is, maakt voor starting_in niet uit.

Dat heet duck typing, naar het spreekwoord als het loopt als een eend en kwaakt als een eend, dan is het een eend. Een object hoeft niet van een bepaalde klasse te zijn; het hoeft alleen de methoden en attributen te hebben die worden gebruikt. Hier is dat de property year.

Wat duck typing niet belooft

Een toehoorder heeft alleen wat starting_in nodig heeft. Vraag je hem iets wat alleen een student kan, dan gaat het mis:

members = [Student("Sanne de Wit", 2024), Auditor("Mila de Boer", 2024)]
for member in members:
    print(member.graduation_year())
2028
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[6], line 3
      1 members = [Student("Sanne de Wit", 2024), Auditor("Mila de Boer", 2024)]
      2 for member in members:
----> 3     print(member.graduation_year())

AttributeError: 'Auditor' object has no attribute 'graduation_year'

De student drukt nog een jaar af; bij de toehoorder komt er een AttributeError. Niemand controleert vooraf of een object de methoden heeft die later nodig zijn. De fout komt pas op het moment van de aanroep, en dat kan ver weg zijn van de plek waar de toehoorder in de lijst werd gezet.

Overerving of compositie

Je hebt nu drie manieren gezien waarop klassen met elkaar te maken hebben:

Hoe ze samenhangen

In Python

Hier

een deeltijdstudent is een student

overerving: class PartTimeStudent(Student):

de subklasse krijgt alles van Student mee

een studiegroep heeft studenten als onderdeel

compositie: een attribuut met objecten erin

self._students in StudyGroup

een toehoorder gedraagt zich op één punt als een student

duck typing: dezelfde methoden en attributen

year in Auditor

Waarom is StudyGroup geen subklasse van Student? Dan zou een groep een naam en een startjaar erven, en een graduation_year en een delay. Een groep die zijn studie uitstelt, betekent niets. Een studiegroep is geen student; ze heeft studenten.

Een vraag die helpt bij het kiezen: is het nieuwe ding een soort van het oude, zodat alles wat voor het oude geldt ook voor het nieuwe geldt? Dan past overerving. Heeft het nieuwe ding zulke objecten als onderdeel? Dan past compositie.

Op een rij

Begrip

Wat het is

Hier

polymorfisme

dezelfde aanroep geeft ander gedrag, afhankelijk van het object

student.graduation_year() in de lus

duck typing

een object doet mee omdat het de methoden en attributen heeft die worden gebruikt, niet omdat het van een bepaalde klasse is

Auditor in StudyGroup

Opdrachten

Opdracht 1

Schrijf een functie latest_graduation(students) die het hoogste afstudeerjaar uit een lijst studenten teruggeeft. Test haar met een lijst waarin alle drie de soorten studenten staan. Welke graduation_year draait er bij elk element?

Opdracht 2

Zet een toehoorder in de lijst van opdracht 1. Wat gebeurt er, en op welke regel? Wat zou je moeten toevoegen, en aan welke klasse, om het te laten werken? Is dat een goed idee, voor iemand die niet afstudeert?

Opdracht 3

Een docent heeft een naam en een jaar waarin hij begon, net als een student. Moet een klasse Teacher een subklasse van Student worden, zodat ze die attributen erft? Leg je keuze uit in twee zinnen.