FACULTEIT INDUSTRIELE INGENIEURSWETENSCHAPPEN CAMPUS DE NAYER
!
N ETWERKEN en O BJECTORIËNTATIE Deel 2: Objectoriëntatie in Python Joost Vennekens
!
Inhoudsopgave 1 Objecten en klassen 1.1 Een beetje geschiedenis . . . 1.2 Objecten . . . . . . . . . . . . . 1.3 Klassen . . . . . . . . . . . . . . 1.4 Constructoren . . . . . . . . . 1.5 Magische methodes . . . . . . 1.6 Wijzigen van attributen . . . . 1.7 Voorbeelden . . . . . . . . . . . 1.7.1 De klasse ’Drank’ . . . 1.7.2 De klasse ’Rechthoek’ 1.7.3 De klasse ’Cirkel’ . . . 1.8 Een blik achter de schermen
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
5 5 7 11 15 18 20 21 21 22 22 23
2 Samenwerkende objecten 2.1 Een object als argument . . . . . . . . . . 2.2 Een object als resultaat . . . . . . . . . . 2.3 Associaties tussen klassen . . . . . . . . 2.4 Lijsten en objecten . . . . . . . . . . . . . . 2.4.1 Objecten met lijsten . . . . . . . . . 2.4.2 Lijsten van objecten . . . . . . . . . 2.4.3 Objecten met lijsten van objecten 2.5 Voorbeelden . . . . . . . . . . . . . . . . . . 2.5.1 Punten en veelhoeken . . . . . . . 2.5.2 Cocktails en dranken . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
25 25 26 27 29 30 31 31 34 34 36
3 Overerving 3.1 Het overschrijven van methodes . . . . . 3.2 Overervingshiërarchieën . . . . . . . . . . 3.3 Methodes uit een superklasse oproepen 3.4 Voorbeelden . . . . . . . . . . . . . . . . . . 3.4.1 Vierkanten . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
39 41 44 46 48 48
3
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
4 Varia 4.1 Python als server-side scripting taal 4.2 Methodes met default argumenten . . 4.3 Vergelijken van objecten . . . . . . . . 4.4 Nog meer vergelijkingen . . . . . . . . 4.5 Uitzonderingen . . . . . . . . . . . . . . 4.5.1 Uitzonderingen opwerpen . . . 4.5.2 Uitzonderingen afhandelen . . 4.5.3 Herwerking van het voorbeeld 4.6 Statische methodes . . . . . . . . . . . 4.7 Nog een blik achter de schermen . . . 4.7.1 De volledige klasse ‘Cirkel’ . . 4.8 Private variabelen . . . . . . . . . . . . 4.9 Eigenschappen (Properties) . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
49 49 50 51 55 56 58 60 62 63 67 68 68 74
A Practicum opgaves A.1 Setup . . . . . . . . . . . . . A.2 Vogelpik . . . . . . . . . . . A.3 De eerste objectjes . . . . . A.4 Punten, ruimtes en ballen A.5 Tijd voor actie . . . . . . . A.6 Muren . . . . . . . . . . . . A.7 Krachten . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
77 77 78 80 81 83 85 87
. . . . . . .
4
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
Objecten en klassen 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 2002 mogelijke interacties tussen lijnen code, die allemaal relevant kunnen zijn voor de programmeur, ten opzichte van 1002 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
1
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 × 1002 mogelijke interacties over, wat een pak minder is dan 2002 . 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 × 202 mogelijke interacties, wat een grootte-orde minder is dan de oorspronkelijke 2002 : 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
# aandeel vodka per eenheid ⤦
def vodkaVoorCocktail ( c o c k t a i l ) : return verhouding * c o c k t a i l def fruitsapVoorCocktail ( c o c k t a i l ) : return (1 − verhouding ) * c o c k t a i l def vodkaVoorFruitsap ( f r u i t s a p ) : return f r u i t s a p * ( 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 ( f r u i t s a p ) : return f r u i t s a p / verhouding def fruitsapVoorVodka2 ( vodka ) : return verhouding * vodka def vodkaVoorCocktail2 ( c o c k t a i l ) : return c o c k t a i l / ( verhouding + 1) def fruitsapVoorCocktail2 ( c o c k t a i l ) : vodka = vodkaVoorCocktail2 ( c o c k t a i l ) 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 ( c o c k t a i l , hoeveelheid , ingredient ) : return hoeveelheid * c o c k t a i l [ ingredient ] def ingredientPerIngredient ( c o c k t a i l , hoeveelheid , gegeven , gezocht ) : return hoeveelheid * ( c o c k t a i l [ gezocht ] / c o c k t a i l [ 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 ) f u n c t i e = dubbel print f u n c t i e ( 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 ( c o c k t a i l , hoeveelheid , ingredient ) : return hoeveelheid * c o c k t a i l [ ingredient ] def ingredientPerIngredient ( c o c k t a i l , hoeveelheid , gegeven , gezocht ) : verhouding = c o c k t a i l [ gezocht ] / c o c k t a i l [ 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 ( c o c k t a i l , hoeveelheid , ingredient ) : som = 0 for drank in c o c k t a i l : som += c o c k t a i l [ drank ] return hoeveelheid * c o c k t a i l [ 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. c o c k t a i l s = [ vodkaOrange , bloodyMary ] vodkaNodig = 0 for c in c o c k t a i l s : 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.
1.3 Klassen In de vorige sectie hebben we de essentie van objectgericht programmeren uit de doeken gedaan, zonder daarvoor iets meer over 11
object = gegevens + gedrag
Een methode is een functie in een object
Python te zien dan er in de cursus van het 1e 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 ( c o c k t a i l , hoeveelheid , ingredient ) : pass # <- Hier komt nog een berekening def ingredientPerIngredient ( c o c k t a i l , 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: o b j e c t . s l e u t e l = 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
> Laten we nu deze methode ook eens implementeren. In de vorige sectie hadden we deze functie: def ingredientPerCocktail ( c o c k t a i l , hoeveelheid , ingredient ) : return hoeveelheid * c o c k t a i l [ ingredient ] 13
De gegevens in een object heten attributen
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 ( c o c k t a i l , hoeveelheid , ingredient ) : return hoeveelheid * c o c k t a i l . 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 * g e t a t t r ( z e l f , ingredient ) def ingredientPerIngredient ( z e l f , hoeveelheid , gegeven , gezocht ) : verhouding = g e t a t t r ( z e l f , ⤦ Ç gezocht ) / g e t a t t r ( z e l f , gegeven ) return hoeveelheid * verhouding
Het eerste argument van een methode is zelf
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
f wordt impliciet gegeven
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 Attr.
Drank naam alcoholpercentage prijs
: string :R :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 zelf . prijs = prijs 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 zelf . prijs = prijs 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: >>> 5 >>> 3.0 >>> ’4’ >>> 7 >>> 7.0
int(5.4) float(3) str(4) int(’7’) float(’7’)
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 zelf . prijs = prijs def _ _ s t r _ _ ( z e l f ) : return z e l f .naam + ’ (’ + ⤦ Ç s t r ( 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 zelf . prijs = prijs . . . # 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 zelf . prijs = prijs . . . # Nog wat andere methodes 20
def k r i j g K o r t i n g ( 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 . k r i j g K o r t i n g ( 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 Attr.
Meth.
Drank naam alcoholpercentage prijs __init__(zelf, naam, __str__(zelf)
: string :R :R perc, prijs)
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 zelf . prijs = prijs def _ _ s t r _ _ ( z e l f ) : return z e l f .naam + ’ (’ + ⤦ Ç s t r ( 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 Attr. Meth.
Rechthoek hoogte breedte oppervlakte(zelf) omtrek(zelf)
: : : :
R R R 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 _ _ s t r _ _ ( z e l f ) : return "Rechthoek van " + s t r ( z e l f . breedte ) + ⤦ Ç "x" + s t r ( 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 Attr. Meth.
Cirkel straal oppervlakte(zelf) omtrek(zelf)
:R :R :R
class Cirkel : def _ _ i n i t _ _ ( z e l f , s t r a a l ) : z e l f . straal = straal def _ _ s t r _ _ ( z e l f ) : return "Cirkel met straal " + s t r ( z e l f . s t r a a l ) def omtrek ( z e l f ) : import math return z e l f . s t r a a l * 2 * math . pi def oppervlakte ( z e l f ) : import math return z e l f . s t r a a l * ( 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__ 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__’: , ’__init__’: }
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 Associatie = contract mogelijk hun hele levensduur lang meegaat. Dit fenomeen wordt ook van onbepaalde duur wel een associatie tussen de twee klassen genoemd.
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 . s t r a a l <= rechthoek . hoogte and z e l f . s t r a a l <= 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 . s t r a a l : rechthoek . hoogte = z e l f . s t r a a l i f rechthoek . breedte > z e l f . s t r a a l : rechthoek . breedte = z e l f . s t r a a l >>> 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 . s t r a a l : nieuweBreedte = z e l f . s t r a a l else : nieuweBreedte = rechthoek . breedte i f rechthoek . hoogte > z e l f . s t r a a l : nieuweHoogte = z e l f . s t r a a l 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 Attr. Meth.
Fles drank : Drank inhoud (in cl) : N 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. Drank Fles inhoud
drank naam
:N
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 ) : p r i j s P e r C l = z e l f . drank . p r i j s / 100.0 return z e l f . inhoud * p r i j s P e r C l 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 . p r i j s P e r C l ( ) * z e l f . inhoud class Drank : ... def p r i j s P e r C l ( 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: >>> >>> [1, >>> >>> [1, >>> ... ... 1 2 3 4 >>> 2 >>> ...
lijst = [1,2,3] print lijst 2, 3] lijst.append(4) print lijst 2, 3, 4] for element in lijst: print element
print lijst[1] for i in range(len(lijst)): print i, "->", lijst[i] 29
... 0 -> 1 -> 2 -> 3 ->
1 2 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 G e t a l l e n R i j : def _ _ i n i t _ _ ( z e l f ) : zelf . rij = [] def voegToe ( z e l f , g e t a l ) : z e l f . r i j . append ( g e t a l ) def _ _ s t r _ _ ( z e l f ) : return s t r ( z e l f . r i j ) def som( z e l f ) : som = 0 for g e t a l in z e l f . r i j : som += g e t a l 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 , 5 0 ) f2 = Fles ( p i l s , 2 5 ) f3 = Fles ( wijn , 7 5 ) 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
4
lijst
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 Attr. Meth.
DrankVoorraad flessen : lijst waarde(zelf) : R
Met de notatie lijst 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 . f l e s s e n . 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 . f l e s s e n : r e s u l t a a t += f . waarde ( ) return r e s u l t a a t Laten we even stilstaan bij het resultaat dat deze methode berekent. Als de drankvoorraad n flessen {f1 , . . . , fn } bevat, waarbij Vi de inhoud is van fles i en pi de prijs van de drank di die in fles fi zit, dan berekent deze methode volgende som w: w = ∑ Vi ⋅ pi . 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 d1
Fles p1
⋮ dn
DrankVoorraad
/ f1
V1 ⋅p1
⋮ pn
Vn ⋅pn
/ fn
33
*
4 ∑i Vi ⋅ pi
GetallenRij rij som(zelf) gemiddelde(zelf)
Veelhoek omtrek(zelf) : R voegToe(zelf,punt)
: lijst :Z :R
Punt hoekpunten ∗ x y afstandNaar(zelf,punt)
:R :R :R
Figuur 2.3: Klassendiagramma van het voorbeeld in Sectie 2.5.
2.5 Voorbeelden 2.5.1 Punten en veelhoeken In dit voorbeeld gebruiken we drie klassen: • De klasse Punt stelt een punt in het Euclidisch vlak voor. • De klasse Veelhoek stelt veelhoek voor als een lijst van hoekpunten. • Bij het berekenen van de omtrek van een Veelhoek, maken we gebruik van de klasse GetallenRij, die we eerder in dit hoofdstuk (Sectie 2.4.1) gebruikt hebben. Het bijhorende klassendiagramma is te zien in Figuur 2.3. Er is dus een associatie tussen een Veelhoek en een lijst van Punten. De klasse GetallenRij wordt enkel maar binnenin één bepaalde methode gebruikt, dus hiermee is er geen associatie. Voor de volledigheid herhalen we eerst de definitie van de klasse GetallenRij nog eens. class G e t a l l e n R i j : def _ _ i n i t _ _ ( z e l f ) : zelf . rij = [] def voegToe ( z e l f , g e t a l ) : z e l f . r i j . append ( g e t a l ) def _ _ s t r _ _ ( z e l f ) : 34
return s t r ( z e l f . r i j ) def som( z e l f ) : som = 0 for g e t a l in z e l f . r i j : som += g e t a l 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 De klasse Punt bevat niet veel meer dan een Euclidische afstandsberekening. class Punt : def _ _ i n i t _ _ ( z e l f , x , y ) : zelf .x = x zelf .y = y def afstandNaar ( z e l f , ander ) : import math # om vierkantswortels te nemen return math . sqrt ( ( z e l f . x − ander . x ) * * 2 + ( z e l f . y − ander . y ) * * 2) def _ _ s t r _ _ ( z e l f ) : return "Punt(" + s t r ( z e l f . x ) + ", " + ⤦ Ç s t r ( z e l f . y ) + ")" De klasse Veelhoek is nu als volgt. class Veelhoek : def _ _ i n i t _ _ ( z e l f , punten ) : z e l f . hoekpunten = punten def voegToe ( z e l f , punt ) : z e l f . hoekpunten . append ( punt ) def l i j s t V a n Z i j d e s ( z e l f ) : r i j = GetallenRij ( ) for i in range ( len ( z e l f . hoekpunten ) ) : van = z e l f . hoekpunten [ i ] i f i +1 < len ( z e l f . hoekpunten ) : naar = z e l f . hoekpunten [ i +1] else : 35
b
1
c
√
2
1
a
2
d
Figuur 2.4: Een veelhoek in het Euclidisch vlak.
naar = z e l f . hoekpunten [ 0 ] r i j . voegToe ( van . afstandNaar ( naar ) ) return r i j def omtrek ( z e l f ) : return z e l f . l i j s t V a n Z i j d e s ( ) .som ( )
Hiermee kunnen we nu als volgt de omtrekt berekenen van de veelhoek in Figuur 2.4. a = Punt ( 2 , 2 ) b = Punt ( 2 , 3 ) c = Punt ( 3 , 3 ) d = Punt ( 4 , 2 ) veelhoek = Veelhoek ( [ a , b , c , d ] ) print veelhoek . omtrek ( )
2.5.2 Cocktails en dranken We herschrijven nu de klasse Cocktail om gebruik te maken van onze klasse Drank, op de manier getoond in onderstaand klassendiagramma. 36
Cocktail naam : String ingrediënten * Ingredient percentage
drank :R
Drank naam alcoholpercentage prijs
: string :R :R
De klasse Drank is nog steeds dezelfde als in Sectie 1.7.1. De klasse Ingredient en Cocktail zijn nu als volgt: class Ingredient : """ Een klasse om de ingredienten van een Cocktail ⤦ Ç voor te stellen Elk ingredient bestaat uit: - een Drank - het percentage van de cocktail dat uit deze drank bestaat """ def _ _ i n i t _ _ ( z e l f , drank , percentage ) : z e l f . drank = drank z e l f . percentage = percentage class Cocktail : """ Een cocktail met een: - naam - een lijst van Ingredient objecten, die de ⤦ Ç samenstelling aangeven """ def _ _ i n i t _ _ ( z e l f , naam, ingredienten ) : z e l f .naam = naam z e l f . ingredienten = ingredienten def ingredientPerCocktail ( z e l f , drank , hoeveelheid ) : for ingr in z e l f . ingredienten : i f ingr . drank == drank : return hoeveelheid * ingr . percentage
37
def ingredientPerIngredient ( z e l f , hoeveelheid , gegeven , gezocht ) : for ingr in z e l f . ingredienten : i f ingr . drank == gegeven : gegevenIngr = ingr i f ingr . drank == gezocht : gezochtIngr = ingr verhouding = gezochtIngr . percentage / ⤦ Ç gegevenIngr . percentage return hoeveelheid * verhouding def alcoholpercentage ( z e l f ) : r i j = GetallenRij ( ) for ingr in z e l f . ingredienten : r i j . voegToe ( ingr . percentage * ingr . drank . alcoholpercentage ) return r i j .som ( ) def isStrafferDan ( z e l f , andere ) : return z e l f . alcoholpercentage ( ) > ⤦ Ç andere . alcoholpercentage ( ) def _ _ s t r _ _ ( z e l f ) : return z e l f .naam Gebruik maken van deze klassen kan dan bijvoorbeeld als volgt: gin = Drank ( "gin" , 45, 12) tonic = Drank ( "tonic" , 0 , 7) ginTonic = Cocktail ( "Gin-tonic" , [ Ingredient ( gin , ⤦ Ç 0.34) , Ingredient ( tonic , 0 . 6 6 ) ] ) print "Voor 10cl gin heb je " + ⤦ Ç s t r ( ginTonic . ingredientPerIngredient (10 , gin , ⤦ Ç tonic ) ) + "cl tonic nodig" tomatensap = Drank ( "tomatensap" , 0 , 2) vodka = Drank ( "vodka" , 0.4 , 10) bloodyMary = Cocktail ( "Bloody Mary" , ⤦ Ç [ Ingredient ( tomatensap , 0.75) , ⤦ Ç Ingredient ( vodka , 0 . 2 5 ) ] ) i f bloodyMary . isStrafferDan ( ginTonic ) : s t r a f s t e = bloodyMary else : s t r a f s t e = ginTonic print "Doe mij maar een " + s t r ( s t r a f s t e )
38
3
Overerving Een belangrijk streefdoel van objectgericht programmeren is herbruikbaarheid van programma-code. In het vorig hoofdstuk hebben we bijvoorbeeld een klasse Veelhoek gedefinieerd. Moesten we nu tien jaar later een programma aan het schrijven zijn waarin we opeens nood blijken te hebben aan een klasse waarmee we een veelhoek kunnen voorstellen, dan zouden we graag hebben dat we gewoon deze klasse uit het vorige hoofdstuk terug kunnen opzoeken en deze zonder verdere moeite herbruiken in ons nieuwe programma. In de praktijk blijkt dit echter vaak toch niet zo eenvoudig te zijn. Een veel voorkomend fenomeen is dat er in het nieuwe programma net iets meer functionaliteit vereist is dan in het oude. Zo zou het bijvoorbeeld kunnen zijn dat we in het nieuwe programma opeens ook de oppervlakte van een veelhoek moeten kunnen berekenen, terwijl dit in het oude nog niet zo was. We zouden dan natuurlijk de oude klasse kunnen gaan aanpassen, maar dan lopen we het risico dat we uiteindelijk een moeilijk te beheren warboel van allemaal verschillende versies van “dezelfde” klasse zullen overhouden. Om dit te voorkomen is er nood een mechanisme waarmee we uitbreidingen van een bestaande klasse kunnen maken, zonder deze klasse zelf echter te moeten veranderen. Dit gebeurt dmv. het mechanisme van overerving. We introduceren eerst wat terminologie. De bestaande klasse waarvan ze vertrekken wordt de superklasse genoemd. Uit deze superklasse gaan we dan een nieuwe klasse afleiden, die de subklasse genoemd wordt. Objecten van deze subklasse zullen alles kunnen wat objecten van de superklasse ook kunnen, maar daarnaast ook nog iets méér. Op deze manier zouden we dus zelfs kunnen zeggen dat elk object dat tot de subklasse behoort eigenlijk óók tot de superklasse behoort. Met andere woorden, we kunnen de subklasse zien als een deelverzameling van de superklasse. Hierin ligt ook meteen de oorsprong van deze twee termen. Dit wordt nog eens grafisch geïllustreerd in Figuur 3.1. De notatie voor overerving in Python is heel eenvoudig: het volstaat om bij de declaratie van de subklasse de naam van de superklasse tussen 39
Overerving dient om bestaande klassen uit te breiden
Subklasse = superklasse + extra’s
superklasse
subklasse
Figuur 3.1: Een superklasse (zoals Cocktail) in rode streepjeslijn (– – –) met een subklasse (zoals CocktailMetGarnituur) in blauwe puntjeslijn (⋯). haakjes te zetten. Onderstaand code-fragmentje definieert bijvoorbeeld een subklasse CocktailMetGarnituur van de klasse Cocktail uit Sectie 2.5.2. class CocktailMetGarnituur ( Cocktail ) : pass Het effect hiervan is dat we nu objectjes van de klasse CocktailMetGarnituur kunnen aanmaken, die zich volledig zullen gedragen zoals onze Cocktail objectjes. We kunnen dus, bijvoorbeeld, het volgende doen: tomatensap = Drank ( "tomatensap" , 0 , 2) vodka = Drank ( "vodka" , 0.4 , 10) bloodyMary = CocktailMetGarnituur ( "Bloody Mary" , ⤦ Ç [ Ingredient ( tomatensap , 0.75) , ⤦ Ç Ingredient ( vodka , 0 . 2 5 ) ] ) print bloodyMary . alcoholpercentage ( ) Het object bloodyMary dat we hier hebben aangemaakt is een object van de klasse CocktailMetGarnituur. In termen van Figuur 3.1, is het dus één van de objectjes in de blauwe verzameling. Dit komt enkel en alleen door het feit dat we de functie met deze naam gebruikt hebben bij het aanmaken van dit object. Met andere woorden, de klasse waartoe een object behoort, ligt al vast van bij het aanmaken van dit object. Tijdens de levensloop van het object, zal dit ook niet meer veranderen. Zoals we in bovenstaand fragmentje kunnen zien, erft elk objectje van een subklasse dus al de methodes van zijn superklasse. Dit verklaart waarom we op ons object bloodyMary toch de methode 40
alcoholpercentage kunnen oproepen, ook al komt deze niet voor in onze definitie van de klasse CocktailMetGarnituur. Overerving wordt natuurlijk pas interessant van zodra we aan de subklasse wat bijkomende functionaliteit gaan toevoegen, die in de superklasse nog niet aanwezig was. class CocktailMetGarnituur ( Cocktail ) : def voegGarnituurToe ( z e l f , gar ) : z e l f . garnituur = gar Nu hebben objecten van de klasse CocktailMetGarnituur dus zowel alle methodes uit de oorspronkelijke klasse Cocktail, als deze nieuwe methode voegGarnituurToe. bloodyMary = CocktailMetGarnituur ( "Bloody Mary" , ⤦ Ç [ Ingredient ( tomatensap , 0.75) , ⤦ Ç Ingredient ( vodka , 0 . 2 5 ) ] ) bloodyMary . voegGarnituurToe ( "selder" )
3.1 Het overschrijven van methodes Dankzij deze extra methode in de klasse CocktailMetGarnituur kunnen we nu al een takje selder toevoegen aan onze bloody mary. Er gebeurt natuurlijk nog niets met deze bijkomende informatie. We zouden bijvoorbeeld onze garnituur ook kunnen willen opnemen in de string-voorstelling van de cocktail. Hiervoor moeten we dan, zoals steeds, een __str__ methode schrijven. Merk eerst op dat onze CocktailMetGarnituur objecten natuurlijk al zo’n methode hebben, namelijk, diegene die ze overerven van hun superklasse Cocktail. Om nog even het globale plaatje samen te vatten: class Cocktail : ... def _ _ s t r _ _ ( z e l f ) : return z e l f .naam class CocktailMetGarnituur ( Cocktail ) : def voegGarnituurToe ( z e l f , gar ) : z e l f . garnituur = gar En dit laat ons toe om het volgende te doen: bloodyMary = CocktailMetGarnituur ( "Bloody Mary" , ⤦ Ç [ Ingredient ( tomatensap , 0.75) , ⤦ Ç Ingredient ( vodka , 0 . 2 5 ) ] ) 41
bloodyMary . voegGarnituurToe ( "selder" ) En als we dan de string-voorstelling van dit object opvragen, wordt—dankzij de overerving—het resultaat berekend door de methode __str__ uit de klasse Cocktail. >>> print bloodyMary Bloody Mary Nu willen we ervoor zorgen dat objecten van de klasse CocktailMetGarnituur een andere string-voorstelling krijgen dan objecten van de klasse Cocktail. Dit doen we door deze klasse zijn eigen __str__ methode te geven. class Cocktail : ... def _ _ s t r _ _ ( z e l f ) : return z e l f .naam class CocktailMetGarnituur ( Cocktail ) : def voegGarnituurToe ( z e l f , gar ) : z e l f . garnituur = gar def _ _ s t r _ _ ( z e l f ) : return z e l f .naam + " met " + z e l f . garnituur
De meest specifieke methode wordt opgeroepen
Als we nu proberen om op onze Bloody Mary de methode __str__ op te roepen (door ofwel bloodyMary.__str__(), ofwel str(bloodyMary), ofwel print bloodyMary te doen), dan zijn er dus eigenlijk twee verschillende methodes die in aanmerking komen. Er is de methode met deze naam die wordt overgeërfd uit de klasse Cocktail en er is de methode met deze naam die in de klasse CocktailMetGarnituur zelf gedefiniëerd wordt. Wat Python in zo’n geval zal doen—en het is trouwens eenvoudig in te zien dat dit ook de enige zinvolle optie is—is de meest specifieke methode kiezen die van toepassing is, in dit geval dus de methode uit CocktailMetGarnituur. >>> print bloodyMary Bloody Mary met selder In dit geval zeggen we dat de methode uit de subklasse de gelijknamige methode uit de superklasse overschrijft; in het Engels spreken we van overriding. Ook constructoren kunnen op deze manier natuurlijk overschreven worden. In bovenstaand voorbeeld hadden we het object bloodyMary als volgt aangemaakt: 42
bloodyMary = CocktailMetGarnituur ( "Bloody Mary" , ⤦ Ç [ Ingredient ( tomatensap , 0.75) , ⤦ Ç Ingredient ( vodka , 0 . 2 5 ) ] ) Wat deze code doet, is natuurlijk de overgeërfde methode __init__ uit de superklasse Cocktail oproepen: class Cocktail : ... def _ _ i n i t _ _ ( z e l f , naam, ingredienten ) : z e l f .naam = naam z e l f . ingredienten = ingredienten Moesten we nu graag de klasse CocktailMetGarnituur zijn eigen, specifieke constructor geven, zodat de garnituur al meteen als argument hiermee kan worden meegegeven, dan kan dit zo: class CocktailMetGarnituur ( Cocktail ) : ... def _ _ i n i t _ _ ( z e l f , naam, ingredienten , garnituur ) : z e l f .naam = naam z e l f . ingredienten = ingredienten z e l f . garnituur = garnituur Ook kunnen natuurlijk niet-magische methodes overschreven worden. Veronderstel bijvoorbeeld dat de aanwezigheid van de garnituur ervoor zorgt dat het alcoholpercentage van de cocktail een procentje lager ligt dan normaal. class Cocktail : ... def alcoholpercentage ( z e l f ) : r i j = GetallenRij ( ) for ingr in z e l f . ingredienten : r i j . voegToe ( ingr . percentage * ⤦ Ç ingr . drank . alcoholpercentage ) return r i j .som ( ) def isStrafferDan ( z e l f , andere ) : return z e l f . alcoholpercentage ( ) > ⤦ Ç andere . alcoholpercentage ( ) class CocktailMetGarnituur ( Cocktail ) : 43
... def alcoholpercentage ( z e l f ) : r i j = GetallenRij ( ) for ingr in z e l f . ingredienten : r i j . voegToe ( ingr . percentage * ⤦ Ç ingr . drank . alcoholpercentage ) return r i j .som ( ) − 0.01 Om het globale effect hiervan te zien, is het de moeite waard om even stil te staan bij wat er juist gebeurt in volgend code-fragmentje: bloodyMary = CocktailMetGarnituur(...) whiskyCola = Cocktail(...) bloodyMary.isStrafferDan(whiskyCola) De methode isStrafferDan is niet overschreven geweest in de subklasse CocktailMetGarnituur, dus hier wordt gewoon de versie uit de klasse Cocktail opgeroepen. In deze methode gebeuren nu twee andere methode-oproepen: • zelf.alcoholpercentage() wordt opgeroepen, waarbij zelf verwijst naar het object bloodyMary. Aangezien dit een object is van de klasse CocktailMetGarnituur en deze klasse de methode alcoholpercentage overschrijft, wordt hiermee dus de methode opgeroepen die 0.01 aftrekt van het gemiddelde alcoholpercentage van de ingrediënten. • ander.alcoholpercentage() wordt opgeroepen, waarbij ander verwijst naar het object whiskyCola. Aangezien dit een object is van de klasse Cocktail, speelt de subklasse CocktailMetGarnituur hierbij geen enkele rol, en wordt dus de methode opgeroepen die gewoon het gemiddelde alcoholpercentage van de ingrediënten teruggeeft. Samengevat zien we dus dat een subklasse alle functionaliteit overerft van zijn superklasse. Door bijkomende methodes toe te voegen, kan deze subklasse de functionaliteit van de superklasse dan gaan uitbreiden, en door reeds bestaande methodes te overschrijven, kan de subklasse de implementatie van sommige functionaliteiten naar wens gaan aanpassen.
3.2 Overervingshiërarchieën Het is natuurlijk ook mogelijk om subklassen te maken van een klasse die zelf al een subklasse is van een andere klasse. Op deze manier 44
x
y
x $ &
z
Figuur 3.2: Classificatie/overervingshiërarchie van veelhoeken.
kunnen hele klassenhiërarchieën tot stand komen. Als voorbeeld kunnen we de classificatie van veelhoeken nemen, die in Figuur 3.2 getoond wordt. Onderstaande code toont hoe we deze hiërarchie in Python klassen kunnen gieten. Omdat we hier enkel geïnteresseerd zijn in de structuur van deze hiërarchie, hebben we al deze klassen voorlopig leeg gelaten. In een echt programma, zouden hier natuurlijk verschillende methodes in aanwezig zijn. class Veelhoek : pass class Driehoek ( Veelhoek ) : pass class Vierhoek ( Veelhoek ) : pass class RechthoekigeDriehoek ( Driehoek ) : pass class G e l i j k z i j d i g e D r i e h o e k ( Driehoek ) : pass class Trapezium ( Vierhoek ) : pass class Parallellogram ( Trapezium ) : 45
pass class Ruit ( Parallellogram ) : pass class Rechthoek ( Parallellogram ) : pass class Vierkant ( Rechthoek , Ruit ) : pass
De klasse object is de voorouder van alle klassen
De klasse Vierkant toont iets wat we nog niet eerder gezien hebben, namelijk een klasse die meer dan één superklasse heeft. Dit is een fenomeen dat meervoudige overerving genoemd wordt. Hoewel Python meervoudige overerving wel degelijk toelaat, kan het gebruik hiervan ervoor zorgen dat programma’s moeilijker te begrijpen vallen, doordat het minder evident wordt om te zien welke methodes juist vanuit welke klasse worden overgeërfd. Om deze reden zijn er verschillende experts die het gebruik van meervoudige overerving afraden. Deze cursus volgt dit standpunt, en we zullen verder dan ook geen meervoudige overerving meer gebruiken. In een overervingshiërarchie kunnen we een onderscheid maken tussen een klasse die een directe subklasse is van een andere (zoals bv. de klasse Vierkant is van Rechthoek) en een klasse die een indirecte subklasse is (zoals bv. Vierkant van Vierhoek). Alle klassen in bovenstaand voorbeeld zijn dus een directe of indirecte subklasse van Veelhoek. We noemen deze klasse daarom ook wel de wortel van deze overervingshiërarchie. In het algemeen zullen we de klassen in een programma dus kunnen ordenen in een aantal overervingshiërarchieën, waarvan er sommigen mogelijk veel klassen en niveaus bevatten, en anderen misschien maar één. In het extreme geval, zouden we zelfs één enkele wortel-klasse in ons programma kunnen hebben, waarvan alle andere klassen (direct of indirect) overerven. Python bevat reeds een klasse object (met kleine eerste letter), die hiervoor specifiek bedoeld is. Dit is een relatief nieuwe toevoeging aan de taal, en om geen oude programma’s stuk te maken, is het gebruik ervan (nog) niet verplicht. Het is echter wel aan te raden om dit toch te doen, maw. om te zorgen dat elke klasse die je definiëert (direct of indirect) overerft van object. In Python terminologie noemt men dit een klasse in de nieuwe stijl (new-style class). We zullen in deze cursus vanaf nu alleen nog maar klassen in de nieuwe stijl maken.
3.3 Methodes uit een superklasse oproepen Een vaak voorkomend fenomeen als we methodes gaan overschrijven, is dat we eigenlijk maar een heel kleine aanpassing willen doen aan het 46
gedrag van de methode uit de superklasse. Dit zagen we bijvoorbeeld bij de methode alcoholpercentage, waarbij we in de subklasse eerst heel de berekening van het gemiddelde alcoholpercentage uit de superklasse herhaald hebben, om er dan daarna nog even een procent van af te trekken. We hadden dit toen gewoon gedaan door een paar regels code uit de superklasse te kopiëren, maar—zoals we intussen weten—is het dupliceren van code iets wat een goede programmeur altijd zoveel mogelijk tracht te vermijden. Python biedt daarom de mogelijkheid aan om vanuit een methode in een subklasse de overschreven gelijknamige methode uit de superklasse toch nog op te roepen. Hiervoor dient de functie super. Deze functie neemt twee argumenten: super(Klasse, zelf). Het resultaat van deze functie is eigenlijk gewoon opnieuw het object zelf, maar dan met de eigenschap dat als je er methodes op oproept, niet de meest specifieke methodes uit Klasse zullen genomen worden, zoals normaal zou gebeuren, maar wel de overschreven methode uit de superklasse van Klasse. Laat ons dit super dient om een overschreven methode illustreren met een voorbeeld. toch nog op te roepen class Cocktail ( o b j e c t ) : ... def alcoholpercentage ( z e l f ) : r i j = GetallenRij ( ) for ingr in z e l f . ingredienten : r i j . voegToe ( ingr . percentage * ⤦ Ç ingr . drank . alcoholpercentage ) return r i j .som ( ) class CocktailMetGarnituur ( Cocktail ) : ... def alcoholpercentage ( z e l f ) : return ⤦ Ç super ( CocktailMetGarnituur , z e l f ) . alcoholpercentage ( ) ⤦ Ç − 0.01 Merk op dat het natuurlijk niet zou werken, moesten we de laatste lijn van dit voorbeeld vervangen door gewoon: return z e l f . alcoholpercentage ( ) − 0.01 Dit zou immers zorgen voor een nooit eindigend programma, waarin de methode alcoholpercentage uit CocktailMetGarnituur zichzelf altijd zou blijven oproepen. Het gebruik van de functie super dient dus precies om ervoor te zorgen dat deze methode niet zichzelf oproept, maar wel de gelijknamige methode uit de klasse Cocktail. 47
Een waarschuwingswoordje: het gebruik van de functie super werkt enkel maar bij klassen in de nieuwe stijl, maw. die een (directe of indirecte) subklasse zijn van object.
3.4 Voorbeelden 3.4.1 Vierkanten We definiëren een klasse Vierkant als subklasse van de klasse Veelhoek uit Sectie 2.5.1. Deze klasse overschrijft de constructor van de klasse Veelhoek om ervoor te zorgen dat we een Vierkant kunnen aanmaken op basis van één hoekpunt en een lengte, in plaats van door het opsommen van al de vier hoekpunten. Daarnaast voegt deze constructor ook een extra attribuut toe (de klasse Veelhoek heeft alleen maar een attribuut punten), namelijk de lengte van een zijde. De andere methodes van de klasse Veelhoek, in het bijzonder de methode omtrek, worden gewoon overgeërfd. Een bijkomende methode oppervlakte wordt toegevoegd. class Vierkant ( Veelhoek ) : def _ _ i n i t _ _ ( z e l f , linksonder , lengte ) : z e l f . lengte = lengte linksboven = Punt ( linksonder . x , linksonder . y + lengte ) rechtsboven = Punt ( linksboven . x + lengte , linksboven . y ) rechtsonder = Punt ( rechtsboven . x , linksonder . y ) hoekpunten = [ linksonder , linksboven , rechtsboven , rechtsonder ] super ( Vierkant , z e l f ) . _ _ i n i t _ _ ( hoekpunten ) def oppervlakte ( z e l f ) : return z e l f . lengte * * 2 v = Vierkant(Punt(1,1), 2) print v.omtrek() print v.oppervlakte()
48
4
Varia In dit hoofdstuk komen tot slot nog een paar kleinere onderwerpen aan bod.
4.1 Python als server-side scripting taal Python kan gebruikt worden als taal om webpagina’s mee te genereren, en om gegevens van webformulieren mee te verwerken. Dit kan via het CGI protocol. De essentie hiervan is eenvoudig: als je jouw Python programma op de juiste plaats zet (en de webserver op de juiste manier geconfigureerd is), dan zal dit worden opgeroepen als de gebruiker naar een bepaalde URL surft. Via gewone print instructies, kan het Python programma dan HTML code genereren, die zal worden teruggestuurd naar de gebruiker. Het is wel aan te bevelen om als eerst de lijn Content-Type: text/html af te printen, die de webbrowser van de gebruiker vertelt dat er een HTML pagina aankomt. Daarna moet er dan nog een lege lijn volgen, vooraleer de HTML pagina zelf mag komen. #!/usr/bin/python print print print print print
"Content-type: text/html" "" " Hoi " " Dit is een pagina.
" ""
Dit wordt natuurlijk pas interessant als we ook invoer van de gebruiker kunnen verwerken. Herinner je dat de gegevens die een gebruiker invult in een webformulier worden doorgestuurd in de vorm van paren van een naam en een waarde, waarbij de naam afkomstig is van een attribuut van het HTML element waarin de gebruiker zijn gegevens heeft ingevuld. 49
Python biedt een methode getvalue(naam) waarbij de waarde die bij een naam hoort kan worden opgevraagd. Deze methode behoort bij een object dat je aanmaakt met de functie cgi.FieldStorage() uit de module cgi. Het volgend voorbeeld laat zien hoe dit in zijn werk gaat. Dit is een HTML-formulier met daarop een tekstveldje met als de naam naam.