|
Hoofdstuk 16 Klassen and functies
16.1 Tijd
Als een ander voorbeeld van een door de gebruiker gedefinieerd type zullen we de klasse Tijd definiëren die de tijd van de dag registreert. De definitie ziet er zo uit:
We maken een nieuw Tijd object aan en kennen de attributen uren, minuten en seconden toe:
Het toestandsdiagram voor het object Tijd ziet er zo uit:
Oefening 16.1 Schrijf een functie genaamd print_tijd die een Tijdobject ontvangt en deze weergeeft in het formaat uur:minuut:seconde. Aanwijzing: het patroon '%.2d' print een geheel getal, bestaande uit tenminste twee cijfers, en indien nodig een voorloopnul.
Oefening 16.2 Schrijf een booleaanse functie genaamd is_na dat twee Tijdobjecten ontvangt, t1 en t2, en True teruggeeft als t1 chronologisch volgt op t2. Anders moet het False teruggeven. Uitdaging: gebruik geen if instructie.
16.2 Pure functies
In de volgende paar secties, zullen we twee functies schrijven die tijdwaarden toevoegen. Deze demonstreren twee soorten functies: pure functies (pure functions) en veranderaars (modifiers). Zij demonstreren ook een ontwikkelplan dat ik prototype en corrigeren (prototype and patch) noem. Dit is een manier van aanpakken van complexe problemen door te beginnen met een simpel prototype en incrementeel de complicaties aan te pakken.
Hier is een eenvoudig prototype van voeg_tijd_toe:
De functie maakt een nieuw Tijdobject aan, initialiseert zijn attributen en geeft een referentie naar het nieuwe object terug. Dit wordt een pure functie genoemd omdat het geen objecten wijzigt, die als argumenten worden meegegeven. Ook heeft het geen effecten, zoals het weergeven van een waarde of het vragen om invoer aan de gebruiker, anders dan het teruggeven van een waarde.
Om deze functie te testen, zal ik twee Tijdobjecten maken; start bevat de starttijd van een film, zoals Monty Python and the Holy Grail en duur bevat de looptijd van de film; deze is 1 uur en 35 minuten.
voeg_tijd_toe zoekt uit op welk tijdstip de film afgelopen is.
Het resultaat, 10:80:00 zal niet zijn wat u ervan verwachtte. Het probleem is dat deze functie niet werkt in het geval dat het aantal seconden of minuten boven de 60 uitkomt. Zodra dat gebeurt moeten we extra seconden naar de minutenkolom en extra minuten naar de urenkolom overhevelen.
Hierbij de verbeterde versie:
Hoewel deze functie correct is, wordt deze wel erg groot. We komen later een korter alternatief tegen.
16.3 Veranderaars
Soms is het handig voor een functie om de als parameters ontvangen objecten te veranderen. In dat geval zijn de veranderingen zichtbaar voor de aanroeper. Functies die op deze manier werken, worden veranderaars (modifiers) genoemd.
verhoging, die een gegeven aantal seconden toevoegt aan het Tijdobject, kan natuurlijk als een veranderaar worden geschreven. Hier is een ruwe schets:
De eerste regel voert een simpele opdracht uit; de rest handelt de speciale zaken af zoals we die hiervoor zagen.
Is deze functie correct? Wat gebeurt er als de parameter seconden groter is dan 60?
In dat geval is het niet genoeg om één keer iets over te dragen. We moeten dat blijven doen totdat tijd.seconde kleiner is dan 60. Een oplossing is het vervangen van de if instructie door een while instructie. Dat zorgt ervoor dat de functie correct werkt maar het is niet erg efficiënt.
Oefening 16.3 Schrijf een correcte versie van verhoging die geen enkele lus bevat.
Alles wat met veranderaars kan worden gedaan, kan ook met pure functies (pure functions) worden gedaan. In feite accepteren bepaalde programmeertalen alleen pure functies. Er is enig bewijs dat programma's die pure functies gebruiken sneller te ontwikkelen en minder foutgevoelig zijn dan programma's die veranderaars gebruiken. Maar in bepaalde gevallen zijn veranderaars handig en programma's met veel functies zijn wel eens minder efficiënt.
In het algemeen is mijn aanbeveling om pure functies te schrijven wanneer het mogelijk is en veranderaars alleen te gebruiken wanneer er een overduidelijk voordeel is te behalen. Deze aanpak wordt wel een functionele programmeerstijl genoemd.
Oefening 16.4 Schrijf een "pure" versie van verhoging die een nieuw Tijdobject aanmaakt en teruggeeft, in plaats van het wijzigen van de parameter.
16.4 Prototype ontwikkeling of plannen
Het gedemonstreerde ontwikkelplan wordt "prototype en corrigeer" (prototype and patch) genoemd. Voor elke functie schreef ik een prototype die de basisberekening uitvoerde; die ik vervolgens testte en waarin ik ondertussen de fouten corrigeerde.
Deze aanpak kan effectief zijn, in het bijzonder als u geen diepgaande kennis hebt over het probleem. Maar correcties op correcties maken de code onnodig gecompliceerd, omdat het veel speciale gevallen kent. Ook wordt de code onbetrouwbaar, omdat onduidelijk is of we alle fouten hebben gevonden.
Een alternatief is geplande ontwikkeling, waarbij een op hoog niveau inzicht hebben in het probleem, het programmeren een stuk eenvoudiger maakt. In dit geval is het inzicht dat het Tijdobject eigenlijk een driecijferig nummer is met grondtal 60 (zie nl.wikipedia.org/wiki/Sexagesimaal)! Het tweede attribuut is de "enen kolom", Het minuut attribuut is de "zestig kolom" en het uur attribuut is de "zesendertighonderd kolom".
Toen we voeg_tijd_toe en verhoging schreven, hebben we effectief een optelling met grondtal 60 uitgevoerd, daarom moesten we van de ene kolom naar de andere overdragen.
Deze waarneming suggereert een andere aanpak van het complete probleem, we kunnen het Tijdobject omzetten naar gehele getallen en gebruikmaken van het feit dat de computer kan rekenen met gehele getallen.
Hierbij een functie die de tijden omzet in gehele getallen:
En hieronder de functie die gehele getallen omzet in Tijd (ik herinner u eraan dat divmod het eerste argument deelt door het tweede en het quotiënt en de rest als een koppel terug geeft).
U moet er mogelijk even over nadenken en testen uitvoeren om u ervan te overtuigen dat deze functies correct zijn. Een manier om deze te testen, is om te controleren dat tijd_naar_int(int_naar_tijd(x)) == x voor een groot aantal waarden van x geldt. Dit is een voorbeeld van een controle op consequentie.
Bent u er eenmaal van overtuigd dat de functies correct zijn, dan zijn deze te gebruiken om voeg_tijd_toe te herschrijven:
Deze versie is korter dan de oorspronkelijke en eenvoudiger te controleren.
Oefening 16.5 Herschrijf verhoging door van tijd_naar_int en int_naar_tijd gebruik te maken.
In sommige opzichten is het omzetten van grondtal 60 naar grondtal 10 en weer terug lastiger dan met tijden te werken. Grondtallen omzetten is abstracter en intuïtief werken we beter met tijden.
Maar als we het inzicht hebben om tijd te behandelen als getallen met grondtal 60 en tijd besteden aan het schrijven van conversie functies (tijd_naar_int en int_naar_tijd), krijgen we een programma dat korter, eenvoudiger te lezen en te debuggen is en betrouwbaarder werkt.
Het is ook gemakkelijker om later onderdelen toe te voegen. Bijvoorbeeld: twee Tijden van elkaar aftrekken om de duur te bepalen. De naïeve aanpak zou de implementatie zijn van aftrekking met lenen. Door de conversie functies te gebruiken wordt dit eenvoudiger en waarschijnlijk eerder correct.
Ironisch genoeg is het moeilijker maken van een probleem (of meer algemeen) aan de andere kant weer gemakkelijker (omdat er minder speciale gevallen en minder kansen op fouten zijn).
16.5 Debuggen
Een Tijdobject is goed opgezet als de waarden voor minuten en seconden tussen de 0 en 60 liggen (inclusief 0 maar geen 60) en als de uren positief zijn. uren en minuten moeten gehele waarden zijn, maar voor seconds mogen we cijfers achter de komma toestaan.
Dit soort vereisten worden invarianten (invariants) genoemd, omdat deze altijd waar moeten zijn. Anders gezegd: als deze niet waar zijn, is er iets verkeerd gegaan.
Code schrijven om uw invarianten te controleren helpt u om de fouten en hun oorzaken te vinden. Bijvoorbeeld: u kunt een functie hebben zoals geldige_tijd die een Tijdobject ontvangt en False teruggeeft als deze een invariant schendt:
Controleer aan het begin van elke functie de argumenten om er zeker van te zijn dat deze geldig zijn:
Of u kunt een assert instructie gebruiken, die een gegeven invariant controleert en een foutmelding geeft als het fout gaat:
assert instructies zijn handig omdat deze code - die met normale vergelijkingen werkt - zich onderscheidt van code die op fouten controleert.
16.6 Woordenlijst
prototype en corrigeren: Een ontwikkelplan dat het schrijven van een ruw ontwerp omvat plus het testen en corrigeren van fouten zodra die zijn gevonden.
Geplande ontwikkeling: Een ontwikkelplan dat op hoog niveau inzicht in het probleem en meer planning vereist dan incrementeel ontwikkelen of prototype-ontwikkeling.
pure functie: Een functie die geen enkele van de objecten die zij ontvangt, zoals argumenten, wijzigt. De meeste pure functies zijn productief.
veranderaar: Een functie, die één of meer objecten, die als argumenten meegegeven zijn, wijzigt. De meeste veranderaars zijn niet zo productief.
functionele programmeerstijl: Een stijl van programmaontwerp waarin hoofdzakelijk pure functies gebruikt zijn.
invariant: Een conditie die altijd "true" dient te zijn tijdens de uitvoering van een programma.
16.7 Oefeningen
Oefening 16.6 Schrijf een functie genaamd vermenigvuldig_tijd die een Tijdobject en een getal ontvangt en een nieuw Tijdobject teruggeeft dat het product bevat van de oorspronkelijke Tijd en het getal.
Gebruik daarna vermenigvuldig_tijd om een functie te schrijven die een Tijdobject ontvangt, dat de aankomsttijd (de tijd wordt op 0 gestart) in een race voorstelt, en een getal, dat de afstand voorstelt; en een Tijdobject teruggeeft, dat de gemiddelde snelheid voor moet stellen (km per uur).
Oefening 16.7 Schrijf een klassedefinitie voor een Datumobject die de attributen dag, maand en jaar heeft. Schrijf een functie genaamd verhoog_datum die het Datumobject datum ontvangt en een geheel getal n, en een nieuw Datumobject teruggeeft, dat de dag n dagen na datum voorstelt. Aanwijzing: "September heeft dertig dagen ..." Uitdaging: gaat uw functie correct om met schrikkeljaren? Zie nl.wikipedia.org/wiki/Schrikkeljaar.
Oefening 16.8 De datetime module levert datum en tijd objecten die gelijk zijn aan de Datum- en Tijdobjecten in dit hoofdstuk maar deze leveren een uitgebreide set aan methoden en operatoren. Lees de documentatie erop na bij docs.python.org/lib/datetime-date.html.
Gebruik de datetime module om een programma te schrijven dat de huidige datum ophaalt en de dag van de week print.
- Schrijf een programma dat een verjaardag als invoer ontvangt en de leeftijd van de gebruiker en het aantal dagen, uren, minuten en seconden tot aan zijn volgende verjaardag print.