Leren Programmeren in Python Deel 2

Wat in dit deel aan bod komt:


Fouten, defecten en falende programma's

Helaas kan er veel fout gaan bij programmeren. Maar als je het onderstaande over fouten weet, dan kun je er goed mee leren omgaan. Zie ook de checklists.

Fouten maken is menselijk en onvermijdelijk, zeker bij 'saaiere' werkzaamheden zoals het intikken van een programma. De kunst is om fouten zo veel mogelijk te voorkomen en om de fouten, die je toch maakt, zo snel mogelijk te ontdekken en dan te verhelpen. Hoe langer een fout onopgemerkt blijft, hoe kostbaarder het is om de zaak te herstellen. Vandaar dat veel maatregelen bij programmeren speciaal gericht zijn op

De term "fout" heeft verscheidene betekenissen. Het is nuttig om deze uit elkaar te houden:

Defecten zijn, direct of indirect, geïntroduceerd doordat menselijke fouten begaan zijn. Defecten kunnen zich uiten doordat ze leiden tot het falen van het programma.

Niet elk defect hoeft direct te leiden tot falen. Denk aan een defect in het commentaar of aan een defect in een stuk van het programma dat zelden geactiveerd wordt. Zulke defecten zijn natuurlijk wel tijdbommen, die later alsnog kunnen afgaan. Zo kan een defect in het commentaar bij onderhoud een ander defect veroorzaken dat wel tot falen leidt.

Alle defecten zijn ongewenst. Ze kunnen bijvoorbeeld zitten in

Het is daarom zaak bij al deze onderdelen zorgvuldig te werk te gaan. We gaan hier echter nader in op defecten in de programmacode. Je moet niet wachten tot ze je toevallig overkomen, maar ze actief zoeken.

Defecten constateren in programma's: nalezen en testen

Zoals we in Deel 1 al opmerkten, zijn er twee methodes om defecten in programma's te constateren:

  1. Door het programma na te lezen en te controleren tegen het ontwerp.
  2. Door het programma te gebruiken en het gedrag te controleren tegen de specificatie, d.w.z. te kijken of het faalt.

Beide methodes zijn nodig; ze vullen elkaar aan. Bij het nalezen is het nuttig een checklist te gebruiken. We gaan hier nu verder in op de tweede methode.

Bij gewoon gebruik kan je te maken krijgen met een falend programma, maar dat is niet een effectieve manier om defecten te zoeken. Testen is het doelgericht zoeken naar defecten in een programma door het programma systematisch te gebruiken en het gedrag ervan te controleren tegen de specificatie. Bij de programma's die we hier beschouwen komt dat neer op het kiezen van geschikte invoer en het controleren van de bijbehorende uitvoer.

Het beste is om vooraf, liefst zelf vóór het coderen, in een testplan een aantal testgevallen vast te leggen. Zie ook de checklist voor het opstellen van een testplan.

Als de programmacode is ingetikt moet het testplan worden uitgevoerd (ook hiervoor is een checklist). Een falend testgeval wijst op de aanwezigheid van een defect. We gaan nu eerst in op hoe Python programma's falen en vervolgens hoe je in een falend programma defecten kan localiseren.

Hoe Python programma's falen

Er zijn twee vormen van falen te onderscheiden bij Python programma's.

  1. Het Python-systeem geeft bij gebruik van het programma een foutmelding. We onderscheiden daarbij verder
  2. De gebruiker stelt een afwijking met de specificatie vast bij gebruik van het programma, maar het Python-systeem meldt niets bijzonders.

Python foutmeldingen

Zodra je een Python programma start, controleert het Python-systeem eerst de syntax (vorm) van het hele programma. Als die niet klopt, dan is het programma niet uitvoerbaar en wordt een SyntaxError gemeld met regelnummer en positie op die regel (de details hangen af van de gebruikte IDE). Kijk bijvoorbeeld wat er gebeurt als je de dubbele punt vergeet in een for-opdracht:

for teller in 0, 1, 2
    print teller

Traceback (most recent call last):
  ...
  File "...", line 1
    for teller in 0, 1, 2
                         ^
SyntaxError: invalid syntax

In een syntactisch correct programma kan tijdens de executie van opdrachten een ongedefinieerde situatie optreden. In zo'n geval wordt het programma, net als bij een syntax-fout, meteen gestopt, maar nu met een runtime-foutmelding: wat ging er mis en waar. Kijk bijvoorbeeld wat er gebeurt bij delen door nul:

for teller in 0, 1, 2 :
    print 1 / teller

Traceback (most recent call last):
  File "...", line 2, in ?
ZeroDivisionError: integer division or modulo by zero

Zie ook de checklist met veelvoorkomende Python foutmeldingen.

Falen zonder Python foutmelding

Het Python-systeem weet niet wat het programma geacht wordt te doen volgens de specificatie. De meeste fouten kunnen daarom niet door het Python-systeem gedetecteerd worden. De gebruiker moet i.h.a. zelf vaststellen of het programma faalt door het extern observeerbare gedrag te controleren tegen de specificatie. We kunnen de volgende vormen van zulk falen onderscheiden. Het programma

Het programma kan natuurlijk ook te langzaam zijn of andere fronten niet voldoende kwaliteit hebben.

[N.B. Automatisch testen is wel mogelijk. Dit vereist dat de specificatie zodanig wordt vastgelegd dat de computer de controle kan doen. Hiervoor is het Python unit testing framework beschikbaar, maar gebruik daarvan vereist meer Python kennis.]

Defecten localiseren in falende programma's

Met een falend testgeval constateer je dat ergens een defect zit. Om defecten te verhelpen moet je vinden waar ze precies zitten. Dat kan veel bewerkelijker zijn. Daarom kun je het beste pas gaan zoeken na het uitvoeren van het hele testplan.

Omdat je defecten uiteindelijk in de programmacode moet vinden, is het in de eerste plaats belangrijk ervoor te zorgen dat je programma's altijd van het begin af goed leesbaar zijn. Dat kost een klein beetje extra moeite vooraf (een soort verzekeringspremie). Maar het kost veel meer moeite om na het constateren van defecten, deze te vinden in een slecht leesbaar programma of om een programma achteraf alsnog leesbaar te maken. Bovendien zul je merken dat je veel minder fouten maakt als je meteen leesbare programma's schrijft. Voorkomen is beter dan genezen.

Het is belangrijk je te realiseren dat de plek waar je constateert dat het programma faalt, vaak niet de plek is waar een defect zit. Bovendien kan er op meer plaatsen iets fout zijn.

Stel een ZeroDivisionError wordt gemeld bij a = b / c. Het kan zijn dat die opdracht verkeerd is en a = c / b had moeten zijn. (Merk op dat zinvolle namen enige bescherming bieden tegen het maken van dit soort fouten). Maar het kan net zo goed zijn dat c in een ander deel van het programma verkeerd is berekend of ingevoerd. Je moet dus verder kijken dan je neus lang is.

Bij het zoeken naar defecten kan het helpen om na eindiging van het programma de waarden van relevante variabelen te inspecteren. Bij sommige IDEs kan dat interactief (achter >>>, of in een apart venster; sommige IDEs hebben zelfs een ingebouwde debugger waarmee je stapsgewijs door een programma kan lopen en de variabelen kan zien), maar je kan ook (tijdelijk) een print-opdracht tussenvoegen. Je kan eventueel tijdelijk het programma voortijdig laten eindigen door de opdracht assert False tussen te voegen. Gebruik assert conditie om alleen te stoppen als conditie NIET geldt.

Als je het defect uiteindelijk hebt gevonden, moet je toegevoegde print- of assert-opdrachten weer verwijderen of ze "uitschakelen" door er een # voor te zetten.

Oefening: Haal het programma gepast0d.py op en test het volgens het testplan uit Deel 1. Zoek alle defecten en verbeter ze. Maak een genummerde lijst met alle fouten; vermeld telkens hoe het programma faalde (welke invoer; waar/welke Python foutmelding ofwel welk afwijkend gedrag opviel) en welk defect het betrof (wat was er mis en waar).


Soorten Commentaar

Een van de dingen die in het eerste Python programma gepast0.py de leesbaarheid kan verhogen, is betere toelichting in de programmatekst. We onderscheiden de volgende soorten toelichting:

Aanhef

De aanhef bovenaan het programma vermeldt in een bondige zin wat het doel is van het programma, gevolgd door een samenvatting van de specificatie:

en tenslotte de auteur(s), datum en versie. Dit kan op verschillende manieren vormgegeven worden:

#############################################################
# Een bedrag gepast betalen met zo min mogelijk euromunten. #
#                                                           #
# Invoer:                                                   #
#   het bedrag tussen 0 en 500 eurocent                     #
# Uitvoer:                                                  #
#   de minimale betaling met euromunten in tabelvorm        #
# Restricties:                                              #
#   er zijn voldoende munten van iedere soort,              #
#   ongebruikte muntsoorten niet vermelden                  #
#                                                           #
# Tom Verhoeff (TUE)                                        #
# 2003/01/04                                                #
# Versie 2.0                                                #
#############################################################

De programmeertaal Python biedt hiervoor echter ook een betere voorziening:

"""Een bedrag gepast betalen met zo min mogelijk euromunten.

   Invoer:
     het bedrag tussen 0 en 500 eurocent
   Uitvoer:
     de minimale betaling met euromunten in tabelvorm
   Restricties:
     er zijn voldoende munten van iedere soort,
     ongebruikte muntsoorten niet vermelden
"""

__author__  = 'Tom Verhoeff (TUE)'
__date__    = '2003/01/04'
__version__ = '2.0'

In deze laatste vorm zijn de gegevens makkelijker toegankelijk voor andere programma's. Dat is nu nog niet van belang, maar kan later handig zijn. [Detail: Als een programma begint met een karakterrij, dan wordt deze in feite toegekend aan de naam __doc__.] Een karakterrij tussen drievoudige aanhalingstekens kan uit meer regels bestaan.

Ontwerpverwijzing

Het is verstandig de grote lijn van het ontwerp te vermelden in de code. We zullen dit achter ## weergeven. Zulk commentaar staat direct vóór het stuk programma waar het op slaat (dit is overigens geen universele gewoonte):

## Vraag de invoer op.
bedrag = input ( 'Geef bedrag tussen 0 en 500 eurocent: ' )

## Doorloop de muntsoorten in dalende volgorde en
## gebruik ze zo vaak mogelijk om het bedrag te passen.
## Schrijf voor elke gebruikte muntsoort een regel in de tabel.

for munt in 200, 100, 50, 20, 10, 5, 2, 1 :
    ...

Interpretatie

Soms helpt het de lezer om uitdrukkingen uit te leggen in termen van de probleemwereld. We schrijven dit commentaar liefst op dezelfde regel:

    while bedrag >= munt :  # munt kan (nogmaals) gebruikt worden
        ...
    
    if aantal > 0 :  # munt is inderdaad gebruikt
        ...

Assertie

De laatste vorm van commentaar is de assertie, ofwel bewering over de toestand ter plekke. Hierin drukken we uit welke relatie geldt tussen de variabelen op het moment dat de executie de assertie "passeert". Als het kan, dan doen we dat met een wiskundige formule, maar dat is niet altijd even makkelijk. Om asserties te onderscheiden van ander commentaar, schrijven we ze achter #@ (ook dit is geen universele gewoonte).

bedrag = input ( 'Geef bedrag tussen 0 en 500 eurocent: ' )
#@ 0 <= bedrag <= 500

Als een bewering direct vóór en na iedere slag van een herhaling geldt, dan noemen we dat een invariante relatie, kortweg invariant. We schrijven er dan de afkorting inv bij:

#@ inv: 0 <= bedrag <= 500
#@      geschreven deel van betaling + bedrag = invoerbedrag

for munt in 200, 100, 50, 20, 10, 5, 2, 1 :
    ...

#@ bedrag = 0, dus invoerbedrag is volledig betaald

Het (rest)bedrag neemt af terwijl er steeds meer van de betaling is geschreven. Opgeteld zijn ze telkens gelijk aan het oorspronkelijke invoerbedrag. Als we kunnen inzien dat na afloop van de herhaling het restbedrag nul is geworden, dan levert de geschreven betaling precies het invoerbedrag.

Zo'n invariant is essentieel om een herhaling te begrijpen. Hij karakteriseert de mogelijke tussentoestanden, met als speciale gevallen de begin- en eindtoestand van de herhaling. Een goed ontwerp begint met het kiezen van een invariant, waaruit dan het programma volgt, en niet omgekeerd.

Ook bij de herhaling met while kunnen we zo'n invariant geven. Deze herhaling verandert telkens aantal en bedrag, zodanig dat aantal*munt + bedrag onveranderd blijft.

    aantal = 0
    
    #@ inv: geschreven deel van betaling + aantal*munt + bedrag = invoerbedrag
    
    while bedrag >= munt :
        aantal = aantal + 1
        bedrag = bedrag - munt
    
    #@ bedrag < munt, dus munt kan niet (vaker) gebruikt worden

Merk op dat de invariante relatie niet geldt tussen het verhogen van aantal en het verminderen van bedrag.

Je kan beweringen over de toestand ook tijdens uitvoering door het Python-systeem laten controleren. Neem de assertie dan op in de assert-opdracht. De controle kost een klein beetje tijd, maar biedt wel extra bescherming tegen fouten. Bijvoorbeeld:

assert bedrag == 0  # dus invoerbedrag is volledig betaald

Zo ziet het programma er uit met al het commentaar erin (gepast1.py):

"""Een bedrag gepast betalen met zo min mogelijk euromunten.

   Invoer:
     het bedrag
   Uitvoer:
     de minimale betaling met euromunten in tabelvorm
   Restricties:
     het bedrag ligt tussen 0 en 500 eurocent,
     er zijn voldoende munten van iedere soort,
     ongebruikte muntsoorten niet vermelden
"""

__author__  = 'Tom Verhoeff (TUE)'
__date__    = '2003/01/04'
__version__ = '2.0'

## Vraag de invoer op.
bedrag = input ( 'Geef bedrag tussen 0 en 500 eurocent: ' )
#@ 0 <= bedrag <= 500

## Doorloop de muntsoorten in dalende volgorde en
## gebruik ze zo vaak mogelijk om het bedrag te passen.
## Schrijf voor elke gebruikte muntsoort een regel in de tabel.

#@ inv: 0 <= bedrag <= 500
#@      geschreven deel van betaling + bedrag = invoerbedrag

for munt in 200, 100, 50, 20, 10, 5, 2, 1 :
    aantal = 0
    
    #@ inv: geschreven deel van betaling + aantal*munt + bedrag = invoerbedrag
    
    while bedrag >= munt :  # munt kan (nogmaals) gebruikt worden
        aantal = aantal + 1
        bedrag = bedrag - munt
    
    #@ bedrag < munt, dus munt kan niet (vaker) gebruikt worden
    
    if aantal > 0 :  # munt is inderdaad gebruikt
        print aantal, 'x', munt

assert bedrag == 0  # dus invoerbedrag is volledig betaald

We zullen in deze tekst niet altijd alle commentaar weergeven (in de programmabestanden zelf staat het meestal wel).

Slecht commentaar

Er zijn ook slechte vormen van commentaar:


Soorten gegevens (data types)

In Python heeft elk object een soort (Eng.: type). Dit type bepaalt welke waarden het object kan hebben en welke bewerkingen er op van toepassing zijn. We hebben de volgende types al gezien:

Gehele getallen kun je vermenigvuldigen, maar karakterrijen niet. Laten we ze nog iets nader bestuderen.

Gehele getallen

De gehele getallen vormen één van de getal-types (Eng.: numeric types) in Python. Er worden twee soorten onderscheiden:

Op gehele getallen zijn de gebruikelijke rekenkundige bewerkingen en vergelijkingen van toepassing.

Het voorbeeldprogramma voor gepast betalen werkt ook voor grote invoerbedragen, maar is dan wat langzaam vanwege het herhaalde aftrekken. Laten we het versnellen door te delen. Hoeveel keer is munt te gebruiken om bedrag te passen? Natuurlijk is dat bedrag gedeeld door munt, waarbij een eventuele rest weggelaten wordt, ofwel delen met afronding naar beneden. Vanaf Python 2.2 kan dit beter geschreven worden als bedrag // munt.

N.B. Vooralsnog betekent in Python (ook in versies ouder dan 2.1) de deeloperatie / bij toegepassing op gehele getallen hetzelfde als //, d.w.z. `deling zonder rest', ofwel `met afronding naar beneden'. Je kan dus ook schrijven bedrag/munt. Maar pas ook op dat 1/2 daarom 0 is en niet 0.5.

Het bedrag dat resteert na gebruik van munt is bedrag - aantal*munt. In Python kun je met bedrag % munt ook direct de rest na deling verkrijgen. Dit levert het volgende programma gepast2a.py (zonder aanhef, ontwerpverwijzingen of asserties):

bedrag = input ( 'Geef bedrag tussen 0 en 500 eurocent: ' )

for munt in 200, 100, 50, 20, 10, 5, 2, 1 :
    aantal = bedrag // munt  # aantal keer dat munt gebruikt kan worden
    bedrag = bedrag % munt  # resterende te passen bedrag

    if aantal > 0 :  # munt is inderdaad gebruikt
        print aantal, 'x', munt

N.B. Als je de volgorde van de twee toekenningen verwisselt klopt het niet meer, omdat aantal dan bepaald wordt o.g.v. het verkeerde bedrag:

    # FOUT!
    bedrag = bedrag % munt  # resterend te passen bedrag
    aantal = bedrag // munt  # aantal keer dat munt gebruikt kan worden

Interactief Python gebruik

Je kan Python ook interactief gebruiken, d.w.z. losse opdrachten laten uitvoeren zonder een heel programma te schrijven. Dit kan door de Python interpreter op te starten, maar veelal ook in een apart venster binnen de IDE.

Achter de prompt >>> kun je een opdracht intikken, bijvoorbeeld print 46%20. Je kan bij interactief gebruik zelfs print weglaten, want bij intikken van een uitdrukking wordt automatisch het resultaat afgedrukt.

>>> print 46 // 20
2
>>> 46 % 20
6

Interactief gebruik is handig om de Python syntax en semantiek te verkennen. Het is echter ongeschikt voor het ontwikkelen van hele programma's.

Karakterrijen

De karakterrijen vormen één van de rij-types (Eng.: sequence types) in Python. Hierop zijn de algemene rij-bewerkingen en de specifieke bewerkingen voor karakterrijen van toepassing.

Zo kun je in Python karakterrijen m.b.v. + "optellen", d.w.z. achterelkaar plakken (Eng.: concatenate). M.b.v. * kun je een karakterrij met een getal "vermenigvuldigen", d.w.z. herhaald achterelkaar plakken: de uitdrukking 3 * 'ha' levert 'hahaha'.

Het getal 42 en de karakterrij '42' zijn objecten van verschillend type, al lijken ze sterk op elkaar. Wel kun je een geheel getal omzetten in een karakterrij met str(), en omgekeerd een karakterij in een geheel getal met int(). Dit is vergelijkbaar met wat print en input() doen.

In het algemeen is het verstandig om invoer, berekening en uitvoer van elkaar te scheiden. Laten we dit doen in ons voorbeeld door de betaling niet meteen te schrijven maar eerst in een karakterrij op te slaan. Ook is het verstandig om invoervariabelen niet te wijzigen, maar een hulpvariabele te introduceren. We passen daartoe het ontwerp aan:

  1. Vraag bedrag op.
  2. Bepaal minimale betaling van het bedrag.
    Deze stap werken we hier niet verder uit; in het programma spelen daarbij de hulpvariabelen restbedrag en aantal een rol.
  3. Schrijf gevonden betaling.
Hiermee krijgen we een nieuw programma gepast4.py:
## Vraag invoer op
bedrag = input ( 'Geef bedrag tussen 0 en 500 eurocent: ' )
#@ 0 <= bedrag <= 500

## Bepaal minimale betaling van bedrag
betaling = ''         # reeds gevonden deel van de betaling
restbedrag = bedrag   # resterende te passen bedrag

#@ inv: 0 <= restbedrag <= 500
#@      betaling + restbedrag = bedrag

for munt in 200, 100, 50, 20, 10, 5, 2, 1 :
    aantal = restbedrag // munt  # aantal keer dat munt gebruikt kan worden
    restbedrag = restbedrag % munt
    
    #@ betaling + aantal*munt + restbedrag = bedrag
    #@ restbedrag < munt
    
    if aantal > 0 :  # munt is inderdaad gebruikt
        betaling = betaling + str(aantal) + ' x ' + str(munt) + '\n'

#@ restbedrag = 0, dus betaling voldoet bedrag

## Schrijf minimale betaling
print betaling

Merk op dat '' de lege karakterrij is en dat '\n' bij schrijven met print een overgang op een nieuwe regel (Eng.: newline) geeft.

Kolommen uitlijnen in tabellen

Het schrijven van tabellen met getallen komt vaak voor. Zo'n tabel ziet er mooier uit als de getallen netjes uitgelijnd onder elkaar staan. Dat kan door elk getal links met voldoende spaties aan te vullen. Hiervoor heeft Python een aparte voorziening, namelijk formaataanpassing met %:

        betaling = betaling + '%d x %3d\n' % ( aantal, munt )

In '%d x %3d\n' wordt %d vervangen door de waarde van aantal en %3d door de waarde van munt links aangevuld met spaties tot 3 karakters. Bij invoer 96 ziet de uitvoer van programma gepast4a.py er als volgt uit:

1 x  50
2 x  20
1 x   5
1 x   1

De algemene vorm van formaataanpassing is s % u, waarbij s een karakterrij met omzetters (Eng.: conversion specifiers) van de vorm %t is en u een bijpassend tupel met uitdrukkingen is. Zo'n tupel staat tussen ronde haakjes, tenzij er maar één element in zit. In s wordt iedere omzetter vervangen door de waarde van de overeenkomstige uitdrukking, rekening houdend met de omzetwensen, zoals een minimale breedte. De overige karakters uit s worden gewoon overgenomen. Een procentteken moet je opnemen als %%.

Drijvende-komma getallen

Een ander belangrijk numeriek type is dat van de drijvende-komma getallen (Eng.: floating-point numbers). Dit zijn benaderingen van reële getallen. Hierop zijn ook de elementaire operaties +, -, * en / van toepassing. Voor machtsverheffen kun je ** gebruiken.

Merk op dat // ook gebruikt wordt om deling zonder rest te doen op floating-point getallen (d.w.z. naar beneden afgeronde deling). Dus 1.0/2.0 is 0.5, maar 1.0//2.0 is 0.0. Een geheel getal n is met float(n) om te zetten naar een floating-point getal: 1/2 is 0, maar float(1)/2 is 0.5.

In de zogenaamde math module zitten nog meer bewerkingen hiervoor, zoals math.sqrt() voor worteltrekken, alsmede goniometrische functies en de constanten math.pi en math.e. Deze module is te gebruiken door de opdracht import math vooraan in het programma op te nemen.

Rekenen met drijvende-komma getallen is vanwege het benaderend karakter niet zo vanzelfsprekend als je misschien denkt. Als afschrikwekkend voorbeeld geven we hieronder programma gepast4b.py dat met euro als eenheid werkt i.p.v. eurocenten.

"""Een bedrag gepast betalen met zo min mogelijk euromunten.

   Poging om met euro te werken i.p.v. eurocenten.
   WERKT ZO NIET!
"""

## Vraag invoer op
bedrag = input ( 'Geef bedrag tussen 0.00 en 5.00 euro: ' )

## Bepaal minimale betaling van bedrag
betaling = ''         # reeds gevonden deel van de betaling
restbedrag = bedrag   # resterende te passen bedrag

for munt in 2.00, 1.00, 0.50, 0.20, 0.10, 0.05, 0.02, 0.01 :
    aantal = restbedrag // munt  # aantal keer dat munt gebruikt kan worden
    restbedrag = restbedrag % munt
    
    if aantal > 0 :  # munt is inderdaad gebruikt
        betaling = betaling + '%d x %4.2f\n' % ( aantal, munt )

## Schrijf minimale betaling
print betaling

In de gebruikte formaataanpassing '%d x %4.2f\n' % ( aantal, munt ) geeft de omzetter %4.2f een floating-point getal weer met 2 cijfers achter de komma. Bij invoer 0.96 verschijnt de (foutieve!) uitvoer:

1 x 0.50
2 x 0.20
1 x 0.05

De oorzaak van de fout zit in het benaderend karakter. Zie ook opgave 6 hieronder.


Oefeningen

Hier volgen wat oefeningen met de nieuwe stof.

  1. Ga na dat de invarianten in programma gepast1.py gelden bij aanvang van de repetitie waar ze bij horen, en dat ze na elke slag van de repetitie (weer) gelden.
  2. Probeer het versnelde programma gepast2a.py eens met een groot bedrag als invoer, zeg 10 ** 9.
    Probeer het ook eens met een negatief getal, zeg -1.
    Vergelijk de resultaten met programma gepast1.py. Voor toegelaten invoer (tussen 0 en 500 eurocent) zijn ze niet uit elkaar te houden, maar daarbuiten blijkbaar wel.
  3. Pas programma gepast4.py zó aan dat de uitvoer op één regel als Python uitdrukking wordt geschreven (met * i.p.v. x). Bij invoer 96 dient de uitvoer te zijn:
    1*50 + 2*20 + 1*5 + 1*1
    
    N.B. Bij invoer 0 mag de uitvoer leeg zijn.
    Ontwerpaanwijzing: Voer een extra variabele operator in, met beginwaarde '' (een lege karakterrij). Plak achter betaling telkens
    '%s%d*%d' % ( operator, aantal, munt )
    en maak dan operator = ' + '.
  4. Pas programma gepast4a.py zó aan dat in de uitvoertabel de gebruikte munten van klein naar groot vermeld worden. Bij invoer 96 dient de uitvoer te zijn:
    1 x   1
    1 x   5
    2 x  20
    1 x  50
    
    N.B. Dit is veel lastiger voor elkaar te krijgen, als de uitvoerregels niet in een string worden opgeslagen.
  5. Pas programma gepast4a.py zó aan dat in de uitvoertabel ook de totalen per muntsoort, het totaal aantal munten en het invoerbedrag staan. Bij invoer 96 dient de uitvoer te zijn:
    1 x  50 =  50
    2 x  20 =  40
    1 x   5 =   5
    1 x   1 =   1
    -------------
    5 munten:  96
    
  6. Zoek uit wat er mis gaat in programma gepast4b.py, door een print-opdracht op te nemen die direct na for de waarden van munt en restbedrag met 20 decimalen weergeeft.

Gestructureerde datatypes

Gegevens kunnen in Python op verscheidene manieren gegroepeerd worden. Zo is een karakterrij een rij van karakters. Python kent ook algemenere rij-types, namelijk

waarvan de waardes bestaan uit rijen willekeurige Python-objecten. Een tupelwaarde wordt tussen ronde haken geschreven en een lijstwaarde tussen rechte haken:

Tupelwaarde Lijstwaarde Toelichting
( ) [ ] Lege rij
( 0, ) [ 0 ] Rij met één element; let op de komma bij het tupel
( 1, 'cent' ) [ 1, 'cent' ] Gemengde rij met twee elementen
( ( 0, 1 ), [ 'bit' ] ) [ ( 0, 1 ), [ 'bit' ] ] Rij bestaande uit een tupel en een lijst

Op sommige plaatsen kunnen de ronde tupelhaken zelfs worden weggelaten, zoals achter for ... in. Maar om het lege tupel ( ) moeten altijd haakjes, en ook in de print-opdracht moeten om een tupel altijd haakjes. Bekijk de uitvoer eens die je krijgt als je interactief intikt:

>>> s = (1)          # geen tupel, maar een uitdrukking met haakjes
>>> t = (1,)         # een tupel met een element
>>> u = 1,           # een tupel met een element, haakjes hier niet nodig
>>> print s, t, u    # druk de drie variabelen af met spaties ertussen
1 (1,) (1,)
>>> print (s, t, u)  # druk een tupel af met de drie lijsten als elementen
(1, (1,), (1,))
>>> print 1,         # print 1 zonder overgang op nieuwe regel
1>>>

Het verschil tussen tupels en lijsten is dat tupelobjecten onveranderbaar (Eng.: immutable) zijn, net als getallen en karakterrijen, maar lijstobjecten wel veranderdbaar (Eng.: mutable) zijn. Op dit moment maakt dat nog niet veel uit. We gebruiken i.h.a. liever lijsten dan tupels vanwege de extra flexibiliteit.

Laat r een rij zijn. Dan is len(r) de lengte van de rij, d.w.z. het aantal elementen in de rij. De objecten in een rij zijn genummerd van 0 t/m len(r)-1. Zo'n volgnummer wordt ook index genoemd. Als r een rij is en i een geheel getal met 0<=i<len(r), dan is r[i] het element met index i.

Met r[i:j] wordt de deelrij van r[i] tot en zonder r[j] aangeduid. Als je i weglaat wordt daar nul voor genomen (het begin), en als je j weglaat wordt daar de lengte van de rij voor genomen (het eind). Zo staat r[ : ] voor (een kopie van) de hele rij. Een eigenschap van deze notatie is dat geldt

r[i:j] == r[i:k] + r[k:j]

Een negatieve index wordt opgevat als tellend vanaf het eind, dus alsof er de lengte bijgeteld is.

Zo staat r[1:-1] voor de rij met weglating van eerste en laatste element. Al deze bewerkingen gelden natuurlijk ook voor karakterrijen.

Er is een ingebouwde functie range(ab) die de lijst bestaande uit de gehele getallen a tot en zonder b levert. Bijvoorbeeld een tabel met alle kwadraten van 1 tot 20 (excl.) is te verkrijgen met:

for n in range ( 1, 20 ) :
    print '%2d^2 = %3d' % ( n, n*n )

Laten we deze mogelijkheden toepassen in een programma dat bepaalt voor welk bedrag de minimale betaling een maximaal aantal munten vereist. Terwille van de overzichtelijkheid geven we de lijst van te gebruiken munten de naam euromunten. Dit noemen we een constante, omdat we de waarde van euromunten in het programma niet meer willen veranderen.

## Definieer constanten
euromunten = [ 1, 2, 5, 10, 20, 50, 100, 200 ]  # euromunten

We gebruiken een lijst i.p.v. een tupel voor de munten, omdat we de lijst nog willen sorteren en omkeren. Door dat apart te doen geeft het niet hoe de munten zijn opgesomd in de constante.

Het nieuwe programma doorloopt alle mogelijke bedragen van 0 t/m 500, bepaalt daarbij voor elk bedrag uit hoeveel munten de minimale betaling bestaat, en houdt ondertussen bij wat het grootste aantal gebruikte munten is (maxaantal) en voor welk bedrag (maxbedrag). Tenslotte drukt het programma het gevonden bedrag en aantal af. Zo ziet programma gepast5.py er uit (asserties zijn weggelaten):

"""Bepaal maximale aantal munten voor minimale betaling met euromunten
   van bedragen tussen 0 en 500 eurocent.
"""

## Definieer constanten
euromunten = [ 1, 2, 5, 10, 20, 50, 100, 200 ]  # euromunten

## Initialiseer de lus-variabelen
munten = euromunten  # de beschikbare munten
munten.sort()  # garandeer dat munten van klein naar groot staan
print "Beschikbare munten:", munten
munten.reverse()  # van groot naar klein vanwege het pas-algoritme
maxaantal = 0  # maximale aantal munten gebruikt in minimale betaling
maxbedrag = 0  # bedrag waarvoor minimale betaling maxaantal munten vergt

## Doorloop alle bedragen om maxaantal en maxbedrag te bepalen
for bedrag in range ( 0, 501 ) :

    ## bepaal minimale betaling van bedrag
    aantal = 0           # aantal munten in de betaling
    restbedrag = bedrag  # resterende te passen bedrag

    for munt in munten :
        aantal = aantal + restbedrag // munt
        restbedrag = restbedrag % munt

    ## pas maxaantal en maxbedrag aan
    if aantal > maxaantal :
        maxaantal = aantal
        maxbedrag = bedrag

## Druk maximale aantal munten en bedrag af
print "Minimale betaling van", maxbedrag, "vergt", maxaantal, "munten"

De uitvoer van het programma is:

Beschikbare munten: [1, 2, 5, 10, 20, 50, 100, 200]
Minimale betaling van 388 vergt 8 munten

Meer Oefeningen

  1. Probeer interactief (achter >>>) de volgende lijst operaties en ga na of of je de resultaten begrijpt (print is interactief niet nodig; je kan "knippen en plakken"):
          range(4)
          range(10, 20)
          range(10, 20, 3)
          [1, 4, 9] + [2, 4, 6, 8]
          r = ['alfa', 'beta', 'gamma', 'delta']
          r[1]
          r[-1]
          r[1:3]
          r[:2]
          r[2:]
          len(r)
          max(r)
          min(r)
          r = zip(r, range(len(r)))
          r
          t = 'epsilon', len(r)
          t
          r.append(t)
          r
          r.reverse()
          r
          r.sort()
          r
          r.extend(t)
          r
        
  2. Pas het programma gepast5.py zó aan dat het de oude Nederlandse munten gebruikt:
    ## definieer constanten
    euromunten = [ 1, 2, 5, 10, 20, 50, 100, 200 ]  # euromunten
    oudeNLmunten = [ 1, 5, 10, 25, 100, 250, 500 ]  # oude NL munten
    
    Wat is nu het kleinste bedrag met maximaal aantal munten in de minimale betaling?
    N.B.Als je de losse cent weglaat, dan zijn alleen vijfvouden gepast te betalen. Dat kun je opvangen door bedrag te laten lopen over de lijst range(0, 501, 5), die alleen de vijfvouden van 0 t/m 500 bevat. Wat is nu het antwoord?
  3. Pas het programma gepast1.py zó aan dat het de betaling uitvoert als lijst van gebruikte munten. Bijvoorbeeld de invoer 96 geeft dan de uitvoer [50, 20, 20, 5, 1].

    In het ontwerp kun je variabele betaling toevoegen om de lijst van gebruikte munten bij te houden. Variabele aantal is dan niet meer nodig. De volgende opdrachten volstaan in het programma:

    betaling = [ ]  # lijst met gebruikte munten
    
    for munt in ... :
        while bedrag >= munt :
            betaling.append ( munt )
            bedrag = bedrag - munt
    
    print betaling
    
    Met delen kan het als volgt:
    betaling = [ ]  # lijst met gebruikte munten
    
    for munt in ... :
        aantal = bedrag // munt
        betaling.extend ( aantal * [ munt ] )
        bedrag = bedrag % munt
    
    print betaling
    

Samenvatting

Mensen begaan fouten; dat is onvermijdelijk. Deze kunnen leiden tot defecten in (tussen)producten, waaronder de programmacode. Bij uitvoering van een programma kan een defect zich uiten doordat het programma faalt, d.w.z. niet voldoet aan de verwachtingen.

Defecten dienen zo snel mogelijk opgespoord en geëlimineerd te worden. Het is daarom belangrijk op twee manieren te zoeken naar defecten in programmacode:

Volg de checklists. Veelvoorkomende manieren van falen

Houd er rekening mee dat de plaats waar een programma faalt veelal niet de plaats is waar het achterliggende defect zit. Het kan al eerder mis zijn gegaan. Kijk verder dan je neus lang is en zorg vanaf het begin voor goed leesbare code. Voorkomen is beter dan genezen.

Er zijn verscheidene vormen van commentaar te onderscheiden:

Python kent verscheidene soorten gegevens (data types):

Interactief gebruik van Python (intikken achter >>>) is een geschikte manier om de syntax en semantiek te verkennen, maar niet om hele programma's te ontwikkelen. Bij interactief gebruik is print niet nodig om de waarde van een uitdrukking af te laten drukken.

[ Deel 3 ]


Copyright © 2003-2004, Tom Verhoeff
Validate HTML