Een Date klasse

Deze pagina bevat uitvoerbare code.

Data

  • dag

  • maand

  • jaar

class Date:
    """Een eigen datatype: een klasse die datums opslaat en bewerkt."""

    def __init__(self, day, month, year):
        """De constructor voor objecten van het type Date."""
        self.day = day
        self.month = month
        self.year = year

    def __repr__(self):
        """Geeft een string die het object self voorstelt."""
        return f"{self.day:02d}/{self.month:02d}/{self.year:04d}"

    def is_leap_year(self):
        """Geeft True als self, het aanroepende object, in een schrikkeljaar valt."""
        if self.year % 400 == 0:
            return True
        if self.year % 100 == 0:
            return False
        if self.year % 4 == 0:
            return True
        return False

Stap voor stap

class Date:
    """Een eigen datatype: een klasse die datums opslaat en bewerkt."""

Met het sleutelwoord class begint de definitie van een nieuw type: Date.

Let op de dubbele punt: alles wat volgt, springt in, omdat het bij de klasse hoort.

    def __init__(self, day, month, year):
        """De constructor voor objecten van het type Date."""
        self.day = day
        self.month = month
        self.year = year

Dit is de constructor voor Date-objecten. Hier krijgen de attributen van een nieuw object hun beginwaarde, uit de argumenten.

d = Date(11, 12, 2013)

De attributen zijn de gegevens die elk Date-object heeft, elk object met zijn eigen waarden.

    def __repr__(self):
        """Geeft een string die het object self voorstelt."""
        return f"{self.day:02d}/{self.month:02d}/{self.year:04d}"

Dit is de string die een Date-object voorstelt: zo weet Python hoe het object afgedrukt moet worden.

f"strings"

Strings formatteren, handig!

klassen = 12
studenten = 25
f"{klassen} klassen met {studenten} studenten is in totaal {klassen * studenten}"
'12 klassen met 25 studenten is in totaal 300'
x = 1
f"{x:02d}"
'01'

02d maakt van de integer x een string van minstens 2 posities, en vult die zo nodig links aan met nullen (“zero-padding”).

    def is_leap_year(self):
        """Geeft True als self, het aanroepende object, in een schrikkeljaar valt."""
        if self.year % 400 == 0:
            return True
        if self.year % 100 == 0:
            return False
        if self.year % 4 == 0:
            return True
        return False

Waarom wordt self gebruikt en niet d?

self

self is het object waarvoor de methode wordt aangeroepen. Bij wd.is_leap_year() is self het object wd. Net als bij de constructor vult Python self zelf in: delay(self, num_years) uit het vorige college roep je aan als student_b.delay(1), en dan is self het object student_b.

Misschien heb je al kennisgemaakt met andere programmeertalen en zal je herkennen dat self overeenkomt met wat je kent als this in deze talen (bijvoorbeeld in Java of C++). Guido van Rossum, de maker van Python, heeft uitgelegd waarom hij niet voor this koos, en noemt het een daad van speels verzet.

Het is hier misschien ook van belang om stil te staan bij het feit dat programmeertalen niet zomaar zijn ontstaan, het is mensenwerk en wij bouwen weer voort op het werk van anderen!

wd = Date(11, 12, 2013)
print(wd)
11/12/2013
wd.is_leap_year()
False
d = Date(11, 12, 2020)
print(d)
11/12/2020
d.is_leap_year()
True

Quiz

De anatomie van een klasse

class Date:                                                        # (1)
    def __init__(self, day, month, year):                          # (2)
        self.day = day                                             # (3)
        self.month = month                                         # (4)
        self.year = year                                           # (5)

    def __repr__(self):                                            # (6)
        return f"{self.day:02d}/{self.month:02d}/{self.year:04d}"  # (7)

    def is_leap_year(self):                                        # (8)
        if self.year % 400 == 0: return True
        if self.year % 100 == 0: return False
        if self.year % 4 == 0: return True
        return False
                                                                   # (9)
wd = Date(11, 12, 2013)                                            # (10)
ny = Date(1, 1, 2021)                                              # (11)

Probeer de volgende onderdelen aan te wijzen:

  • class keyword

  • waar eindigt de klassedefinitie?

  • objecten die worden aangemaakt, 2 totaal

  • methoden, 3 totaal

  • constructor

  • attributen, 3 totaal

  • welke methode bepaalt wat een Date afdrukt?

Oplossing

  • class keyword: (1)

  • waar eindigt de klassedefinitie?: (9)

  • objecten die worden aangemaakt, 2 totaal: (10, 11)

  • methoden, 3 totaal: (2, 6, 8)

  • constructor: (2)

  • attributen, 3 totaal: (3, 4, 5)

  • welke methode bepaalt wat een Date afdrukt?: (6)

Waarde en identiteit

Maak twee datums voor dezelfde dag, en druk ze allebei af:

wd = Date(11, 12, 2013)
wd2 = Date(11, 12, 2013)
print(wd)
print(wd2)
11/12/2013
11/12/2013

Vergelijk ze nu met ==:

wd == wd2
False

Twee keer dezelfde datum, en toch zegt Python False. wd en wd2 zijn twee objecten. Elke aanroep Date(...) maakt een nieuw object, ook als de argumenten dezelfde zijn. De twee objecten hebben dezelfde dag, maand en jaar, maar het blijven er twee.

Het kan ook andersom: één object met twee namen. Neem de klasse Student uit het vorige college, de versie met _year:

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

Maak student_b en geef hem een tweede naam, jasper. Stel dan de studie uit via jasper, en druk student_b af:

student_b = Student("Jasper Klein", 2021)
jasper = student_b
jasper.delay(1)
print(student_b)
naam: Jasper Klein, startjaar: 2022

Je hebt de studie niet via student_b uitgesteld, en toch is zijn startjaar veranderd. jasper = student_b maakt geen nieuwe student. Een variabele bevat het object niet zelf, maar een verwijzing ernaar: ze wijst het object aan. Na jasper = student_b wijzen beide namen hetzelfde object aan, dus wat je via de ene naam verandert, zie je via de andere.

Code

Wat er gebeurt

jasper = student_b

geen nieuw object: jasper is een tweede naam voor hetzelfde object

wd2 = Date(11, 12, 2013)

een nieuw object, met dezelfde datum als wd

De begrippen op een rij:

Begrip

Wat het is

Hierboven

waarde

wat er in een object staat: de waarden van zijn attributen

wd en wd2 hebben dezelfde waarde

identiteit

welk object het is

jasper en student_b hebben dezelfde identiteit: het is één object

verwijzing

wat een variabele bevat: ze wijst een object aan

jasper en student_b bevatten een verwijzing naar hetzelfde object

In Programmeren 1 zag je al dat twee namen dezelfde lijst kunnen aanwijzen, in het college More on Lists. Bij je eigen objecten werkt dat net zo.

is en ==

Met is vraag je naar de identiteit. a is b geeft True als a en b hetzelfde object aanwijzen, en anders False:

print(jasper is student_b)
print(wd is wd2)
True
False

Het omgekeerde is is not: wd is not wd2 geeft True.

En ==? Bij je eigen objecten, van een klasse die je zelf schrijft, vergelijkt == of het hetzelfde object is: de identiteit, net als is. Daarom gaf wd == wd2 hierboven False. Dat geldt alleen voor je eigen objecten. Bij getallen, strings en lijsten vergelijkt == de waarde, zoals je gewend bent.

Wil je bij je eigen objecten weten of twee namen hetzelfde object aanwijzen, schrijf dan is: dat zegt wat je bedoelt. Wil je de waarde vergelijken, dan heb je een eigen methode nodig. In de basisopgave krijg je daarvoor de methode equals.

None bestaat maar één keer. Of een variabele None aanwijst, vraag je daarom ook met is:

best = None
print(best is None)
best = student_b
print(best is None)
True
False

Identiteit bij compositie

In het vorige college gaf de studiegroep een ander antwoord nadat je student_b.delay(1) buiten de groep om had aangeroepen. Nu kun je zeggen waarom. group.add(student_b) zette een verwijzing naar Jasper in de groep, geen kopie. Het lid in de groep en student_b hebben dezelfde identiteit: het is hetzelfde object als daarbuiten. Een object dat andere objecten als onderdeel heeft, bewaart dus verwijzingen naar die objecten.

Opdracht

Werk met de klasse Student van hierboven.

  1. Maak een student a, met een naam die je zelf kiest en startjaar 2024. Maak een tweede naam b voor dezelfde student. Maak ook een student c, met dezelfde naam en hetzelfde startjaar als a.

  2. Voorspel wat a is b, a is c en a == c geven. Controleer je voorspelling met print.

  3. Stel de studie een jaar uit via b. Voorspel het startjaar van a en dat van c, en controleer je voorspelling met print.

  4. Leg in één zin uit waarom het startjaar van a verandert en dat van c niet.