Basis

Deze pagina bevat uitvoerbare code.

Een kassabon

Bij de kassa van de supermarkt gaat alles over dezelfde band, maar niet alles kost op dezelfde manier geld. Brood heeft een vaste prijs. Kaas betaal je per kilo, dus de prijs hangt af van het gewicht. Koffie in de aanbieding kost minder dan de gewone prijs. En voor een lege fles krijg je statiegeld terug. De kassabon telt het allemaal op, zonder dat de bon hoeft te weten hoe elke prijs tot stand kwam.

In deze opgave schrijf je die kassabon. Elke soort product wordt een subklasse van één klasse Product, en de bon vraagt elk product alleen naar zijn prijs.

De regels

Alle prijzen zijn in centen, als integer: 289 is € 2,89. Met centen reken je exact; met euro’s als floating-point getal kan 0.1 + 0.2 al 0.30000000000000004 geven.

Soort

Voorbeeld

Prijs in centen

gewoon product

Product("brood", 289)

289

gewogen product: een prijs per kilo en een gewicht in gram

WeighedProduct("kaas", 1450, 400)

1450 * 400 // 1000 is 580

product in de aanbieding: een prijs en een kortingspercentage

DiscountedProduct("koffie", 599, 25)

599 - 599 * 25 // 100 is 450

statiegeld: een bedrag dat je terugkrijgt

Deposit("fles", 25)

-25

Delen gebeurt met // en rondt naar beneden af op hele centen: bij een gewogen product de prijs, bij een product in de aanbieding de korting. De koffie kost dus 450 en niet 449: de korting van 149,75 cent wordt 149.

De klasse Product

Je begint met deze klasse. Voer de cel uit. Een product heeft een naam en een prijs, en drukt zichzelf af als een regel op de kassabon, zoals brood: 289.

class Product:
    """Een product met een naam en een prijs in centen."""

    def __init__(self, name, cents):
        """Maak een product met de gegeven naam en prijs in centen."""
        self.name = name
        self._cents = cents

    def price(self):
        """Geeft de prijs in centen."""
        return self._cents

    def __repr__(self):
        """Geeft het product als regel op de kassabon, zoals brood: 289."""
        return f"{self.name}: {self.price()}"

Kijk goed naar __repr__: die leest niet self._cents, maar roept de methode self.price() aan. Dat wordt in stap 3 belangrijk.

Wat je gaat maken

Stap

Wat

Doet

1

WeighedProduct

een subklasse van Product, met een gewicht erbij

2

WeighedProduct.price

de prijs van een gewogen product

3

-

uitleggen waarom __repr__ meeverandert

4

DiscountedProduct

een product in de aanbieding, via super().price()

5

Receipt

een kassabon die producten optelt

6

most_expensive

het duurste item op de bon

7

Deposit

statiegeld, zonder subklasse van Product te zijn

8

-

beoordelen wat iets nodig heeft om op de bon te mogen

9

Receipt.__repr__

de bon afdrukken

10

een eigen subklasse

zelf een soort product ontwerpen en testen

11

gewogen én in de aanbieding

een combinatie van twee soorten

Stap 1: de klasse WeighedProduct

Een gewogen product is een product: het heeft een naam en een prijs. Alleen is die prijs een prijs per kilo, en komt er een gewicht bij. Schrijf een subklasse WeighedProduct van Product, met een constructor __init__(self, name, cents_per_kg, grams). Die laat de constructor van Product de naam en de prijs per kilo opslaan, en bewaart daarna het gewicht in self._grams.

Hint

super().__init__(name, cents_per_kg) roept de constructor van Product aan. Die zet self.name en self._cents. Bij een gewogen product is _cents dus de prijs per kilo.

Voer daarna de test uit. print(kaas) drukt kaas: 1450 af: de prijs van een hele kilo. Dat klopt nog niet, en dat los je in stap 2 op.

# jouw oplossing
kaas = WeighedProduct("kaas", 1450, 400)
assert kaas.name == "kaas"
print(kaas)

Stap 2: de prijs van een gewogen product

Geef WeighedProduct een eigen methode price(self). Die geeft de prijs per kilo maal het gewicht in gram, gedeeld door 1000 met //. Zo overschrijft WeighedProduct de methode price van Product.

Aanroep

Resultaat

WeighedProduct("kaas", 1450, 400).price()

580

WeighedProduct("appels", 299, 1000).price()

299

WeighedProduct("druiven", 499, 250).price()

124

Voer de cel van stap 1 opnieuw uit, en dan de test hieronder.

kaas = WeighedProduct("kaas", 1450, 400)
assert kaas.price() == 580
assert repr(kaas) == "kaas: 580"
assert WeighedProduct("appels", 299, 1000).price() == 299
assert WeighedProduct("druiven", 499, 250).price() == 124
assert Product("brood", 289).price() == 289

Stap 3: waarom __repr__ meeverandert

Voer de test van stap 1 nog eens uit. Nu drukt print(kaas) kaas: 580 af, terwijl je __repr__ niet hebt aangeraakt.

Beantwoord als commentaar in de cel hieronder:

  1. __repr__ staat alleen in Product. Welke price roept ze aan bij kaas: die van Product of die van WeighedProduct? Hoe weet Python dat?

  2. Wat had print(kaas) afgedrukt als __repr__ self._cents las in plaats van self.price() aan te roepen?

# jouw antwoorden

Stap 4: de klasse DiscountedProduct

Een product in de aanbieding is ook een product. Schrijf een subklasse DiscountedProduct van Product, met een constructor __init__(self, name, cents, percent), en een methode price die de prijs na de korting teruggeeft: de gewone prijs min percent procent daarvan.

Aanroep

Resultaat

DiscountedProduct("koffie", 599, 25).price()

450

DiscountedProduct("thee", 250, 0).price()

250

DiscountedProduct("thee", 250, 100).price()

0

Hint

De gewone prijs heb je al: dat is wat price van Product teruggeeft. Die methode roep je aan met super().price(). Reken daarmee de korting uit, en haal die ervan af.

# jouw oplossing
koffie = DiscountedProduct("koffie", 599, 25)
assert koffie.price() == 450
assert repr(koffie) == "koffie: 450"
assert DiscountedProduct("thee", 250, 0).price() == 250
assert DiscountedProduct("thee", 250, 100).price() == 0

Stap 5: de klasse Receipt

Een kassabon heeft producten: dat is compositie, zoals je in week 5 zag. Schrijf een klasse Receipt:

Methode

Doet

__init__(self)

maakt een lege bon: de lijst self._items is leeg

add(self, item)

zet item op de bon

total(self)

geeft de som van de prijzen van alle items op de bon

total roept bij elk item price() aan. Welke price er draait, hangt af van het item: bij kaas die van WeighedProduct, bij koffie die van DiscountedProduct. Receipt hoeft daar niets van te weten. Dat is polymorfisme: één aanroep, ander gedrag per object.

Hint

Voor total gebruik je een verzamelvariabele die begint op 0.

# jouw oplossing
bon = Receipt()
assert bon.total() == 0
bon.add(Product("brood", 289))
bon.add(WeighedProduct("kaas", 1450, 400))
bon.add(DiscountedProduct("koffie", 599, 25))
assert bon.total() == 1319

Stap 6: het duurste item

Geef Receipt een methode most_expensive(self), die het item met de hoogste prijs teruggeeft. Is de bon leeg, dan geeft ze None. Voer daarna de cel van stap 5 opnieuw uit, en dan de test hieronder.

Hint

Begin met een variabele die None is, en loop over de items. Is die variabele nog None, of is de prijs van het item hoger dan die van het duurste tot nu toe, dan is het item het nieuwe duurste. Bij een lege bon loopt de lus nul keer.

brood = Product("brood", 289)
kaas = WeighedProduct("kaas", 1450, 400)
bon = Receipt()
assert bon.most_expensive() is None
bon.add(brood)
assert bon.most_expensive() is brood
bon.add(kaas)
bon.add(DiscountedProduct("koffie", 599, 25))
assert bon.most_expensive() is kaas

Stap 7: statiegeld

Statiegeld is geen product dat je koopt: je krijgt het terug. Schrijf een klasse Deposit, die geen subklasse van Product is, maar wel op de bon kan:

Methode

Doet

__init__(self, name, cents)

slaat de naam op in self.name en het bedrag in self._cents

price(self)

geeft het bedrag negatief: Deposit("fles", 25).price() is -25

__repr__(self)

geeft een regel voor de bon, zoals fles: -25

Receipt roept alleen price() aan, en Deposit heeft price(). Meer is er voor total niet nodig. Dat een object alleen de methoden en attributen hoeft te hebben die worden gebruikt, en niet van een bepaalde klasse hoeft te zijn, heet duck typing: als het loopt als een eend en kwaakt als een eend, dan is het een eend.

# jouw oplossing
fles = Deposit("fles", 25)
assert fles.price() == -25
assert repr(fles) == "fles: -25"
bon = Receipt()
bon.add(Product("brood", 289))
bon.add(WeighedProduct("kaas", 1450, 400))
bon.add(DiscountedProduct("koffie", 599, 25))
bon.add(fles)
assert bon.total() == 1294

Stap 8: wat mag er op de bon?

Beantwoord als commentaar in de cel hieronder:

  1. Welke methoden moet een object hebben om mee te tellen in total en in most_expensive?

  2. Stel dat Deposit geen methode price had. Wat gaat er dan mis, en op welk moment merk je dat: bij bon.add(fles) of later?

  3. Had Deposit ook een subklasse van Product kunnen zijn? Wat had het dan geërfd, en had het daar iets aan gehad?

# jouw antwoorden

Stap 9: de bon afdrukken

Geef Receipt een methode __repr__(self). Die geeft elk item op een eigen regel, in de volgorde waarin ze op de bon zijn gezet, en daaronder een regel met het totaal. Voer daarna de cel van stap 5 opnieuw uit, en dan de test hieronder.

brood: 289
kaas: 580
fles: -25
totaal: 844

Een lege bon geeft alleen totaal: 0.

Hint

Bouw een string op met een verzamelvariabele die begint als "". repr(item) geeft de regel van een item, en "\n" is het teken voor een nieuwe regel.

bon = Receipt()
assert repr(bon) == "totaal: 0"
bon.add(Product("brood", 289))
bon.add(WeighedProduct("kaas", 1450, 400))
bon.add(Deposit("fles", 25))
assert repr(bon) == "brood: 289\nkaas: 580\nfles: -25\ntotaal: 844"
print(bon)

Stap 10: een eigen soort product

Bedenk zelf een soort product dat op een andere manier geld kost, en schrijf het als subklasse van Product. Bijvoorbeeld een product dat per stuk wordt verkocht in een aantal stuks, zoals acht mandarijnen van 35 cent, of een product waarbij je een vast bedrag korting krijgt in plaats van een percentage.

Je klasse moet:

  1. een constructor hebben die super().__init__ aanroept;

  2. price overschrijven;

  3. goed afgedrukt worden door __repr__ van Product, zonder dat je __repr__ zelf schrijft.

Schrijf in de tweede cel minstens drie eigen assertions, waarvan één een randgeval, zoals nul stuks of nul korting.

# jouw klasse
# jouw assertions

Stap 11: gewogen én in de aanbieding

Deze week is de kaas in de aanbieding: 25% korting op de prijs van het gewicht. Een WeighedProduct kent geen korting, en een DiscountedProduct geen gewicht.

Schrijf een klasse DiscountedWeighedProduct als subklasse van WeighedProduct, met een constructor __init__(self, name, cents_per_kg, grams, percent). Haar price rekent eerst de prijs van het gewicht uit met super().price(), en haalt daar het percentage korting van af.

Aanroep

Resultaat

DiscountedWeighedProduct("kaas", 1450, 400, 25).price()

435

DiscountedWeighedProduct("kaas", 1450, 400, 0).price()

580

# jouw oplossing
kaas = DiscountedWeighedProduct("kaas", 1450, 400, 25)
assert kaas.price() == 435
assert repr(kaas) == "kaas: 435"
assert DiscountedWeighedProduct("kaas", 1450, 400, 0).price() == 580

Het werkt. Kijk nu naar wat het kost. Beantwoord als commentaar in de cel hieronder:

  1. De berekening van de korting staat nu op twee plekken. In welke klassen?

  2. Stel dat er ook een product per stuk in de aanbieding kan zijn, zoals in stap 10, en dat er naast een percentage ook een vast bedrag korting bestaat. Hoeveel klassen heb je dan nodig om elke soort product met elke soort korting te kunnen maken?

# jouw antwoorden

Tot slot

Je kassabon telt producten op die elk op hun eigen manier geld kosten, en statiegeld dat helemaal geen product is. Receipt roept alleen price() aan en hoeft van geen enkel item te weten wat het is. Een nieuwe soort product is een nieuwe subklasse, en aan de bon verandert niets.

Maar stap 11 liet zien waar overerving ophoudt. Een kaas in de aanbieding is een gewogen product, en ook een product in de aanbieding. Met alleen subklassen heb je voor elke combinatie een eigen klasse nodig, en staat dezelfde berekening op steeds meer plekken.

Misschien is een korting geen soort product, maar iets wat een product heeft: een object dat een product en een percentage bewaart, en zijn prijs uitrekent door het product naar zijn prijs te vragen. Dan werkt één klasse voor korting op elk soort product. Dat is compositie, uit week 5. Probeer het eens: welke methoden moet zo’n korting hebben om op de bon te kunnen?