NETWERKEN en OBJECTORIËNTATIE

Vergelijkbare documenten
NETWERKEN en OBJECTORIËNTATIE

Objectgericht Programmeren. (in Python)

N&O: Objectgericht Programmeren. (in Python)

Datatypes Een datatype is de sort van van een waarde van een variabele, veel gebruikte datatypes zijn: String, int, Bool, char en double.

Het relaas van de beginnende programmeur. Het hoe en waarom van de assistent

Maak automatisch een geschikte configuratie van een softwaresysteem;

Programmeermethoden NA. Week 5: Functies (vervolg)

OEFENINGEN PYTHON REEKS 1

Objectgericht programmeren 1.

OEFENINGEN PYTHON REEKS 1

Variabelen en statements in ActionScript

Programmeermethoden NA. Week 5: Functies (vervolg)

[15] Variabelen in functies (of: een woordje over scope)

Programmeren (1) Examen NAAM:

Vakgroep CW KAHO Sint-Lieven

Inleiding Programmeren 2

[14] Functies. Volg mee via 14_Functies-1.py. We beginnen met een eenvoudig voorbeeldje:

Inleiding Programmeren 2

Programmeermethoden NA

Een spoedcursus python

Programmeermethoden NA. Week 6: Lijsten

BEGINNER JAVA Inhoudsopgave

VAN HET PROGRAMMEREN. Inleiding

Beginselen van programmeren Practicum 1 (Doolhof) : Oplossing

4 ASP.NET MVC. 4.1 Controllers

OEFENINGEN PYTHON REEKS 1

HOE TEKEN IK EEN OMGEVINGSMODEL

Je gaat leren programmeren en een spel bouwen met de programmeertaal Python. Websites zoals YouTube en Instagram zijn gebouwd met Python.

Zoemzinnen. Algemene info. Functies met een variabel aantal argumenten

[13] Rondjes draaien (loops)

Inleiding Programmeren 2

INHOUDSOPGAVE. Over de auteur, de illustrator en de technische redacteuren 13

Overerving & Polymorfisme

Arrays. Complexe datastructuren. Waarom arrays. Geen stijlvol programma:

Modelleren en Programmeren

II. ZELFGEDEFINIEERDE FUNCTIES

Les 3. Gebruik in volledige programma Default argumenten Vergelijken van objecten

VAN HET PROGRAMMEREN. Inleiding. Het spiraalmodel. De programmeertaal. vervolgens de berekening van het totale bedrag, incl. BTW:

1 Delers 1. 3 Grootste gemene deler en kleinste gemene veelvoud 12

Visual Basic.NET. Visual Basic.NET. M. den Besten 0.3 VB. NET

Python. Vraag 1: Expressies en types. Vraag 1 b: Types -Ingebouwde functies- Vraag 1 a 3/10/14

Een eenvoudig algoritme om permutaties te genereren

10 Meer over functies

Als een PSD selecties bevat, deelt de lijn van het programma zich op met de verschillende antwoorden op het vraagstuk.

OEFENINGEN PYTHON REEKS 5

Uitleg van de Hough transformatie

Informatica. Deel II: les 1. Java versus Python. Jan Lemeire Informatica deel II februari mei Parallel Systems: Introduction

Computervaardigheden. Universiteit Antwerpen. Computervaardigheden en Programmatie. Grafieken en Rapporten 1. Inhoud. Wat is scripting?

Uitwerking Tweede deeltentamen Imperatief programmeren - versie 1 Vrijdag 21 oktober 2016, uur

start -> id (k (f c s) (g s c)) -> k (f c s) (g s c) -> f c s -> s c

MINICURSUS PHP. Op dit lesmateriaal is een Creative Commons licentie van toepassing Sebastiaan Franken en Rosalie de Klerk Bambara

PYTHON REEKS 2: FUNCTIES. Mathias Polfliet

[8] De ene 1 is de andere niet

Omschrijf bij ieder onderdeel van de methode de betekenis ervan. Java kent twee groepen van klassen die een GUI kunnen maken: awt en swing.

Programmeren: Visual Basic

Toets Programmeren, 2YP05 op donderdag 13 november 2008, 09:00-12:00

Javascript oefenblad 1

Dynamiek met VO-Script

Niet-numerieke data-types

Algemeen. Rorschachtest. Algemene info

HOOFDSTUK 3. Imperatief programmeren. 3.1 Stapsgewijs programmeren. 3.2 If Then Else. Module 4 Programmeren

Een gelinkte lijst in C#

Labo 2 Programmeren II

Cursus MSW-Logo. Def. Recursie: recursie is het oproepen van dezelfde functie of procedure binnen de functie of procedure

Getallen 1 is een computerprogramma voor het aanleren van de basis rekenvaardigheden (getalbegrip).

Modulewijzer InfPbs00DT

van PSD naar JavaScript

Objectgeoriënteerd programmeren in Java 1

Numerieke aspecten van de vergelijking van Cantor. Opgedragen aan Th. J. Dekker. H. W. Lenstra, Jr.

Een inleiding in de Unified Modeling Language 79

Abstracte klassen & Interfaces

Instructie voor Docenten. Hoofdstuk 13 OMTREK EN OPPERVLAKTE

Informatica: C# WPO 9

PYTHON REEKS 1: BASICS. Mathias Polfliet

Uitleg: In de bovenstaande oefening zie je in het eerste blokje een LEES en een SCHRIJF opdracht. Dit is nog lesstof uit het tweede trimester.

Opgaven. Python Assessment

Programmeren. a. 0, 0, 0 b. 0, 0, 27 c. 15, 12, 0 d. 15, 12, 27

case: toestandsdiagrammen

Scala. Korte introductie. Sylvia Stuurman

[7] Variabelen en constanten

Verder zijn er de nodige websites waarbij voorbeelden van objectgeoriënteerd PHP (of Objec Oriented PHP, OO PHP) te vinden zijn.

Hoofdstuk 0. Van Python tot Java.

Ontwerp van Informatiesystemen

Disclaimer Het bestand dat voor u ligt, is nog in ontwikkeling. Op verzoek is deze versie digitaal gedeeld. Wij willen de lezer er dan ook op wijzen

Programmeren in C++ Efficiënte zoekfunctie in een boek

Les F-02 UML. 2013, David Lans

1 Inleiding in Functioneel Programmeren

return an ; } private I L i s t l i j s t ;

Programmeermethoden. Recursie. week 11: november kosterswa/pm/

Programmeermethoden NA

Instructies zijn niet alleen visueel, maar ook auditief, met hoogkwalitatief ingesproken geluid (geen computerstem).

Practicum Programmeerprincipes

IMP Uitwerking week 13

Gebruik van verschilbestanden

Objectgeoriënteerd Programmeren: WPO 4B

Constanten. Variabelen. Expressies. Variabelen. Constanten. Voorbeeld : varid.py. een symbolische naam voor een object.

Zelftest Inleiding Programmeren

Controle structuren. Keuze. Herhaling. Het if statement. even1.c : testen of getal even of oneven is. statement1 statement2

VISUALISATIE VAN KROMMEN EN OPPERVLAKKEN. 1. Inleiding

PROS1E1 Gestructureerd programmeren in C Dd/Kf/Bd

Transcriptie:

FACULTEIT INDUSTRIELE INGENIEURSWETENSCHAPPEN! CAMPUS DE NAYER! NETWERKEN en OBJECTORIËNTATIE Deel 2: Objectoriëntatie in Python Joost Vennekens

Inhoudsopgave 1 Objecten en klassen 5 1.1 Een beetje geschiedenis...................... 5 1.2 Objecten................................ 7 1.3 Klassen................................. 11 1.4 Constructoren............................ 15 1.5 Magische methodes......................... 18 1.6 Wijzigen van attributen....................... 20 1.7 Voorbeelden.............................. 21 1.7.1 De klasse Drank...................... 21 1.7.2 De klasse Rechthoek................... 22 1.7.3 De klasse Cirkel...................... 22 1.8 Een blik achter de schermen................... 23 2 Samenwerkende objecten 25 2.1 Een object als argument...................... 25 2.2 Een object als resultaat...................... 26 2.3 Associaties tussen klassen.................... 27 2.4 Lijsten en objecten.......................... 29 2.4.1 Objecten met lijsten..................... 30 2.4.2 Lijsten van objecten..................... 31 2.4.3 Objecten met lijsten van objecten............ 31 2.5 Voorbeelden.............................. 34 2.5.1 Punten en veelhoeken................... 34 2.5.2 Cocktails en dranken.................... 36 3 Overerving 39 3.1 Het overschrijven van methodes................. 41 3.2 Overervingshiërarchieën...................... 44 3.3 Methodes uit een superklasse oproepen............ 46 3.4 Voorbeelden.............................. 48 3.4.1 Vierkanten.......................... 48 3

4 Varia 49 4.1 Python als server-side scripting taal.............. 49 4.2 Methodes met default argumenten................ 50 4.3 Vergelijken van objecten...................... 51 4.4 Nog meer vergelijkingen...................... 55 4.5 Uitzonderingen............................ 56 4.5.1 Uitzonderingen opwerpen................. 58 4.5.2 Uitzonderingen afhandelen................ 60 4.5.3 Herwerking van het voorbeeld.............. 62 4.6 Statische methodes......................... 63 4.7 Nog een blik achter de schermen................. 67 4.7.1 De volledige klasse Cirkel................ 68 4.8 Private variabelen.......................... 68 4.9 Eigenschappen (Properties).................... 74 A Practicum opgaves 77 A.1 Setup.................................. 77 A.2 Vogelpik................................ 78 A.3 De eerste objectjes.......................... 80 A.4 Punten, ruimtes en ballen..................... 81 A.5 Tijd voor actie............................ 83 A.6 Muren................................. 85 A.7 Krachten................................ 87 4

Objecten en klassen 1 1.1 Een beetje geschiedenis Als 1 programmeur op 1 dag 100 lijnen Python code kan schrijven, hoeveel lijnen code schrijft hij dan op 10 dagen? Of, hoeveel code schrijft een team van 10 programmeurs dan op 1 dag? Het is vooral voor IT managers verleidelijk om op deze vragen het antwoord 1000 te geven. De praktijk wijst echter uit dat echte antwoord veel minder is. De verklaring daarvoor ligt in het feit dat lijnen code niet onafhankelijk zijn van elkaar. Als de ene lijn een toekenning x = x * 2 doet, en een paar lijnen erna volgt een test op de waarde van x: i f x > 5: print "wat veel!" dan is de eerste lijn code duidelijk relevant voor de tweede lijn. Met andere woorden, om te kunnen begrijpen wat de tweede lijn juist doet, moeten we ons bewust zijn van het bestaan van de eerste lijn. Een programma van 200 regels is daarom niet gewoon maar dubbel zo complex als een programma van 100 regels, maar veel méér: elke lijn van de 200 regels kan immers potentieel relevant zijn voor elke andere lijn. Er zijn dus 200 2 mogelijke interacties tussen lijnen code, die allemaal relevant kunnen zijn voor de programmeur, ten opzichte van 100 2 mogelijke interacties in het kortere programma. De complexiteit van een programma hoe moeilijk het is om dit programma te begrijpen stijgt dus veel sneller dan het aantal lijnen code. Programmeertalen hebben sinds de begindagen van de computer een grote ontwikkeling doorgemaakt. Deze evoluties kunnen best begrepen worden als een voortdurende zoektocht naar manieren om dit fenomeen tegen te gaan. Globaal gesproken is het de bedoeling om grote programma s zoveel mogelijk uiteen te trekken in kleinere stukjes. Als we ons programma van 200 lijnen code kunnen opdelen in twee 5

stukken van elk 100 lijnen én we kunnen dit zodanig doen dat we er zeker van zijn dat geen enkele lijn code uit het ene deel relevant is voor het andere deel, dan blijven en slechts 2 100 2 mogelijke interacties over, wat een pak minder is dan 200 2. Maar hoe kunnen we dat nu verwezenlijken? In het begin zocht men vooral zijn heil in het opsplitsen van de grote taak die een programma moest vervullen in kleinere deeltaken. Dit heeft geleid tot het invoeren van functies. De hoop hierbij was dat men een programma zou kunnen begrijpen door al zijn functies te begrijpen, en dat men elke functie afzonderlijk zou kunnen begrijpen. Men dacht, met andere woorden, dat enkel maar lijnen code binnen dezelfde functie relevant zouden zijn voor elkaar, zodat het zou volstaan om een programma op te splitsen in functies die klein genoeg zijn. Als we onze 200 lijnen code opsplitsen in 10 functies van 20 lijnen, dan zijn er maar 10 20 2 mogelijke interacties, wat een grootte-orde minder is dan de oorspronkelijke 200 2 : eureka! Helaas bleek al snel dat dit in de praktijk toch niet zo goed werkte. De reden hiervoor ligt zelfs nogal voor de hand: als we ons programma gaan opsplitsen in deeltaken, dan moeten we deze deeltaken natuurlijk na elkaar uitvoeren. Maar dat betekent dat de invoer van taak 2 natuurlijk de uitvoer van taak 1 zal zijn! En de invoer van taak 20 is de uitvoer van taak 19, wiens invoer de uitvoer van taak 18 was, wiens invoer de uitvoer was van taak 17, wiens invoer.... We zien al snel dat een taak helemaal niet onafhankelijk is van de taken die ervoor kwamen, maar dat ze hier juist heel erg van afhangt. Dit betekent ook dat als we in taak 1 een aanpassing doen, deze aanpassing mogelijk een effect kan hebben op taak 2, en dus ook op taak 3, en dus ook op.... Als we een regel veranderen in taak 1, dan zouden we dus eigenlijk alle andere regels code uit taken 2 t/m 20 terug moeten gaan bekijken, om te zien of we ze ook niet moeten aanpassen. Dat is duidelijk niet wat we willen. In het begin van de jaren 90 is men dan op zoek gegaan naar een alternatief. Merk op dat men hier dus niet gewoon maar op zoek was naar een nieuwe programmeertaal, maar naar een nieuw wereldbeeld. Er was nood aan een andere manier van kijken: een programma moest niet langer gezien worden als een verzameling van taken die konden worden opgesplitst in deeltaken, maar als... iets anders? De grote doorbraak die er dan gekomen is, is dat men beseft heeft dat een oud concept, bedacht door academici in de jaren 60, eigenlijk perfect het antwoord op deze vraag kon geven. Het concept was dat van objectgericht programmeren, en het antwoord is simpelweg: beschouw een programma niet langer als een verzameling van (deel-)taken, maar als een verzameling van samenwerkende objecten. Hoewel dit antwoord dus al bestaat sinds de jaren 60, heeft het tot de jaren 90 geduurd voor men eindelijk de bijhorende vraag bedacht heeft, namelijk: hoe kunnen we onze programma s zodanig structureren dat zoveel mogelijk 6

regels code onafhankelijk zijn van elkaar? 1.2 Objecten Om echt te kunnen begrijpen hoe het objectgerichte wereldbeeld ertoe kan leiden dat programma s beter (dwz. met meer onafhankelijkheden) gestructureerd worden, is het nodig om eerst wat dieper in te gaan op de betekenis die de term object in deze context heeft. We doen dit door even versneld de geschiedenis van het programmeren door te maken, aan de hand van een eenvoudig voorbeeld. Een cocktail bestaat uit een aantal ingrediënten die in een specifieke verhouding door elkaar gemengd moeten worden. Een vodka-orange bestaat bijvoorbeeld voor een derde uit vodka, en voor de rest uit fruitsap. Dit betekent dat om een glas van 25cl te vullen met vodka-orange, er 8,3cl vodka nodig is en 16,7cl fruitsap. En als we bijvoorbeeld al 10cl vodka hebben ingeschonken, is er nog 20cl fruitsap nodig. Dergelijke berekeningen worden natuurlijk des te uitdagender, naarmate er meer vodka-oranges geconsumeerd worden. Laten we daarom een computerprogramma maken dat ons kan helpen. verhouding = 1.0 / 3.0 cocktail def vodkavoorcocktail ( cocktail ) : return verhouding * cocktail def fruitsapvoorcocktail ( cocktail ) : return (1 verhouding ) * cocktail # aandeel vodka per eenheid def vodkavoorfruitsap ( fruitsap ) : return fruitsap * ( verhouding / (1 verhouding ) ) def fruitsapvoorvodka ( vodka ) : return vodka * ( ( 1 verhouding ) / verhouding ) Deze functies gebruiken we dan natuurlijk als volgt: >>> vodkavoorcocktail(25) 8.3333333333333321 >>> fruitsapvoorvodka(10) 20.000000000000004 In traditionele terminologie hebben we hier gebruik gemaakt van een datastructuur, waarin we de gegevens die we nodig hebben kunnen bijhouden, en daarbij horen een aantal functies, die op basis van deze datastructuur de gewenste uitvoer produceren. Natuurlijk is onze datastructuur hier heel eenvoudig, aangezien hij enkel maar bestaat uit 7

de waarde 1/3. Zelfs voor zo n eenvoudige datastructuur zijn er echter tal van alternatieven te bedenken. Bijvoorbeeld, we hadden in plaats van de hoeveelheid vodka per eenheid cocktail, ook de verhouding tussen de hoeveelheid fruitsap en de hoeveelheid vodka kunnen gebruiken. verhouding = 2.0 # eenheden fruitsap per eenheid vodka def vodkavoorfruitsap2 ( fruitsap ) : return fruitsap / verhouding def fruitsapvoorvodka2 ( vodka ) : return verhouding * vodka def vodkavoorcocktail2 ( cocktail ) : return cocktail / ( verhouding + 1) def fruitsapvoorcocktail2 ( cocktail ) : vodka = vodkavoorcocktail2 ( cocktail ) return fruitsapvoorvodka2 ( vodka ) Deze functies berekenen natuurlijk net dezelfde resultaten als voorheen: >>> vodkavoorcocktail2(25) 8.3333333333333339 >>> fruitsapvoorvodka2(10) 20.0 Hier zien we een eenvoudige illustratie van een belangrijk fenomeen: als we de voorstelling van onze data veranderen, dan moeten we ook alle functies veranderen die deze data gebruiken. Wat zou er gebeuren als we dit zouden vergeten? Dan zouden we bijvoorbeeld per ongeluk onze oude definitie van de functie fruitsapvoorvodka(vodka) samen kunnen gebruiken met onze nieuwe datavoorstelling verhouding = 2.0. Het is duidelijk dat dit een fout resultaat zal opleveren, en een cocktail die veel te licht is. Historisch weetje: In 1999 verloor de NASA $327.600.000 toen de Mars Climate Orbiter missie mislukte. De oorzaak van het feit dat deze satelliet opbrandde in de atmosfeer van Mars, zonder ook maar één zinvol resultaat te produceren, was een software-fout: de ene functie dacht dat een bepaald getalletje een waarde in Newton voorstelde, terwijl de andere dacht dat dit een waarde in Pond was. Of het nu gaat om ontploffende satellieten of cocktails die niet straf genoeg zijn, de les is dezelfde: het is gevaarlijk om een datastructuur los te zien van de functies die hem moeten gebruiken. Deze les is meteen de belangrijkste motivatie voor objectgericht programmeren. 8

Laat ons, voor we verder gaan, eerst eens een meer realistische versie van ons programma bekijken. Het is natuurlijk een beetje belachelijk dat we code geschreven hebben die enkel maar werkt voor vodka-oranges. Met een kleine beetje extra moeite, zouden we code kunnen schrijven die werkt voor alle cocktails. Hiervoor zullen we natuurlijk onze datastructuur iets ingewikkelder moeten maken, en onze functies daaraan aanpassen. # ingredienten per eenheid cocktail vodkaorange = { vodka : 0.33, fruitsap : 0. 67} def ingredientpercocktail ( cocktail, hoeveelheid, ingredient ) : return hoeveelheid * cocktail [ ingredient ] def ingredientperingredient ( cocktail, hoeveelheid, gegeven, gezocht ) : return hoeveelheid * ( cocktail [ gezocht ]/ cocktail [ gegeven ] ) Als we nu bijvoorbeeld willen weten hoeveel vodka er nodig is voor 20cl fruitsap, doen we: >>> ingredientperingredient(vodkaorange, 20, fruitsap, vodka ) 9.8507462686567155 We gebruiken nu als datastructuur een woordenboek (dictionary) en hebben twee functies geschreven die gebruik maken van deze datastructuur. Hier is nu eens een idee: aangezien we toch net besloten hebben dat de functies die een datastructuur gebruiken onafscheidelijk met deze datastructuur verbonden zijn, waarom steken we die functies dan niet gewoon bij in dat woordenboek? In Python kan dit immers perfect: we kunnen met een functie alles doen wat we met bijvoorbeeld een getal of een string kunnen doen. Iets als dit kan bijvoorbeeld perfect: def dubbel ( x ) : return x * 2 print dubbel ( 3 ) functie = dubbel print functie ( 3 ) De toekenning in de voorlaatste lijn geeft de functie dubbel in wezen gewoon een tweede naam, die we zoals de laatste lijn laat zien daarna eveneens kunnen gebruiken om de functie op te roepen. We kunnen dus even goed onze functie nemen en deze bij in de gegevensstructuur van onze cocktail plaatsen. 9

def ingredientpercocktail ( cocktail, hoeveelheid, ingredient ) : return hoeveelheid * cocktail [ ingredient ] def ingredientperingredient ( cocktail, hoeveelheid, gegeven, gezocht ) : verhouding = cocktail [ gezocht ] / cocktail [ gegeven ] return hoeveelheid * verhouding vodkaorange = { vodka : 0.33, fruitsap : 0.67, ipc : ingredientpercocktail, ipi : ingredientperingredient } Eender welke bewerking we met onze vodkaorange willen doen, kunnen we nu voor elkaar krijgen door enkel maar naar dit woordenboek te kijken. Het berekenen van een hoeveelheid fruitsap per hoeveelheid vodka, kan nu bijvoorbeeld zo: vodkaorange [ ipi ] ( vodkaorange, 20, fruitsap, vodka ) Hier komen dus zowel de functie die we toepassen als de data waarop we ze toepassen uit hetzelfde woordenboek. Een groot voordeel van deze aanpak is dat gelijk welke datastructuur we nu kiezen om onze gegevens in voor te stellen, we altijd de juiste functies erbij zullen hebben. Veronderstel bijvoorbeeld dat we, in plaats van te zeggen dat een Bloody Mary voor 0.25 uit vodka bestaat en voor 0.75 uit tomatensap (zoals we hierboven deden voor onze vodka-orange), liever zeggen dat hij één deel vodka moet bevatten per 3 delen tomatensap. bloodymary = { vodka : 1. 0, tomatensap : 3. 0} Bij deze voorstelling horen nu natuurlijk andere functies dan bij onze vodka-orange, namelijk: def ingredientpercocktail2 ( cocktail, hoeveelheid, ingredient ) : som = 0 for drank in cocktail : som += cocktail [ drank ] return hoeveelheid * cocktail [ ingredient ] / som (De functie ingredientperingredient mag dezelfde blijven, aangezien de som daar in zowel teller als noemer zou staan.) Deze functie kunnen we nu ook bij in onze bloodymary steken. bloodymary [ ipi ] = ingredientperingredient bloodymary [ ipc ] = ingredientpercocktail2 Nu lopen we dus nooit het risico dat we ons zullen vergissen tussen ingredientpercocktail en ingredientpercocktail2! 10

(Opmerking voor de aandachtige lezer: Moest je bovenstaande code effectief proberen uit te voeren, zou je merken dat er nog een fout in dit programma zit. Het is instructief om eens na te denken over hoe we deze fout in het algemeen zouden kunnen vermijden.) Er is nog een tweede voordeel, dat mooi geïllustreerd wordt door volgend fragmentje, dat berekent hoeveel vodka we nodig hebben voor 25cl Vodka-orange en 25cl Bloody Mary samen. cocktails = [ vodkaorange, bloodymary ] vodkanodig = 0 for c in cocktails : vodkanodig += c [ ipc ] ( c, 25, vodka ) Hoewel dat aan dit fragmentje helemaal niet te zien is, zal er hier voor onze twee verschillende cocktails een verschillende functie worden opgeroepen. Bovendien zal dit ook telkens de juiste functie zijn! Het feit dat de twee cocktails achter de schermen een verschillende voorstelling voor hun gegevens gebruiken is nu vanuit het oogpunt van bovenstaand fragmentje niet meer relevant. We hebben hier dus een mooie onafhankelijkheid tussen verschillende delen van onze code kunnen realiseren. Deze manier van werken is nu de essentie van het objectgeöriënteerd programmeren. Met de term object bedoelt men immers niet meer of niet minder dan een gegevensstructuur waar alle functies die nodig zijn om deze gegevensstructuur te manipuleren bij inzitten. Samengevat: een gegevensstructuur wéét iets, een functie kán iets, en een object weet niet alleen iets, maar kan daar ook iets mee. Het voornaamste voordeel van de objectgeöriënteerde manier van werken is encapsulatie. Dit betekent dat, zolang we de gegevens die in een object vervat zitten enkel maar manipuleren door middel van de functies die in het object zitten, we helemaal niet hoeven te weten hoe dit object deze gegevens juist voorstelt. Al deze details zitten immers netjes ingekapseld in het object. Hierdoor kunnen we ook op elk moment de gegevensvoorstelling veranderen bijvoorbeeld van onze vodkaorange voorstelling naar de bloodymary zonder dat we aan de rest van ons programma iets hoeven te veranderen. De essentie van objectgeöriënteerd programmeren zit hem dus in de functies die bij in het object zitten. Aangezien dit concept van een functie die bij in een object zit zodanig belangrijk is, heeft men daar dan ook maar meteen een woord voor verzonnen: dit noemt men een methode. object = gegevens + gedrag Een methode is een functie in een object 1.3 Klassen In de vorige sectie hebben we de essentie van objectgericht programmeren uit de doeken gedaan, zonder daarvoor iets meer over 11

Python te zien dan er in de cursus van het 1 e jaar reeds besproken werd. Python is echter een objectgerichte programmeertaal, wat betekent dat deze taal een aantal speciale voorzieningen zogenaamde syntactische suiker aanbiedt om programmeren op de objectgerichte manier aangenamer te maken. Een eerste ding dat al meteen opvalt als we onze cocktailbar verder willen uitbreiden, is dat we nogal veel repetitief tikwerk moeten doen. bloodymary = { vodka : 0.34, tomatensap : 0.66, ipc : ingredientpercocktail, ipi : ingredientperingredient } vodkaorange = { vodka : 0.34, fruitsap : 0.66, ipc : ingredientpercocktail, ipi : ingredientperingredient } gintonic = { gin : 0.34, tonic : 0.66, ipc : ingredientpercocktail, ipi : ingredientperingredient } Al deze cocktail-objects hebben immers dezelfde methodes. Aangezien luiheid een grote deugd is voor een programmeur, zouden we liever gewoon één keer zeggen dat alle cocktail-objectjes deze methodes moeten hebben, in plaats van dit elke keer opnieuw te moeten tikken. Hiervoor bestaat het concept van een klasse: een klasse is een verzameling van objecten die allemaal dezelfde methodes hebben. In het geval van ons voorbeeld, gaan we dus een klasse Cocktail invoeren, de methodes ingredientpercocktail en ingredientperingredient koppelen aan deze klasse, en tot slot zeggen dat bloodymary, vodkaorange en gintonic allemaal Cocktails zijn. Dit gaat als volgt: class Cocktail : def ingredientpercocktail ( cocktail, hoeveelheid, ingredient ) : pass # <- Hier komt nog een berekening def ingredientperingredient ( cocktail, hoeveelheid, gegeven, gezocht ) : pass # <- Hier komt nog een berekening gintonic = Cocktail ( ) bloodymary = Cocktail ( ) vodkaorange = Cocktail ( ) 12

Met het sleutelwoord class definiëren we dus een klasse met een bepaalde naam, waarbij we dan alle methodes van deze klasse opsommen. Nadien kunnen we de naam van deze klasse gebruiken als een functie die een nieuw objectje aanmaakt, dat tot deze bepaalde klasse behoort. We zeggen dan ook wel dat dit object een instantiatie van deze klasse is. Het netto-effect is dus dat aan elk object dat door middel van de uitdrukking Cocktail() wordt aangemaakt, de twee methodes worden toegevoegd die in de declaratie van deze klasse zijn opgenomen. In de voorgaan sectie hebben we een woordenboek gebruikt om een object voor te stellen. In Python zijn echte objecten (dwz. objecten die zijn aangemaakt op basis van een class) een klein beetje verschillend van woordenboeken. Daar waar we in een woordenboek volgende notatie gebruiken om aan een nieuw sleutel-waarde paar toe te voegen: woordenboek [ sleutel ] = waarde doen we dat met een object als volgt: object. sleutel = waarde Ook hier is een beetje terminologie voor: we noemen sleutel in dit geval een attribuut van het object. Nadat we dus bovenstaande Cocktails hebben aangemaakt, kunnen we er als volgt ingrediënten aan toevoegen: gintonic. gin = 0.34 gintonic. tonic = 0.66 Nu heeft het object gintonic dus twee methodes (namelijk de methodes ingredientpercocktail en ingredientperingredient van zijn klasse) en twee attributen (gin en tonic). De notatie om methodes en attributen van een object aan te spreken is trouwens identiek dezelfde, alleen zijn er natuurlijk ook haakjes en argumenten nodig om een methode op te roepen. gintonic. ingredientpercocktail ( argumenten ) Zonder de haakjes en argumenten, zouden we de methode niet oproepen, maar krijgen we gewoon deze methode zelf terug, zoals te zien is in de Python interpreter: >>> gintonic.ingredientpercocktail <bound method Cocktail.ingredientPerCocktail of < main.cocktail instance at 0x50da08>> Laten we nu deze methode ook eens implementeren. In de vorige sectie hadden we deze functie: def ingredientpercocktail ( cocktail, hoeveelheid, ingredient ) : return hoeveelheid * cocktail [ ingredient ] De gegevens in een object heten attributen 13

Nu we van de cocktail een object gemaakt hebben in plaats van een woordenboek, moeten we deze functie natuurlijk aanpassen. Het idee is dat we eigenlijk dit zouden willen doen: def ingredientpercocktail ( cocktail, hoeveelheid, ingredient ) : return hoeveelheid * cocktail. ingredient Maar dit zal helaas niet werken! Deze code zal immers op zoek gaan naar een (niet-bestaand) attribuut ingredient van het object gintonic. Terwijl we eigenlijk willen dat als we deze functie oproepen met als laatste argument bv. de string gin, dat dan het attribuut cocktail.gin gezocht zou worden. Gelukkig kent Python speciaal voor dit probleem een functie getattr(object,naam), waarvan het tweede argument een string moet zijn. Met andere woorden, als we getattr(gintonic, gin ) doen, dan zal het attribuut gintonic.gin worden opgehaald. Hiermee wordt de definitie van onze klasse dan: class Cocktail : def ingredientpercocktail ( z e l f, hoeveelheid, ingredient ) : return hoeveelheid * getattr ( z e l f, ingredient ) Het eerste argument van een methode is zelf def ingredientperingredient ( z e l f, hoeveelheid, gegeven, gezocht ) : verhouding = getattr ( z e l f, gezocht ) /getattr ( z e l f, gegeven ) return hoeveelheid * verhouding We hebben nu ook nog een tweede, kleine wijziging gedaan tov. onze vorige code. We hebben het eerste argument van onze methodes hernoemd naar zelf. In Python is het eerste argument van een methode altijd het object waarbij de methode hoort. De conventie is om dit argument altijd deze naam te geven (of self in Engelstalige code). We zouden verwachten dat we deze methode nu als volgt kunnen oproepen: gintonic. ingredientpercocktail ( gintonic, 20, gin ) Dit is echter niet helemaal juist. In werkelijkheid is het namelijk nog net iets eenvoudiger. Het eerste argument van deze methode-oproep zal immers toch altijd hetzelfde zijn als het object waarin de methode zelf zit. Meer nog, het feit dat de methode gegevens manipuleert die in haar eigen object zitten is net de essentie van objectgericht programmeren! Daarom zal Python dit eerste argument impliciet achter de schermen zelf doorgeven, zonder dat wij dit zelf hoeven te doen gintonic. ingredientpercocktail ( 20, gin ) 14

wordt egeven impliciet Hoewel er dus in de definitie van deze methode drie argumenten waren, moeten we er bij de oproep van deze methode slechts twee zelf expliciet doorgeven. Het ontbrekende argument is het object waarop de methode wordt opgeroepen (gintonic, in dit geval), dat als impliciet eerste argument wordt doorgegeven. 1.4 Constructoren We hebben tot dusver de ingrediënten van onze cocktails gewoon voorgesteld door een string ( gin, vodka,... ). Als we in ons programma meer moeten weten over een drank dan enkel maar zijn naam, dan zal deze voorstelling ontoereikend zijn en hebben we nood aan een gegevensstructuur waarin we al de relevante informatie over een drank kunnen bijhouden. Hiervoor kunnen we natuurlijk ook weer objecten gaan gebruiken. We hebben dan een klasse nodig, die we hier Drank gaan noemen. Het is vaak nuttig om, vooraleer we effectief Python code gaan schrijven, even kort samen te vatten wat wij juist van plan zijn, door de attributen en methodes van de klasse op te lijsten. We doen dit in de vorm van een info-kaartje, dat er zo uitziet: Klasse Drank Attr. naam : string alcoholpercentage : R prijs : R Meth. Op dit ogenblik, zijn we dus niet van plan om methodes te voorzien in onze Drank-objecten. Dit betekent dat we eigenlijk evengoed een woordenboek zouden kunnen gebruiken om deze gegevens in voor te stellen, als een object. Er zijn twee goede redenen om toch voor een object te kiezen. Ten eerste is het verwarrend om in hetzelfde programma sommige gegevens voor te stellen door een object en sommige door een woordenboek; in een objectgericht programma kiezen we dus best zoveel mogelijk voor objecten. Ten tweede zou het altijd nog kunnen dat we later alsnog methodes blijken nodig te hebben. Als we van in het begin voor objecten gekozen hebben, is dit een veel eenvoudigere operatie, dan wanneer we een woordenboek zouden moeten gaan omvormen tot een object. Wegens het gebrek aan methodes, krijgen we dus een klasse definitie die leeg is. class Drank: pass Deze lege klasse dient vooral om ons nu alvast een plaats te geven waar we later als we het programma verder gaan uitbreiden eventuele 15

methodes van de klasse Drank kunnen gaan toevoegen. Het feit dat de definitie van deze klasse leeg is, belet ons natuurlijk niet om objecten hiervan aan te maken. gin = Drank ( ) gin.naam = gin gin. alcoholpercentage = 0.15 gin. p r i j s = 12 vodka = Drank ( ) vodka.naam = vodka vodka. alcoholprecentage = 0.40 vodka. p r i j s = 20 We zien hier opnieuw een hoop tikwerk opduiken, met bovendien het risico op moeilijk te vinden fouten. Zo staat er in het voorbeeld hierboven een tikfout, die op dit moment nog ongemerkt voorbij zal gaan, maar ongetwijfeld later voor problemen gaat zal zorgen als we het alcoholpercentage van onze dranken willen raadplegen. Beter is het dus om een methode te definiëren die de verschillende attributen van een Drank invult. class Drank: def vulattributenin ( z e l f, naam, perc, p r i j s ) : z e l f.naam = naam z e l f. alcoholpercentage = perc z e l f. p r i j s = p r i j s gin = Drank ( ) gin. vulattributenin ( gin, 0.15, 12) vodka = Drank ( ) vodka. vulattributenin ( vodka, 0.40, 20) In dit voorbeeld zien we dat we, telkens als we een Drank aanmaken, we als eerste werk de initializatie-functie vulattributenin hierop gaan oproepen. Dit zal bovendien in heel ons programma waarschijnlijk altijd zo zijn. Python laat ons toe om ons programma nog wat compacter te maken door deze twee stappen het aanmaken van een object en het initializeren ervan in één instructie uit te voeren. Het enige dat we hiervoor moeten doen, is onze initializatie-methode een speciale naam geven: init. De naam van deze functie bestaat dus uit het woordje init (als afkorting van initializatie), voorafgegaan en gevolgd door telkens twee underscores _. De reden voor de underscores is dat Python deze notatie gebruikt voor dingen die op één of andere manier speciaal zijn, in de zin dat Python zelf er achter de rug van de programmeur dingen mee zal 16

doen. Deze notatie oogt een beetje vreemd, maar dat is eigenlijk precies de bedoeling: de underscores dienen als een waarschuwing voor mensen die de code zouden lezen zonder de speciale functie te kennen. Als ze de underscores zien, dan weten ze dat deze functie iets speciaals doet, en dat ze best eens de Python documentatie erop zouden naslaan om te weten te komen wat dit speciale juist is. Deze speciale functies worden ook wel magische functies genoemd, omdat ze een effect kunnen hebben op het gedrag van een programma in delen die er op het eerste zicht helemaal niets mee te maken hebben. class Drank: def i n i t ( z e l f, naam, perc, p r i j s ) : z e l f.naam = naam z e l f. alcoholpercentage = perc z e l f. p r i j s = p r i j s gin = Drank ( gin, 0.15, 12) vodka = Drank ( vodka, 0.40, 20) Als we in onze definitie van onze klasse een methode voorzien met de naam init, dan zal Python dus voor ons een functie definiëren met volgende eigenschappen: de naam van de functie is dezelfde als de naam van de klasse; het aantal argumenten van de functie is hetzelfde als het aantal argumenten van de init methode. Wat deze functie zal doen is: 1. Eerst maakt de functie een nieuw object aan van de klasse, en koppelt hieraan alle methodes die bij de klasse horen; 2. Daarna roept de functie de initializatie-methode init van deze klasse op op het object dat ze net heeft aangemaakt, met als argumenten de argumenten die ze zelf gekregen heeft; 3. Tot slot geeft deze functie het nieuw aangemaakte en geïnitializeerde object terug als haar resultaat. Deze functie wordt de constructor van de klasse genoemd. (Opmerking terzijde: Sommige objectgerichte programmeertalen bieden de mogelijkheid aan om per klasse meerdere constructoren te voorzien, die bijvoorbeeld objecten initializeren op basis van verschillende parameters. In Python is dit niet mogelijk, aangezien het gedrag van de constructor volledig bepaald wordt door hetgeen er in de init -methode staat, en er maar één methode met deze naam in 17

elke klasse kan zijn. Wel is het mogelijk om sommige argumenten van deze methode een default waarde mee te geven, waarmee een deel van de functionaliteit waarvoor het hebben van meerdere constructors in andere programmeertalen gebruikt wordt, toch gerealizeerd kan worden.) 1.5 Magische methodes Naast init zijn er in Python nog een hele hoop andere speciale functies en methodes. Een greep uit het gamma. Python biedt een aantal functies aan die objecten van één datatype omzetten naar een ander datatype. Bijvoorbeeld: >>> int(5.4) 5 >>> float(3) 3.0 >>> str(4) 4 >>> int( 7 ) 7 >>> float( 7 ) 7.0 Al deze functies werken door achter de schermen een corresponderende magische methode op te roepen, die dezelfde naam heeft maar dan aangevuld met de nodige underscores. Door in onze eigen klassen deze methodes te implementeren, kunnen we dus bepalen hoe onze eigen objecten zullen worden omgezet naar andere datatypes: class Drank: def i n i t ( z e l f, naam, perc, p r i j s ) : z e l f.naam = naam z e l f. alcoholpercentage = perc z e l f. p r i j s = p r i j s def str ( z e l f ) : return z e l f.naam + ( + str ( z e l f. alcoholpercentage ) + %) def f l o a t ( z e l f ) : return z e l f. alcoholpercentage Laten we dit eens uitproberen: >>> d = Drank( pils, 0.4, 2) 18

>>> str(d) pils (0.4%) >>> float(d) 0.40000000000000002 >>> int(d) Traceback (most recent call last): File "<stdin>", line 1, in? AttributeError: Drank instance has no attribute int In bovenstaande klasse is het waarschijnlijk niet nodig om een methode float te hebben. Er zijn immers weinig situaties te bedenken waarin we op het idee zouden komen om een drank als een kommagetal te gebruiken. De methode str lijkt daarentegen wel zinvol. Telkens als we een drank zouden willen afprinten, moeten we deze immers transformeren naar een string. Sterker nog: als we een drank meegeven aan een print opdracht, dan zal Python achter de schermen deze conversie uitvoeren. Normaalgezien krijgen we iets als dit te zien, als we een Drank-object proberen af te printen: >>> print Drank( pils, 0.4, 2) < main.drank instance at 0x50e9b8> Nu we echter een str methode gedefiniëerd hebben in de klasse Drank, ziet het resultaat er anders uit: >>> print Drank( pils, 0.4, 2) pils (0.4%) Van de verschillende conversie-methodes die hierboven werden aangehaald, is str dan ook veruit de meest gebruikte. Tot slot nog een laatste beetje magie. In de interactieve Python shell, kan je het commando help gebruiken om meer informatie in te winnen over ingebouwde objecten, klassen, functies of methodes. Bijvoorbeeld: >>> help(str)... Return a nice string representation of the object. If the argument is a string, the return value is the same object.... Bij de definitie van een nieuwe klasse, kan je als eerste instructie een string opgeven, en deze zal dan afgebeeld worden als gebruikers om hulp vragen over deze klasse. Deze string wordt per conventie tussen driedubbele aanhalingstekens geplaatst, ook als hij op één lijn past, en wordt de docstring van de klasse genoemd. 19

class WatDoetDit : """ Een klasse die het gebruik van een docstring illustreert. """ pass >>> help(watdoetdit) Help on class WatDoetDit in module main : class WatDoetDit Een klasse die het gebruik van een docstring illustreert. 1.6 Wijzigen van attributen De attributen van een object komen altijd tot stand tijdens zijn initializatie. Het is natuurlijk mogelijk om achteraf de waarde van deze attributen nog te gaan wijzigen. Als we bijvoorbeeld plots 2,5 e korting krijgen op gin, dan kan dit als volgt verwerkt worden: class Drank: def i n i t ( z e l f, naam, perc, p r i j s ) : z e l f.naam = naam z e l f. alcoholpercentage = perc z e l f. p r i j s = p r i j s... # Nog wat andere methodes gin = Drank ( gin, 0.35, 10) gin. p r i j s = gin. p r i j s 2.5 Hierbij wordt de waarde van attribuut prijs van het object gin dus aangepast van buiten deze klasse. Een alternatief is dat we de klasse Drank een extra methode geven, waarmee we deze aanpassing binnen de klasse doen. class Drank: def i n i t ( z e l f, naam, perc, p r i j s ) : z e l f.naam = naam z e l f. alcoholpercentage = perc z e l f. p r i j s = p r i j s... # Nog wat andere methodes 20

def krijgkorting ( z e l f, bedrag ) : z e l f. p r i j s = z e l f. p r i j s bedrag gin = Drank ( gin, 0.35, 10) gin. krijgkorting ( 2. 5 ) Aangezien het de bedoeling van objectgericht programmeren is dat klassen zoveel mogelijk hun eigen gegevens inkapselen, is de tweede optie vaak de beste. 1.7 Voorbeelden Tot slot van dit hoofdstuk, nog een aantal voorbeelden van volledige klassen. 1.7.1 De klasse Drank Het info-kaartje van onze klasse Drank is intussen dit geworden: Klasse Drank Attr. naam : string alcoholpercentage : R prijs : R Meth. init (zelf, naam, perc, prijs) str (zelf) En de bijhorende Python code is dan: class Drank: """ Objecten van deze klasse stellen een drank voor met: """ - een naam, - een alcoholpercentage, - een prijs (in euro per liter). def i n i t ( z e l f, naam, perc, p r i j s ) : z e l f.naam = naam z e l f. alcoholpercentage = perc z e l f. p r i j s = p r i j s def str ( z e l f ) : return z e l f.naam + ( + str ( z e l f. alcoholpercentage ) + %) 21

1.7.2 De klasse Rechthoek Volgende klasse laat ons toe om rechthoeken voor te stellen, af te printen, en hun oppervlakte en omtrek te berekenen. Klasse Rechthoek Attr. hoogte : R breedte : R Meth. oppervlakte(zelf) : R omtrek(zelf) : R class Rechthoek : """ Objecten van deze klasse stellen een meetkundige rechthoek voor met een hoogte en breedte. """ def i n i t ( z e l f, b, h) : z e l f. breedte = b z e l f. hoogte = h def str ( z e l f ) : return "Rechthoek van " + str ( z e l f. breedte ) + "x" + str ( z e l f. hoogte ) def oppervlakte ( z e l f ) : return z e l f. breedte * z e l f. hoogte def omtrek ( z e l f ) : return 2 * ( z e l f. breedte + z e l f. hoogte ) Deze klasse gebruiken we dan bijvoorbeeld zo: >>> r = Rechthoek(2,3) >>> print r Rechthoek van 2x3 >>> r.oppervlakte() 6 >>> r.omtrek() 10 1.7.3 De klasse Cirkel Andere meetkundige vormen kunnen natuurlijk op een gelijkaardige manier worden voorgesteld. Voor een cirkel hebben we het getal π nodig, dat we kunnen aanspreken als math.pi, nadat we eerst deze module math geïmporteerd hebben. 22

Klasse Cirkel Attr. straal : R Meth. oppervlakte(zelf) : R omtrek(zelf) : R class Cirkel : def i n i t ( z e l f, straal ) : z e l f. straal = straal def str ( z e l f ) : return "Cirkel met straal " + str ( z e l f. straal ) def omtrek ( z e l f ) : import math return z e l f. straal * 2 * math. pi def oppervlakte ( z e l f ) : import math return z e l f. straal * (math. pi ** 2) 1.8* Een blik achter de schermen Veel programmeertalen hebben de filosofie dat ze programmeurs tegen zichzelf of tegen hun collega s moeten beschermen. Python heeft deze filosofie niet. Dit is natuurlijk slecht nieuws voor programmeurs die deze bescherming nodig hebben, maar goed nieuws voor de anderen. We hebben eerder gezien dat er een grote gelijkenis bestaat tussen attributen van een object en sleutel-waarde paren in een woordenboek. Dit hoeft geen verwondering te wekken, want achter de schermen worden de attributen van een object gewoon in een woordenboek gestoken. Dit magische woordenboek heeft de naam dict en is zelf een attribuut van het object. >>> d = Drank( pils, 0.04, 2) >>> d. dict { naam : pils, alcoholpercentage : 0.04, prijs : 2} Elk object behoort, zoals je weet, tot een bepaalde klasse. Deze klasse wordt bijgehouden in het attribuut class. >>> d. class <class main.drank at 0x505b40> Op basis van de inleiding van dit hoofdstuk, had je misschien verwacht dat de methodes van de klasse Drank bij in het woordenboek 23

d. dict zouden zitten. Dit is echter niet het geval. De reden hiervoor is gewoon zuinigheid: aangezien alle objecten van de klasse toch dezelfde methodes delen, hoeven ze die niet allemaal afzonderlijk bij te houden. Het is voldoende als gewoon de klasse d. class de methodes bijhoudt. Dit doet ze in haar eigen dict, waar we o.a. ook de docstring van de klasse terugvinden. >>> d. class. dict { module : main, doc : Objecten van deze klasse stellen een drank voor met:\n\n - een naam,\n - een alcoholpercentage, \n - een prijs (in euro per liter). \n, str : <function str at 0x50a230>, init : <function init at 0x50a630>} 24

Samenwerkende objecten 2 In het vorige hoofdstuk hebben we enkel maar naar geïsoleerde klassen en objecten gekeken. In een echt programma zullen verschillende klassen normaalgezien moeten samenwerken. Er zijn drie manieren waarop één klasse een andere kan gebruiken: Een klasse kan een methode hebben, waarin een object van een andere klasse als argument voorkomt; Een klasse kan een methode hebben, die een object van een andere klasse teruggeeft; Een klasse kan een attribuut hebben waarin ze een object van een andere klasse bijhoudt. Er is duidelijk verschil tussen de eerste twee mogelijkheden en de derde, namelijk de duurtijd van de samenwerking. In de eerste twee gevallen is dit een tijdelijke samenwerking, waarbij de ene klasse de andere enkel maar nodig heeft tijdens één enkel methode oproep. Het laatste geval, daarentegen, beschrijft een duurzame binding tussen twee objecten, die mogelijk hun hele levensduur lang meegaat. Dit fenomeen wordt ook wel een associatie tussen de twee klassen genoemd. Associatie = contract van onbepaalde duur 2.1 Een object als argument In Secties 1.7.2 en 1.7.3 introduceerden we een klasse Cirkel en een klasse Rechthoek. Laat ons nu de klasse Cirkel uitbreiden met een methode die kan nagaan of de cirkel in een gegeven Rechthoek past. class Cirkel : #... de klasse zoals voorheen def pastin ( z e l f, rechthoek ) : 25

return ( z e l f. straal <= rechthoek. hoogte and z e l f. straal <= rechthoek. breedte ) Een voorbeeldje van het gebruik van deze methode: >>> cirkel = Cirkel(5) >>> rh = Rechthoek(6,7) >>> cirkel.pastin(rh) True Een object dat als argument wordt meegegeven, gedraagt zich zoals een woordenboek of een lijst, in die zin dat als er in de methode wijzigingen gebeuren aan een attribuut van dit object, deze wijzigingen ook buiten de methode zichtbaar zullen zijn. Laat ons dit illustreren met een methode die een rechthoek inkrimpt totdat hij in een cirkel past. class Cirkel : #... de klasse zoals voorheen def maakingesloten ( z e l f, rechthoek ) : i f rechthoek. hoogte > z e l f. straal : rechthoek. hoogte = z e l f. straal i f rechthoek. breedte > z e l f. straal : rechthoek. breedte = z e l f. straal >>> cirkel = Cirkel(5) >>> rh = Rechthoek(3,7) >>> print rh Rechthoek van 3x7 >>> cirkel.maakingesloten(rh) >>> print rh Rechthoek van 3x5 2.2 Een object als resultaat Als we in bovenstaand voorbeeld nog andere plannen hebben met onze originele rechthoek rh, dan kan het lastig zijn dat we deze nu net in place veranderd hebben. Een alternatief is om in onze methode een nieuwe Rechthoek aan te maken, en deze terug te geven als resultaat. De oorspronkelijke rechthoek kan dan onveranderd blijven. Vergelijk onderstaande code met de versie uit de vorige sectie: class Cirkel : #... de klasse zoals voorheen 26

def maakingesloten ( z e l f, rechthoek ) : i f rechthoek. breedte > z e l f. straal : nieuwebreedte = z e l f. straal else : nieuwebreedte = rechthoek. breedte i f rechthoek. hoogte > z e l f. straal : nieuwehoogte = z e l f. straal else : nieuwehoogte = rechthoek. hoogte return Rechthoek ( nieuwebreedte, nieuwehoogte ) Het gebruik van deze methode moet dan natuurlijk ook anders: >>> cirkel = Cirkel(5) >>> rh = rechthoek(3,7) >>> rh2 = cirkel.maakingesloten(rh) >>> print rh Rechthoek van 3x7 >>> print rh2 Rechthoek van 3x5 Welk van beide stijlen te verkiezen valt, is vaak een kwestie van persoonlijke smaak. In de Python gemeenschap, is men vaak nogal gewonnen voor een functionele stijl van programmeren, waarin functies en methodes zich gedragen zoals wiskundige functies. Dit betekent dat, net zoals een wiskundige functie f, een methode wel een resultaat y = f(x) zal berekenen, maar geen veranderingen zal aanbrengen aan x. Aanhangers van deze programmeerstijl zouden dus waarschijnlijk onze tweede variant van de methode maakingesloten verkiezen. De achterliggende motivatie is dezelfde als altijd: ze geloven dat er op deze manier gemakkelijker onafhankelijkheden tussen verschillende stukken code gerealiseerd kunnen worden, wat uiteindelijk aanleiding zou moeten geven tot programma s die gemakkelijker te ontwikkelen en te onderhouden zijn. 2.3 Associaties tussen klassen Een associatie tussen twee klassen betekent dat elk object van de ene klasse een attribuut heeft waarin een object van de andere klasse wordt bijgehouden. Om dit te illustreren verlaten we even onze rechthoeken en cirkels voor een belangrijkere toepassing, namelijk het bijhouden van onze drankvoorraad. We zagen in het vorige hoofdstuk (Sectie 1.7.1) al een klasse Drank, waarmee we kunnen bijhouden welke dranken we in voorraad hebben. Dit breiden we nu zodanig uit, dat we ook kunnen bijhouden hoeveel er van een bepaalde drank in voorraad is. Hiervoor introduceren we volgende klasse: 27

Klasse Fles Attr. drank : Drank inhoud (in cl) : N Meth. haaluit(zelf, hoeveelheid) voegtoe(zelf, hoeveelheid) waarde(zelf) : R Elk object van de klasse Fles heeft dus een attribuut waarin het een object van de klasse Drank gaat bijhouden. Er is dus, maw., een associatie tussen Fles en Drank. Vaak is het inzichtelijker om in plaats van bovenstaand info-kaartje een zogenaamd klassendiagramma te tekenen. Hierop worden associates aanduid met een pijl tussen de twee klassen in kwestie. Deze pijl vertrekt bij de klasse die het attribuut heeft waarmee deze associatie wordt voorgesteld, en dit attribuut wordt dan niet meer opgenomen in diens lijst met attributen. Eventueel kan de naam van het attribuut wel nog vermeld worden als label bij de pijl. Om het geheel overzichtelijk te houden, wordt er vaak ook voor gekozen om de methodes of zelfs de attributen van een klasse niet te vermelden. Fles inhoud : N drank Drank naam alcoholpercentage prijs : string : R : R class Fles : def i n i t ( z e l f, drank, inhoud ) : z e l f. drank = drank z e l f. inhoud = inhoud def haaluit ( z e l f, hoeveelheid ) : z e l f. inhoud = z e l f. inhoud hoeveelheid def voegtoe ( z e l f, hoeveelheid ) : z e l f. inhoud = z e l f. inhoud + hoeveelheid def waarde ( z e l f ) : prijspercl = z e l f. drank. p r i j s / 100.0 return z e l f. inhoud * prijspercl De interessantste methode is hier de laatste, waarin een Fles object zijn eigen inhoud moet combineren met de prijs van zijn drank om zijn eigen waarde te bepalen. De deling door 100 is nodig omdat de klasse Drank zijn prijs bijhoudt in e/l, terwijl de inhoud van een Fles 28

in cl wordt bijgehouden. Op zich kan dit riskant zijn (denk aan de ontplofte Mars Observator): als we later zouden besluiten om de prijs van een Drank ook in e/cl te zetten, moeten we eraan denken om de deling door 100 weg te halen, of anders krijgen we natuurlijk foute resultaten. Het is in dit geval waarschijnlijk veiliger om voor een andere strategie te kiezen, waarbij het doen van berekeningen met het attribuut drank.prijs zoveel mogelijk in de klasse Drank gebeurt. class Fles :... def waarde ( z e l f ) : return z e l f. drank. prijspercl ( ) * z e l f. inhoud class Drank:... def prijspercl ( z e l f ) : return z e l f. p r i j s / 100 2.4 Lijsten en objecten Ook in onze eigen objecten kunnen we vaak op nuttige wijze gebruik maken van de functionaliteit van Pythons ingebouwde lijsten. In de cursus van het eerste jaar, heb je gezien hoe je met deze lijsten moet omgaan. Een kort voorbeeldje ter herinnering: >>> lijst = [1,2,3] >>> print lijst [1, 2, 3] >>> lijst.append(4) >>> print lijst [1, 2, 3, 4] >>> for element in lijst:... print element... 1 2 3 4 >>> print lijst[1] 2 >>> for i in range(len(lijst)):... print i, "->", lijst[i] 29

... 0 -> 1 1 -> 2 2 -> 3 3 -> 4 Herinner je bij de uitvoer van het laatste commando ook dat de index van een lijst altijd begint te tellen vanaf 0; het element dat bij index 1 hoort, is dus niet het eerste, maar wel het tweede element van de lijst. 2.4.1 Objecten met lijsten Een lijst kan net zoals bijvoorbeeld een getal, een string of een object gebruikt worden als een attribuut van een object. Als een object zo n attribuut heeft, zal het vaak methodes aanbieden waarmee elementen kunnen worden toegevoegd aan en/of weggehaald uit de lijst. Als de constructor van zo n object al geen lijst meekrijgt als argument, dan zal hier typisch een nieuwe, lege lijst worden aangemaakt. Het volgende voorbeeld toont een klasse waarmee een rij van getallen kan worden bijgehouden om hiervan het gemiddelde te berekenen. class GetallenRij : def i n i t ( z e l f ) : z e l f. r i j = [ ] def voegtoe ( z e l f, getal ) : z e l f. r i j. append ( getal ) def str ( z e l f ) : return str ( z e l f. r i j ) def som( z e l f ) : som = 0 for getal in z e l f. r i j : som += getal return som def gemiddelde ( z e l f ) : som = z e l f.som ( ) return f l o a t (som) / len ( z e l f. r i j ) # ^^^^^ anders krijgen we een gehele deling Deze klasse kunnen we dan bijvoorbeeld als volgt gebruiken. >>> rij = GetallenRij() >>> rij.voegtoe(3) >>> rij.voegtoe(6) 30

>>> rij.voegtoe(7) >>> print rij [3, 6, 7] >>> rij.gemiddelde() 5.333333333333333 2.4.2 Lijsten van objecten Naast waardes van een primitief type, zoals getallen of strings, kunnen ook objecten in een lijst gestoken worden. Zo zal onderstaande code een lijst van twee objecten maken, en daar dan nadien nog een geheel getal en een derde object aan toevoegen. gin = Drank (... ) p i l s = Drank (... ) wijn = Drank (... ) f1 = Fles ( gin,50) f2 = Fles ( pils,25) f3 = Fles ( wijn,75) l i j s t = [ f1, f2 ] l i j s t. append ( 4 ) l i j s t. append ( f3 ) De lijst die door deze code geproduceerd wordt, ziet er als volgt uit: >>> print lijst [< main.fles instance at 0x50d580>, < main.fles instance at 0x50d5a8>, 4, < main.fles instance at 0x50d5d0>] Een grafische voorstelling hiervan is te zien in Figuur 2.1. Het belangrijkste punt hiervan, is dat lijst[0] dus eigenlijk gewoon een andere naam is voor hetzelfde object dat ook al de naam f1 heeft. Het effect hiervan zien we bijvoorbeeld in volgende interactie: >>> print f1.inhoud 50 >>> lijst[0].inhoud = 25 >>> print f1.inhoud 25 2.4.3 Objecten met lijsten van objecten Als we objecten in een lijst kunnen steken, is het natuurlijk ook mogelijk om zo n lijst van objecten te gebruiken als een attribuut van een andere object. Op deze manier kunnen we, met andere woorden, een associatie tot stand brengen van een object van één klasse met een verzameling van objecten van een andere klasse. Hiervan kunnen we 31

0 1 2 3 lijst 4 Fles drank gin inhoud 50 Fles drank pils inhoud 25 Fles drank wijn inhoud 75 f1 f2 f3 Figuur 2.1: Een grafische voorstelling van het resultaat van de code lijst = [f1,f2,4,f3]. bijvoorbeeld gebruik maken om gegevens over een DrankVoorraad bij te houden, door middel van een lijst van flessen waaruit deze voorraad bestaat. Klasse DrankVoorraad Attr. flessen : lijst<fles> Meth. waarde(zelf) : R Met de notatie lijst<fles> bedoelen we een lijst met daarin een aantal Fles objecten. In een klassendiagramma, kunnen we zo n lijst aanduiden met een sterretje, zoals te zien in Figuur 2.2. Veel van het gedrag dat een object van een klasse zoals DrankVoorraad zal aanbieden, wordt typisch gerealizeerd door middel van een iteratie over de Fles-objecten die in zijn lijst zitten. Bijvoorbeeld de methode om de volledige waarde van een drankkast te berekenen, gegeven de waarde van de individuele flessen, valt op deze manier te implementeren. class DrankVoorraad : def i n i t ( z e l f ) : z e l f. flessen = [ ] def voegtoe ( z e l f, f l e s ) : z e l f. flessen. append ( f l e s ) def waarde ( z e l f ) : 32

DrankVoorraad waarde(zelf) : R flessen Fles inhoud : N waarde(zelf) : R voegtoe(zelf, hoeveelheid) haaluit(zelf, hoeveelheid) drank Drank naam alcoholpercentage prijs : string : R : R Figuur 2.2: Een klassendiagramma waarbij een drankvoorraad uit een verzameling flessen bestaat. resultaat = 0 for f in z e l f. flessen : resultaat += f. waarde ( ) return resultaat Laten we even stilstaan bij het resultaat dat deze methode berekent. Als de drankvoorraad n flessen {f 1,..., f n } bevat, waarbij V i de inhoud is van fles i en p i de prijs van de drank d i die in fles f i zit, dan berekent deze methode volgende som w: w = V i p i. 1 i n De methode waarde uit de klasse DrankVoorraad berekent heel deze som, gebruikmakend van de gelijknamige methode uit de klasse Fles, die één term ervan berekent. Sommige getalletjes leggen dus een hele weg af, voordat ze uiteindelijk in deze som belanden: Drank Fles DrankVoorraad p 1 d 1 f 1 V 1 p 1 i V i p i d n p n f n V n p n 33