Basis¶
De eeuwigdurende kalender, met operatoren¶
In week 5 bouwde je de klasse Date. Een datum vergelijken of verschuiven gaat
daar met methoden: d.equals(d2), d.is_before(d2), d.add_n_days(3). Bij
getallen schrijf je daarvoor ==, < en +=, en dat leest makkelijker. In deze
opgave geef je Date die operatoren. Je schrijft daarvoor magische methoden:
methoden die Python zelf aanroept als je een operator gebruikt. Dat heet
operator overloading.
Daarna maak je af wat in week 5 openbleef. Iedereen kan nog steeds
Date(30, 2, 2021) maken, een datum die niet bestaat. Die weigert de constructor
voortaan met een exception. En tot slot lees je datums in die mensen zelf hebben
getypt, fouten en al, zonder dat je programma bij de eerste fout stopt.
De klasse Date¶
Je begint met de klasse Date zoals je haar in week 5 afmaakte, na stap 13.
Voer de cel hieronder uit. In deze cel voeg je in elke stap iets toe. Voer haar
daarna steeds opnieuw uit, en dan de testcel van die stap.
Heb je week 5 niet af, dan is dit ook de uitwerking van die week.
class Date:
"""Een datum: dag, maand en jaar, die van buiten alleen te lezen zijn."""
def __init__(self, day, month, year):
"""Maak een datum met de gegeven dag, maand en jaar."""
self._day = day
self._month = month
self._year = year
def __repr__(self):
"""Geeft de datum als string, zoals 02/12/2020."""
return f"{self.day:02d}/{self.month:02d}/{self.year:04d}"
def is_leap_year(self):
"""Geeft True als de datum in een schrikkeljaar valt."""
if self.year % 400 == 0:
return True
elif self.year % 100 == 0:
return False
elif self.year % 4 == 0:
return True
return False
def copy(self):
"""Geeft een nieuw object met dezelfde dag, maand en jaar."""
return Date(self.day, self.month, self.year)
def equals(self, d2):
"""Geeft True als self en d2 dezelfde kalenderdatum voorstellen."""
return self.year == d2.year and self.month == d2.month and self.day == d2.day
def is_before(self, d2):
"""Geeft True als self eerder valt dan d2."""
if self.year != d2.year:
return self.year < d2.year
if self.month != d2.month:
return self.month < d2.month
return self.day < d2.day
def is_after(self, d2):
"""Geeft True als self later valt dan d2."""
return not self.is_before(d2) and not self.equals(d2)
def tomorrow(self):
"""Verandert de datum in de dag erna."""
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
self._day += 1
if self.day > dim[self.month]:
self._day = 1
self._month += 1
if self.month > 12:
self._month = 1
self._year += 1
def yesterday(self):
"""Verandert de datum in de dag ervoor."""
if self.is_leap_year():
fdays = 29
else:
fdays = 28
dim = [0, 31, fdays, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
self._day -= 1
if self.day < 1:
self._month -= 1
if self.month < 1:
self._month = 12
self._year -= 1
self._day = dim[self.month]
def add_n_days(self, n):
"""Verandert de datum in n dagen later en drukt elke datum onderweg af."""
print(self)
for i in range(n):
self.tomorrow()
print(self)
def sub_n_days(self, n):
"""Verandert de datum in n dagen eerder en drukt elke datum onderweg af."""
print(self)
for i in range(n):
self.yesterday()
print(self)
def diff(self, d2):
"""Geeft het aantal dagen van d2 tot self; negatief als self eerder valt."""
self_copy = self.copy()
d2_copy = d2.copy()
count = 0
while self_copy.is_before(d2_copy):
self_copy.tomorrow()
count -= 1
while self_copy.is_after(d2_copy):
self_copy.yesterday()
count += 1
return count
def dow(self):
"""Geeft de dag van de week als Engelse string, zoals "Monday"."""
days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
return days[self.diff(Date(10, 10, 2010)) % 7]
@property
def day(self):
"""De dag, alleen om te lezen."""
return self._day
@property
def month(self):
"""De maand, alleen om te lezen."""
return self._month
@property
def year(self):
"""Het jaar, alleen om te lezen."""
return self._year
Wat je gaat maken¶
Stap |
Methode |
Maakt mogelijk |
|---|---|---|
1 |
|
|
2 |
|
|
3 |
|
|
4 |
- |
begrijpen waarom |
5 |
|
|
6 |
|
|
7 |
|
een datum die niet bestaat weigeren |
8 |
|
datums uit tekst inlezen, en de ongeldige afvangen |
Elke operator roept Python aan via een vaste methodenaam:
Je schrijft |
Python roept aan |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
De methoden die je al hebt, zoals equals en is_before, blijven staan. De
nieuwe methoden gebruiken ze.
Stap 1: __eq__(self, other)¶
Geeft True als other een Date is met dezelfde kalenderdatum als self.
Is other geen Date, dan geeft __eq__ False.
Hiermee verandert wat == bij een datum betekent. In week 5 vergeleek == bij
je eigen objecten de identiteit, net als is. Nu vergelijkt == de waarde:
Aanroep |
In week 5 |
Na deze stap |
|---|---|---|
|
|
|
|
|
|
|
|
|
is blijft dus de identiteit vergelijken. != hoef je niet te schrijven: Python
keert het antwoord van __eq__ om.
Hint
Begin met if not isinstance(other, Date): en geef in dat geval False
terug. Voor de rest heb je equals al.
d = Date(1, 1, 2100)
d2 = d.copy()
assert d == d2
assert d is not d2
assert not d == Date(2, 1, 2100)
assert d != Date(2, 1, 2100)
assert not d == "01/01/2100"
Stap 2: __lt__(self, other) en __gt__(self, other)¶
d < d2 geeft True als d eerder valt dan d2, en d > d2 als d later
valt. Is other geen Date, dan gooien ze een TypeError:
raise TypeError("een Date is alleen met een Date te vergelijken")
Aanroep |
Resultaat |
|---|---|
|
|
|
|
|
|
Waarom False bij == en een foutmelding bij <? Een datum en een string zijn
gewoon niet gelijk. Maar of een datum eerder valt dan een string, is een vraag
zonder antwoord.
Python kan d > d2 ook zonder __gt__ beantwoorden, door de vergelijking om
te draaien naar d2 < d. Je schrijft __gt__ hier toch, zodat > naast
is_after staat, zoals < naast is_before.
Hint
Schrijf de vergelijking niet opnieuw: is_before en is_after heb je al.
ny = Date(1, 1, 2021)
d = Date(2, 12, 2020)
assert d < ny
assert not ny < d
assert not d < d
assert ny > d
assert not d > ny
assert not d > d
Stap 3: __iadd__(self, n)¶
Verandert self in een datum n dagen later, en geeft self terug. n is een
integer, nooit negatief. Is n geen integer, dan gooit __iadd__ een
TypeError. isinstance werkt ook met een ingebouwd type:
isinstance(n, int) geeft True als n een integer is.
Voor |
Code |
Na |
|---|---|---|
|
|
|
|
|
|
add_n_days drukt elke datum onderweg af. __iadd__ doet dat niet: bij
d += 1318 wil je geen 1319 regels zien. Roep dus niet add_n_days aan, maar
tomorrow, in een for-lus. Zet return self aan het eind; waarom, zie je in
stap 4.
+= verandert het object zelf, net als bij een lijst. Wijst een tweede naam
hetzelfde object aan, dan zie je de verandering ook via die naam. De assertion
d2 is d hieronder laat dat zien.
De laatste regels van de test gebruiken try en except uit de opstap. Ze
controleren dat d += 1.5 een TypeError gooit: alleen dan wordt got_error
True. Gebruik je range(n) in de lus, dan gooit range bij 1.5 zelf al een
TypeError, ook zonder je controle met isinstance. De test ziet dus niet of
die controle er staat; kijk dat zelf na.
d = Date(28, 2, 2021)
d += 3
assert repr(d) == "03/03/2021"
d += 0
assert repr(d) == "03/03/2021"
d2 = d
d += 1318
assert repr(d) == "11/10/2024"
assert d2 is d
got_error = False
try:
d += 1.5
except TypeError:
got_error = True
assert got_error
Stap 4: waarom return self¶
Haal in __iadd__ de regel return self even weg, voer de cel met de klasse
opnieuw uit, en voer daarna deze code uit:
d = Date(28, 2, 2021)
d += 3
print(d)
Beantwoord in de cel hieronder, als commentaar:
Wat drukt de code af?
In de tabel bij Wat je gaat maken staat dat Python
d += nuitvoert alsd = d.__iadd__(n). Wat geeft__iadd__zonderreturn selfterug, en wat wijstddaarna dus aan?Is de datum zelf wel drie dagen verschoven?
Zet return self daarna terug, en voer de cel met de klasse opnieuw uit.
# jouw antwoorden
Stap 5: __isub__(self, n)¶
Het spiegelbeeld van __iadd__: verandert self in een datum n dagen eerder,
en geeft self terug. Is n geen integer, dan gooit __isub__ een TypeError.
Voor de laatste regels van de test geldt wat bij stap 3 staat.
Voor |
Code |
Na |
|---|---|---|
|
|
|
|
|
|
d = Date(3, 3, 2021)
d -= 3
assert repr(d) == "28/02/2021"
d = Date(1, 1, 2021)
d -= 366
assert repr(d) == "01/01/2020"
got_error = False
try:
d -= "drie"
except TypeError:
got_error = True
assert got_error
Stap 6: __sub__(self, other)¶
d - d2 geeft het aantal dagen tussen d en d2 als integer, precies zoals
d.diff(d2). Net als diff verandert - de twee datums niet. Is other geen
Date, dan gooit __sub__ een TypeError.
Aanroep |
Resultaat |
|---|---|
|
|
|
|
|
een |
Let op het verschil met stap 5. d - d2 verandert geen van beide datums: het
geeft een getal. d -= 5 verandert d zelf.
d = Date(2, 12, 2020)
d2 = Date(19, 7, 2021)
assert d2 - d == 229
assert d - d2 == -229
assert d - d == 0
assert repr(d) == "02/12/2020"
assert repr(d2) == "19/07/2021"
Voer daarna deze cel uit, en lees de laatste regel van de foutmelding:
d = Date(2, 12, 2020)
print(d - 5)
Stap 7: een datum die niet bestaat, weigeren¶
In week 5 schermde je de datum af: code van buiten de klasse kan dag, maand en
jaar niet meer veranderen. Eén deur bleef open, de constructor. Date(30, 2, 2021)
maakt nog steeds een datum die niet bestaat. Die deur sluit je nu.
Laat de constructor een ValueError gooien als de maand niet van 1 tot en met
12 is, of als de dag niet in die maand bestaat:
raise ValueError(f"{self} bestaat niet")
Aanroep |
Resultaat |
|---|---|
|
een datum: 2024 is een schrikkeljaar |
|
|
|
|
|
|
|
|
Hint
Zet eerst de drie attributen, zoals nu al gebeurt; dan kan de constructor
self.is_leap_year() gebruiken. Daarna heb je de lijst dim uit tomorrow
nodig. Controleer de maand vóór je dim[month] opzoekt: bij maand 13 bestaat
die index niet.
assert repr(Date(29, 2, 2024)) == "29/02/2024"
assert repr(Date(31, 12, 2021)) == "31/12/2021"
assert repr(Date(1, 1, 2000)) == "01/01/2000"
d = Date(28, 2, 2021)
d += 1
assert repr(d) == "01/03/2021"
got_error = False
try:
Date(0, 5, 2024)
except ValueError:
got_error = True
assert got_error
Voer daarna deze cel uit. Hij hoort een ValueError te geven:
d = Date(30, 2, 2021)
Stap 8: datums uit tekst inlezen¶
Op een aanmeldformulier typen mensen hun geboortedatum zelf in, en niet iedereen
doet dat goed. Schrijf een functie parse_dates(texts), buiten de klasse. Ze
krijgt een lijst strings in de vorm dag/maand/jaar en geeft een lijst
Date-objecten terug, één voor elke geldige string. Een string waar geen
datum van te maken is, slaat ze over, en ze drukt overgeslagen: af met die
string erachter.
String |
Wat er gebeurt |
|---|---|
|
wordt |
|
wordt |
|
overgeslagen: april heeft 30 dagen |
|
overgeslagen: geen schuine strepen |
Zo handel je de fouten af: het programma meldt ze en gaat door, in plaats van te stoppen bij de eerste.
Hint
day, month, year = text.split("/") zet de drie delen van de string in drie
namen. Zijn er geen drie delen, dan gooit Python een ValueError. Dat doet
int ook bij een deel dat geen getal is, en je constructor bij een datum die
niet bestaat. Eén try met except ValueError, binnen de lus, vangt dus alle
drie af.
Met de lijst in de test hieronder drukt parse_dates vijf regels af:
overgeslagen: 31/04/2024
overgeslagen: 29/02/2023
overgeslagen: 15/13/2024
overgeslagen: 00/05/2024
overgeslagen: 01-05-2024
# jouw oplossing
texts = [
"30/01/2024",
"31/04/2024",
"29/02/2024",
"29/02/2023",
"15/13/2024",
"00/05/2024",
"01-05-2024",
"1/5/2024",
]
dates = parse_dates(texts)
assert [repr(d) for d in dates] == ["30/01/2024", "29/02/2024", "01/05/2024"]
assert parse_dates([]) == []
Tot slot¶
Je Date werkt nu met de operatoren van Python, en er bestaat geen Date meer
die niet kan bestaan: de constructor weigert hem, en tomorrow en yesterday
maken van een bestaande datum altijd een bestaande. Wie een datum uit tekst
maakt, krijgt bij een fout een exception en beslist zelf wat ermee gebeurt.
In het practicum geef je wezens en partijen
operatoren, en laat je de stille terugvallen van week 5 en 6 plaatsmaken voor
exceptions. In de extra-opgave vergelijk je zetten van
Vier op een rij met <.