Een index met expliciete dubbele-codecontrole
Vul build_price_index(rows) in: doorloop paren (code, prijs) eenmaal en geef een nieuwe dict. Een herhaalde code geeft ValueError("artikelcode dubbel"), ook als de prijs gelijk is. Wijzig de bron niet.
Je rapport zoekt voor iedere bestelling opnieuw door de hele prijslijst. Bij enkele regels merk je weinig, maar het zoekwerk groeit mee met zowel de bestellingen als de catalogus. Je wilt de herhaalde zoekronde verminderen en tegelijk bewijzen dat het rapport hetzelfde blijft betekenen.
Een regressie is gedrag dat na een verandering onbedoeld verslechtert. Schrijf daarom eerst op wat behouden moet blijven. In deze les mag een code vaker besteld zijn, is een onbekende code een fout en mag een dubbele cataloguscode niet stil een prijs overschrijven.
Een lege bestelling heeft totaal nul. De invoer blijft ongewijzigd. Door deze afspraken vooraf te testen, voorkom je dat een snelle versie vooral minder werk doet doordat zij gegevens vergeet.
Een index koppelt een zoeksleutel rechtstreeks aan de bijbehorende waarde. Hier wordt dat een dictionary van artikelcode naar prijs. Je loopt eenmaal door de catalogus om die index op te bouwen en gebruikt daarna een dictionarylookup per bestelling.
Een dictionarylookup heeft bij gebruikelijke sleutels gemiddeld een constante kost, maar dat is geen absolute garantie voor iedere denkbare invoer. De index kost daarnaast geheugen en opbouwtijd. Voor één kleine zoekvraag kan een eenvoudige lus daarom een prima keuze blijven.
rows = [("p", 3), ("q", 5), ("r", 7), ("s", 9)]
codes = ["r", "s"] * 3
scans = 0
slow_total = 0
for code in codes:
for tag, price in rows:
scans += 1
if tag == code:
slow_total += price
break
print(slow_total)
print(scans)
index, built = {}, 0
for tag, price in rows:
built += 1
index[tag] = price
reads = 0
fast_total = 0
for code in codes:
reads += 1
fast_total += index[code]
print(fast_total)
print(built)
print(reads)48 21 48 4 6
Dit voorbeeld voert beide versies echt uit en telt de uitgevoerde zoekstappen. De indexversie telt ook hoeveel catalogusregels zijn voorbereid. De twee totalen zijn gelijk; de kosten zijn in verschillende onderdelen uitgesplitst.
Een tijdmeting met timeit kan later laten zien wat deze keuze op jouw machine oplevert. Kies dan dezelfde gegevens, herhalingen en scope. Als je de index maar één keer gebruikt, hoort het bouwen bij die vergelijking; bij hergebruik kun je opbouw en aanvragen apart rapporteren.
De oefencontrole gebruikt daarnaast stringcodes met een teller op hun gelijkheidsvergelijkingen en behoudt het gewone stringhashcontract. Dat onderscheidt concrete herhaalde-zoekvarianten op deze invoer. Het bewijst niet dat ieder denkbaar kwadratisch algoritme wordt uitgesloten.
Een teller voor zoekhandelingen beantwoordt een andere vraag dan een timer. Hij laat zien hoeveel van een afgesproken soort werk gebeurde, zonder te claimen hoeveel seconden dat kostte. Beide zijn nuttig als je hun betekenis erbij vertelt.
Voor het rapport wil je ook weten hoe vaak iedere code voorkwam. Loop eenmaal door de codes en verhoog in een dictionary hun teller. Voor iedere code opnieuw de hele lijst doorzoeken werkt tegen hetzelfde probleem aan als de oorspronkelijke prijszoeker.
De codes kunnen een iterator zijn, die je maar eenmaal kunt verbruiken. Verzamel daarom de aantallen en gebruik die samenvatting voor het eindtotaal. Een code met aantal drie draagt drie keer zijn prijs bij.
def source():
for code in ["B", "A", "B"]:
print(f"lees {code}")
yield code
items = source()
counts = {}
for code in items:
counts[code] = counts.get(code, 0) + 1
print(sorted(counts.items()))
print(list(items))lees B
lees A
lees B
[('A', 1), ('B', 2)]
[]
Het tweede voorbeeld gebruikt een eigen generator en laat zien dat iedere bronwaarde eenmaal wordt aangeboden. De uitkomst bevat ook herhalingen, terwijl de bron daarna uitgeput is. Een tweede lus over dezelfde iterator zou de gegevens niet terughalen.
Met index.get(code, 0) verdwijnt een onbekende code stil uit de rekening. Dat is snel en geeft een getal terug, maar het verandert het foutcontract. Gebruik een echte lookup die de bestaande KeyError doorgeeft.
Ook een dictionarycomprehension kan dubbele cataloguscodes ongemerkt overschrijven. Controleer tijdens de indexbouw of de code er al staat. Zelfs twee gelijke prijzen moeten volgens deze oefening als dubbel worden geweigerd.
Vul build_price_index(rows) in: doorloop paren (code, prijs) eenmaal en geef een nieuwe dict. Een herhaalde code geeft ValueError("artikelcode dubbel"), ook als de prijs gelijk is. Wijzig de bron niet.
Vul indexed_total(codes, index) in: tel index[code] voor iedere code op, ook bij herhalingen. Gebruik precies één lookup per gelezen code, wijzig de index niet en geef een oorspronkelijke KeyError ongewijzigd door.
Vul count_codes(codes) in: geef een dict met het aantal voorkomens per code. Werk de teller tijdens één doorgang bij; vermijd herhaalde volledige zoekrondes en verander de bron niet.
Vul make_report(codes, rows) in: bouw eenmaal de prijsindex en eenmaal de telling. Geef (totaalbedrag, alfabetisch gesorteerde lijst (code, aantal)-tuples). Behoud dubbele-code- en onbekende-codefouten; beide bronnen mogen maar eenmaal worden doorlopen.
De volgende module brengt de geleerde onderdelen samen in een groter eindproject met duidelijke regressietests.
Voer je code uit en ontdek
wat je hebt gemaakt.
Je hoeft het niet in één keer goed te doen. Na het uitvoeren zie je hier welke stappen al kloppen.