Basis¶
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 |
|
|
gewogen product: een prijs per kilo en een gewicht in gram |
|
|
product in de aanbieding: een prijs en een kortingspercentage |
|
|
statiegeld: een bedrag dat je terugkrijgt |
|
|
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 |
|
een subklasse van |
2 |
|
de prijs van een gewogen product |
3 |
- |
uitleggen waarom |
4 |
|
een product in de aanbieding, via |
5 |
|
een kassabon die producten optelt |
6 |
|
het duurste item op de bon |
7 |
|
statiegeld, zonder subklasse van |
8 |
- |
beoordelen wat iets nodig heeft om op de bon te mogen |
9 |
|
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 |
|---|---|
|
|
|
|
|
|
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:
__repr__staat alleen inProduct. Welkepriceroept ze aan bijkaas: die vanProductof die vanWeighedProduct? Hoe weet Python dat?Wat had
print(kaas)afgedrukt als__repr__self._centslas in plaats vanself.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 |
|---|---|
|
|
|
|
|
|
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 |
|---|---|
|
maakt een lege bon: de lijst |
|
zet |
|
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 |
|---|---|
|
slaat de naam op in |
|
geeft het bedrag negatief: |
|
geeft een regel voor de bon, zoals |
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:
Welke methoden moet een object hebben om mee te tellen in
totalen inmost_expensive?Stel dat
Depositgeen methodepricehad. Wat gaat er dan mis, en op welk moment merk je dat: bijbon.add(fles)of later?Had
Depositook een subklasse vanProductkunnen 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:
een constructor hebben die
super().__init__aanroept;priceoverschrijven;goed afgedrukt worden door
__repr__vanProduct, 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 |
|---|---|
|
|
|
|
# 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:
De berekening van de korting staat nu op twee plekken. In welke klassen?
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?