Opstap

Deze pagina bevat uitvoerbare code.

Operatoren en exceptions

Leerdoel: een eigen klasse laten werken met == en < door een magische methode te schrijven, een fout melden met raise, een exception afvangen met try/except, en een superklasse laten afdwingen dat een subklasse een methode overschrijft.

Deze opdrachten zijn kaal met opzet. Ze oefenen de handgrepen die je bij de basisopgave en het practicum van deze week nodig hebt, zodat je daar met het probleem bezig kunt zijn in plaats van met de syntaxis.

De opdrachten bouwen voort op de klassen Lamp en DimmableLamp uit de opstap van week 6. Voer de cel hieronder eerst uit. In deze cel voeg je in opdracht 2, 3 en 4 een methode toe; voer haar daarna steeds opnieuw uit, en dan de testcel van die opdracht.

class Lamp:
    """Een lamp die aan of uit staat."""

    def __init__(self, color):
        """Maak een lamp met de gegeven kleur; hij staat uit."""
        self.color = color
        self.on = False

    def __repr__(self):
        """Geeft de lamp als string, met de naam van de klasse en de kleur."""
        return f"{self.__class__.__name__}({self.color})"

    def switch(self):
        """Zet de lamp aan als hij uit staat, en uit als hij aan staat."""
        self.on = not self.on

    def describe(self):
        """Geeft een korte beschrijving, zoals lamp rood: uit."""
        if self.on:
            return f"lamp {self.color}: aan"
        return f"lamp {self.color}: uit"


class DimmableLamp(Lamp):
    """Een lamp die je kunt dimmen."""

    def __init__(self, color, level=100):
        """Maak een dimbare lamp; zonder level brandt hij op 100%."""
        super().__init__(color)
        self.level = level

    def describe(self):
        """Geeft de beschrijving van een lamp, met de helderheid erachter."""
        return super().describe() + f", {self.level}%"

Opdracht 1

Voorspel eerst wat deze code afdrukt, en voer haar daarna uit:

a = Lamp("rood")
b = Lamp("rood")
print(a == b)
print(a is b)

Beantwoord daarna als commentaar in de cel: waarom geeft a == b dit antwoord, terwijl de twee lampen dezelfde kleur hebben en allebei uit staan? Denk aan wat == in week 5 bij je eigen klassen vergeleek.

# voer de code hier uit

Opdracht 2

Je kunt zelf bepalen wat == bij een lamp betekent. Python vertaalt a == b naar de aanroep a.__eq__(b). Een methode als __eq__, die Python zelf aanroept, heet een magische methode. Je kent er al twee: __init__ en __repr__.

Geef Lamp een methode __eq__(self, other). Twee lampen zijn gelijk als ze dezelfde kleur hebben en allebei aan of allebei uit staan. Is other geen lamp, dan geeft __eq__ False.

Aanroep

Resultaat

Lamp("rood") == Lamp("rood")

True

Lamp("rood") == Lamp("blauw")

False

Lamp("rood") == "rood"

False: een string is geen lamp

Hint

Begin met if not isinstance(other, Lamp): en geef in dat geval False terug. isinstance ken je uit week 6. Daarna vergelijk je self.color met other.color, en self.on met other.on.

# schrijf __eq__ in de cel met de klasse Lamp, en voer die cel opnieuw uit
a = Lamp("rood")
b = Lamp("rood")
assert a == b
assert a is not b
assert not a == Lamp("blauw")
a.switch()
assert not a == b
assert a != b
assert not Lamp("rood") == "rood"

Kijk naar de tweede assertion: a == b is nu True, en a is b blijft False. == vergelijkt voortaan de waarde, is nog steeds de identiteit. Ook != werkt, zonder dat je het schrijft: Python keert het antwoord van __eq__ om.

Opdracht 3

Python vertaalt a < b naar a.__lt__(b), met lt van less than. Geef DimmableLamp een methode __lt__(self, other): een lamp is kleiner dan een andere als hij minder fel brandt.

Aanroep

Resultaat

DimmableLamp("wit", 30) < DimmableLamp("wit", 80)

True

DimmableLamp("wit", 80) < DimmableLamp("wit", 30)

False

Is other geen DimmableLamp, dan valt er niets te vergelijken. Gooi dan een exception, met deze regel:

raise TypeError("een DimmableLamp is alleen met een DimmableLamp te vergelijken")

raise gooit een exception: Python stopt met de methode en toont een foutmelding. TypeError is de soort fout voor een waarde van het verkeerde type. In opdracht 5 zie je wat raise precies doet.

Heb je __lt__, dan kan sorted een lijst lampen sorteren, want sorted vergelijkt met <.

# schrijf __lt__ in de klasse DimmableLamp, en voer de cel opnieuw uit
dim = DimmableLamp("wit", 30)
bright = DimmableLamp("wit", 80)
assert dim < bright
assert not bright < dim
assert not dim < DimmableLamp("geel", 30)
lamps = [DimmableLamp("rood", 60), DimmableLamp("wit", 10), DimmableLamp("geel", 100)]
assert [lamp.level for lamp in sorted(lamps)] == [10, 60, 100]

Opdracht 4

Een helderheid van 150% of -5% bestaat niet. Laat de constructor van DimmableLamp zo’n lamp weigeren: is level kleiner dan 0 of groter dan 100, dan gooit hij een ValueError. Dat is de soort fout voor een waarde van het goede type die toch niet kan.

raise ValueError("de helderheid moet van 0 tot en met 100 zijn")

Aanroep

Resultaat

DimmableLamp("wit", 0)

een lamp met helderheid 0

DimmableLamp("wit", 150)

een ValueError

# pas de constructor van DimmableLamp aan, en voer de cel opnieuw uit
assert DimmableLamp("wit", 0).level == 0
assert DimmableLamp("wit", 100).level == 100
assert DimmableLamp("wit").level == 100
assert DimmableLamp("wit", 30).describe() == "lamp wit: uit, 30%"

Opdracht 5

Voorspel eerst wat deze code doet, en voer haar daarna uit:

lamp = DimmableLamp("wit", 150)
print("gelukt")

Beantwoord als commentaar in de cel: wordt gelukt afgedrukt? Wat staat er op de laatste regel van de foutmelding, en waar komt die tekst vandaan?

# voer de code hier uit

Opdracht 6

Een exception hoeft je programma niet te laten stoppen. Met try en except vang je haar af:

try:
    lamp = DimmableLamp("wit", 150)
    print("gelukt")
except ValueError:
    print("geen geldige helderheid")
print("klaar")

Python voert het blok onder try uit. Gooit een regel daarin een ValueError, dan slaat Python de rest van dat blok over en voert het blok onder except uit. Daarna gaat het programma gewoon verder. Gooit niets een exception, dan wordt het except-blok overgeslagen.

Voorspel eerst wat de code afdrukt, en voer haar daarna uit. Verander daarna 150 in 50: wat drukt ze dan af?

# voer de code hier uit

Opdracht 7

Schrijf een functie make_lamps(color, levels). Ze geeft een lijst dimbare lampen in de kleur color terug, één voor elke helderheid in de lijst levels. Een helderheid die de constructor weigert, slaat de functie over, en ze drukt overgeslagen: af met die helderheid erachter. Zo handel je de fout af: het programma doet iets zinnigs in plaats van te stoppen.

Aanroep

Geeft terug

Drukt af

make_lamps("wit", [30, 150, 0])

lampen met helderheid 30 en 0

overgeslagen: 150

make_lamps("rood", [])

[]

niets

Hint

Zet de try binnen de lus, om het maken van één lamp. Dan gaat de lus na een fout verder met de volgende helderheid.

# jouw oplossing
lamps = make_lamps("wit", [30, 150, 0, -5, 100])
assert [lamp.level for lamp in lamps] == [30, 0, 100]
assert make_lamps("rood", []) == []

Opdracht 8

In week 6 viel een subklasse die een methode niet overschreef, stilletjes terug op de versie van de superklasse. Een superklasse kan ook afdwingen dat elke subklasse een methode zelf schrijft: haar eigen versie gooit dan een NotImplementedError.

Voorspel eerst wat deze code doet, en voer haar daarna uit:

class LightSource:
    """Iets dat licht geeft. Elke subklasse moet describe overschrijven."""

    def describe(self):
        """Gooit een NotImplementedError: een subklasse moet dit zelf schrijven."""
        name = self.__class__.__name__
        raise NotImplementedError(f"{name} moet describe overschrijven")


class Candle(LightSource):
    """Een kaars."""

    def describe(self):
        """Geeft een korte beschrijving van de kaars."""
        return "kaars"


class Torch(LightSource):
    """Een zaklamp, die vergeet describe te overschrijven."""


print(Candle().describe())
print(Torch().describe())

Beantwoord daarna als commentaar in de cel: welke describe draait er bij de kaars, en welke bij de zaklamp? Welke klassenaam staat in de foutmelding, en waarom die?

# voer de code hier uit

Opdracht 9

DimmableLamp heeft __lt__ voor <, maar geen methode voor >. Voorspel eerst wat deze code doet, en voer haar daarna uit:

dim = DimmableLamp("wit", 30)
bright = DimmableLamp("wit", 80)
print(bright > dim)
print(dim > bright)

Beantwoord als commentaar in de cel: hoe kan Python bright > dim beantwoorden zonder methode voor >? Denk aan wat bright > dim betekent als je het omdraait.

# voer de code hier uit

Opdracht 10

Voorspel eerst wat deze code doet, en voer haar daarna uit:

print(DimmableLamp("wit", 30) == "wit")
print(DimmableLamp("wit", 30) < Lamp("rood"))

Beantwoord als commentaar in de cel: waarom geeft de eerste regel een antwoord, en de tweede een foutmelding? Welke methode gooit die exception?

# voer de code hier uit