Wat in dit deel aan bod komt:
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.
Zoals we in Deel 1 al opmerkten, zijn er twee methodes om defecten in programma's te constateren:
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.
Er zijn twee vormen van falen te onderscheiden bij Python programma's.
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.
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.]
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).
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:
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.
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 : ...
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 ...
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).
Er zijn ook slechte vormen van commentaar:
Goed commentaar legt uit waarom het programma zo in elkaar zit.aantal = aantal + 1 # verhoog aantal met 1 [OVERBODIG] bedrag = bedrag - munt # trek munt af van bedrag [OVERBODIG]
while bedrag >= munt : # munt is gebruikt [VERWARREND] aantal = aantal + 1 bedrag = bedrag - munt # trek bedrag af van munt [FOUT]
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.
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
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.
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:
## 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.
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 %%.
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.
Hier volgen wat oefeningen met de nieuwe stof.
1*50 + 2*20 + 1*5 + 1*1 |
'%s%d*%d' % ( operator, aantal, munt )en maak dan operator = ' + '.
1 x 1 1 x 5 2 x 20 1 x 50 |
1 x 50 = 50 2 x 20 = 40 1 x 5 = 5 1 x 1 = 1 ------------- 5 munten: 96 |
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(a, b) 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
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
Wat is nu het kleinste bedrag met maximaal aantal munten in de minimale betaling?## definieer constanten euromunten = [ 1, 2, 5, 10, 20, 50, 100, 200 ] # euromunten oudeNLmunten = [ 1, 5, 10, 25, 100, 250, 500 ] # oude NL munten
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?
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:
Met delen kan het als volgt:betaling = [ ] # lijst met gebruikte munten for munt in ... : while bedrag >= munt : betaling.append ( munt ) bedrag = bedrag - munt print betaling
betaling = [ ] # lijst met gebruikte munten for munt in ... : aantal = bedrag // munt betaling.extend ( aantal * [ munt ] ) bedrag = bedrag % munt print betaling
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.