Polymorfisme en duck typing¶
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: |
de subklasse krijgt alles van |
een studiegroep heeft studenten als onderdeel |
compositie: een attribuut met objecten erin |
|
een toehoorder gedraagt zich op één punt als een student |
duck typing: dezelfde methoden en attributen |
|
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 |
|
duck typing |
een object doet mee omdat het de methoden en attributen heeft die worden gebruikt, niet omdat het van een bepaalde klasse is |
|
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.