Basis

Deze pagina bevat uitvoerbare code.

Een kassabon

Opgave: Een kassabon

De klasse Product uit de opgave:

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()}"

Stap 1: de klasse WeighedProduct

De klasse hieronder bevat ook de methode price van stap 2. Zonder die methode drukt print(kaas) kaas: 1450 af, omdat dan price van Product draait.

class WeighedProduct(Product):
    """Een product dat per kilo wordt verkocht."""

    def __init__(self, name, cents_per_kg, grams):
        """Maak een gewogen product; _cents is hier de prijs per kilo."""
        super().__init__(name, cents_per_kg)
        self._grams = grams

    def price(self):
        """Geeft de prijs van het gewicht in centen, naar beneden afgerond."""
        return self._cents * self._grams // 1000
kaas = WeighedProduct("kaas", 1450, 400)
assert kaas.name == "kaas"
print(kaas)
kaas: 580

Stap 2: de prijs van een gewogen product

1450 * 400 is 580000, en 580000 // 1000 is 580. Bij de druiven is 499 * 250 // 1000 124: 124.75 naar beneden afgerond.

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

  1. Die van WeighedProduct. self is in __repr__ het object waarvoor de methode wordt aangeroepen, hier kaas, en kaas is een WeighedProduct. Python zoekt price eerst in de klasse van het object, en pas daarna in de superklasse. Dat de aanroep in een methode van Product staat, maakt daarvoor niet uit.

  2. kaas: 1450. Dan leest __repr__ de prijs per kilo, en gebruikt ze de berekening van WeighedProduct niet.

Stap 4: de klasse DiscountedProduct

super().price() geeft de gewone prijs. De uitwerking bewaart die in full, zodat de methode price van Product maar één keer wordt aangeroepen.

class DiscountedProduct(Product):
    """Een product in de aanbieding, met een kortingspercentage."""

    def __init__(self, name, cents, percent):
        """Maak een product met de gewone prijs cents en percent procent korting."""
        super().__init__(name, cents)
        self._percent = percent

    def price(self):
        """Geeft de prijs na de korting in centen."""
        full = super().price()
        return full - full * self._percent // 100
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

De klasse hieronder bevat ook most_expensive van stap 6 en __repr__ van stap 9. total gebruikt de verzamelvariabele result.

class Receipt:
    """Een kassabon met de producten die zijn aangeslagen."""

    def __init__(self):
        """Maak een lege kassabon."""
        self._items = []

    def add(self, item):
        """Zet item op de bon."""
        self._items.append(item)

    def total(self):
        """Geeft het totaal van de bon in centen."""
        result = 0
        for item in self._items:
            result += item.price()
        return result

    def most_expensive(self):
        """Geeft het duurste item op de bon, of None als de bon leeg is."""
        most = None
        for item in self._items:
            if most is None or item.price() > most.price():
                most = item
        return most

    def __repr__(self):
        """Geeft de bon: elk item op een eigen regel, en daaronder het totaal."""
        s = ""
        for item in self._items:
            s += repr(item) + "\n"
        return s + f"totaal: {self.total()}"
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

most_expensive vergelijkt de items alleen via price(). Bij een lege bon loopt de lus nul keer, en blijft most dus None.

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

Deposit heeft geen superklasse tussen haakjes.

class Deposit:
    """Statiegeld dat je terugkrijgt; geen product, maar het kan wel op de bon."""

    def __init__(self, name, cents):
        """Maak statiegeld van cents centen voor name."""
        self.name = name
        self._cents = cents

    def price(self):
        """Geeft het bedrag in centen, negatief: je krijgt het terug."""
        return -self._cents

    def __repr__(self):
        """Geeft het statiegeld als regel op de kassabon, zoals fles: -25."""
        return f"{self.name}: {self.price()}"
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?

  1. Alleen price(). Voor het afdrukken van de bon in stap 9 is daarnaast een __repr__ nodig.

  2. Dan geeft bon.total() een AttributeError: 'Deposit' object has no attribute 'price'. Dat merk je pas bij total of most_expensive, want add zet het item alleen in de lijst en roept er niets op aan.

  3. Het kan: statiegeld heeft ook een naam en een bedrag. Het had dan de constructor en __repr__ van Product geërfd, en alleen price hoeven te overschrijven. Tegelijk is statiegeld iets wat je terugkrijgt, en geen product dat je koopt. Beide keuzes zijn te verdedigen; de bon werkt met allebei, omdat hij alleen price() aanroept.

Stap 9: de bon afdrukken

__repr__ staat in de klasse Receipt bij stap 5. Elk item krijgt een eigen regel door "\n" erachter, en het totaal komt eronder.

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)
brood: 289
kaas: 580
fles: -25
totaal: 844

Stap 10: een eigen soort product

Er is niet één goed antwoord. Een voorbeeld: een product per stuk, in een aantal stuks. Net als bij WeighedProduct is _cents hier de prijs van één eenheid.

class PieceProduct(Product):
    """Een product dat per stuk wordt verkocht, in een aantal stuks."""

    def __init__(self, name, cents_per_piece, count):
        """Maak count stuks van name; _cents is hier de prijs van één stuk."""
        super().__init__(name, cents_per_piece)
        self._count = count

    def price(self):
        """Geeft de prijs van alle stuks samen in centen."""
        return self._cents * self._count
assert PieceProduct("mandarijnen", 35, 8).price() == 280
assert PieceProduct("mandarijnen", 35, 1).price() == 35
assert PieceProduct("mandarijnen", 35, 0).price() == 0
assert repr(PieceProduct("eieren", 30, 6)) == "eieren: 180"

Stap 11: gewogen én in de aanbieding

super().price() is hier de prijs van WeighedProduct: de prijs van het gewicht. Daar gaat de korting af: 580 - 580 * 25 // 100 is 435.

class DiscountedWeighedProduct(WeighedProduct):
    """Een gewogen product in de aanbieding."""

    def __init__(self, name, cents_per_kg, grams, percent):
        """Maak een gewogen product met percent procent korting."""
        super().__init__(name, cents_per_kg, grams)
        self._percent = percent

    def price(self):
        """Geeft de prijs van het gewicht na de korting in centen."""
        full = super().price()
        return full - full * self._percent // 100
kaas = DiscountedWeighedProduct("kaas", 1450, 400, 25)
assert kaas.price() == 435
assert repr(kaas) == "kaas: 435"
assert DiscountedWeighedProduct("kaas", 1450, 400, 0).price() == 580
  1. In DiscountedProduct en in DiscountedWeighedProduct.

  2. Met drie soorten product (gewoon, gewogen, per stuk) en twee soorten korting (percentage, vast bedrag) zijn het drie klassen zonder korting en zes met: negen klassen, en elke nieuwe soort maakt het er meer. De berekening van een percentage korting staat dan in drie klassen, en die van een vast bedrag ook.

Tot slot

De vraag in de afsluiting: een korting die een product heeft, heeft alleen price() nodig om mee te tellen in total en most_expensive, en __repr__ om op de afgedrukte bon te staan. Meer roept Receipt niet aan.