Hoofdstuk 17 Klassen en methodes
17.1 Object-georiënteerde functies
Python is een object-oriented programming language (object-georiënteerde programmeertaal); dit houdt in dat deze functies levert die object-georiënteerd programmeren ondersteunen.
Het is niet gemakkelijk uit te leggen wat object-georiënteerd programmeren is, maar we zijn al wel verschillende aspecten tegengekomen:
• Programma's worden gemaakt met objectdefinities en functiedefinities; veel programmeerwerk wordt uitgedrukt in operators en objecten.
• Elke definitie van een object correspondeert met een object of concept in de echte wereld; de functies die uitgevoerd worden op het betreffende object staan in verbinding met de operaties die ook uitgevoerd worden in de echte wereld.
Bijvoorbeeld: De klasse Tijd, gedefinieerd in hoofdstuk 16, komt overeen met de manier waarop mensen de tijd registreren en de gedefinieerde functies komen overeen met die zaken die mensen doen met tijden. Op dezelfde manier corresponderen de klassen Punt en rechthoek met de wiskundige concepten van een punt en een rechthoek.
Tot nu toe hebben we nog niet de voordelen benut van de mogelijkheden die Python biedt voor object-georiënteerd programmeren. Deze mogelijkheden zijn niet strikt noodzakelijk, de meeste leveren een alternatieve zinsbouw voor zaken die we al hebben uitgevoerd. Maar in veel gevallen is het alternatief beknopter en ondersteunt deze beter de structuur van het programma.
Bijvoorbeeld: in het programma Tijd bestaat er geen voor de hand liggende verbinding tussen de klassedefinitie en de functiedefinities die daarop volgen. Na wat onderzoek wordt het duidelijk dat elke functie tenminste één Tijd object als argument gebruikt.
Deze observatie is de motivatie voor methodes; een methode is een functie die verbonden is met een specifieke klasse. We hebben methodes gezien voor strings, lijsten, woordenboeken en tupels. In dit hoofdstuk zullen we methodes definiëren voor door de gebruiker gedefinieerde typen.
Methodes zijn in semantisch opzicht hetzelfde als functies, maar er zijn twee verschillen qua zinsbouw:
• Methodes worden binnen een klassedefinitie gedefinieerd om de relatie tussen de klasse en de methode expliciet te maken.
• De zinsbouw voor het aanroepen van een methode is anders dan de zinsbouw voor het aanroepen voor een functie.
In de volgende paragrafen zullen we de functies uit de twee vorige hoofdstukken nemen en deze omzetten naar methodes. Deze transformatie is zuiver mechanisch; u kunt dit eenvoudig uitvoeren door een aantal stappen te volgen. Hebt u een goed gevoel bij het omzetten van de ene vorm naar de andere, dan bent u in staat om de beste vorm te kiezen voor alles wat u wilt programmeren.
17.2 Objecten printen
In hoofdstuk 16 hebben we een klasse Tijd gedefinieerd. In oefening 16.1 hebt u de functie print_tijd geschreven:
Bij het aanroepen van deze functie moet u een Tijd object als argument meegeven:
Voor het maken van print_tijd volgens een methode hoeven we alleen de functiedefinitie binnen in de klassedefinitie te plaatsen. Let op de verandering van het inspringen.
We kunnen op twee manieren print_tijd aanroepen. De eerste (en minst gebruikelijke) manier is om de zinsbouw van een functie te gebruiken:
Bij deze manier van puntnotatie is tijd de naam van de klasse en print_tijd de naam van de methode. start is meegegeven als een parameter.
De tweede (en beknoptere) manier is om de zinsbouw van een methode te gebruiken:
Bij deze manier van puntnotatie is print_tijd de naam van de methode (nogmaals) en start is het object waarvan de methode is aangeroepen; dit wordt het onderwerp genoemd. Net zoals het onderwerp van een zin inhoudt waar de zin over gaat, gaat het onderwerp van een methodeaanroep, over dat waar de methode over gaat.
In de methode wordt het onderwerp toegekend aan de eerste parameter, dus in dit geval is start toegekend aan tijd.
Volgens afspraak wordt de eerste parameter van een methode self genoemd, zodat het gebruikelijker is om print_tijd op deze manier te schrijven:
De reden voor deze afpraak is een impliciete metafoor:
• De zinsbouw voor een functieaanroep, print_time(start), suggereert dat de functie een actieve agent is. Deze zegt zoiets als, "Hé print_tijd! Hier heb je een object om te printen".
• Bij object-geörienteerd programmeren zijn de objecten de actieve agenten. Een methode aanroep als start.print_tijd() zegt "Hé start! Print jezelf alsjeblieft".
Deze verandering van perspectief is misschien beleefder, maar het ligt niet voor de hand dat deze nuttiger is. In de voorbeelden die we tot nu toe gezien hebben, is dat wellicht niet zo. Maar soms maakt het verplaatsen van de verantwoordelijkheid van functies naar objecten het mogelijk om functies te schrijven die breder inzetbaar zijn en het maakt het gemakkelijker om code te onderhouden en te hergebruiken.
Oefening 17.1 Herschrijf tijd_naar_int (van paragraaf 16.4) als een methode. Waarschijnlijk is int_to_tijd niet geschikt om te herschrijven als een methode, het is niet duidelijk welk object je moet aanroepen!
17.3 Nog een voorbeeld
Hierbij de versie van verhoging (uit paragraaf 16.3) herschreven als een methode:
Deze versie veronderstelt dat tijd_naar_int als een methode is geschreven, net zoals in Oefening 17.1. Merk ook op dat dit een pure functie is, dus geen veranderaar.
Op deze manier roep je verhoging aan:
Het onderwerp start wordt toegekend aan de eerste parameter, self. Het argument, 1337, wordt toegekend aan de tweede parameter, seconden.
Dit mechanisme kan verwarrend zijn, zeker wanneer u een fout maakt. Bijvoorbeeld: roept u verhoging aan met twee argumenten, dan krijgt u:
Deze foutmelding is op het eerste gezicht verwarrend, omdat er tussen de haakjes slechts twee argumenten staan. Maar het onderwerp wordt ook beschouwd als een argument en dat maakt totaal drie.
17.4 Een ingewikkelder voorbeeld
is_na (uit Oefening 16.2) is iets ingewikkelder omdat het twee Tijd objecten meekrijgt als parameters. In dit geval is het gebruikelijk om de eerste parameter self te noemen, de tweede parameter other:
Om deze methode te gebruiken, moet u die door middel van één object aanroepen en het andere object als argument meegeven:
Het leuke aan deze zinsbouw is dat het bijna leest als Nederlands: "einde is na start"?
17.5 De init methode
De init methode (afkorting voor “initialization” (initialisatie)) is een speciale methode die wordt aangeroepen wanneer een object wordt geïnitialiseerd. De volledige naam is __init__ twee onderlijningtekens gevolgd door init en vervolgens nogmaals twee onderlijningtekens. Een init methode voor de klasse Tijd kan er zo uit zien:
Het is gebruikelijk dat de parameters van __init__ dezelfde namen hebben als hun attributen. De instructie
self.uur = uur
slaat de waarde van de parameter uur op als een attribuut van self.
De parameters zijn optioneel, dus als u Tijd zonder argumenten aanroept krijgt u de standaardwaarden.
Als u één argument meegeeft wordt uur overschreven:
Geeft u twee argumenten mee, dan overschrijven ze uur en minuut.
En geeft u drie argumenten mee, dan overschrijven ze alle drie de standaardwaarden.
Oefening 17.2 Schrijf een init methode voor de klasse Punt die x en y als optionele parameters mee krijgt en deze toekent aan de overeenkomstige attributen.
17.6 De str methode
__str__ is een speciale methode, net zoals __init__, die verondersteld wordt om een stringrepresentatie van een object terug te geven.
Hierbij een voorbeeld van een str methode voor Tijd objecten:
Wanneer u een object print gebruikt, roept Python de str methode aan:
Wanneer ik een nieuwe klasse schrijf, begin ik bijna altijd met __init__ te schrijven, dat maakt het gemakkelijker om objecten en __str__ te concretiseren en dat is handig bij het debuggen.
Oefening 17.3 Schrijf een str methode voor de klasse Punt. Maak een Punt object en print deze.
17.7 Operator overbelasting
Door andere speciale methodes te definiëren, kunt u het gedrag van operators bij door gebruiker gedefinieerde types specificeren. Bijvoorbeeld: definieert u een methode genaamd __add__ voor de klasse Tijd, dan kunt u de + operator gebruiken op Tijdobjecten.
Zie hieronder hoe een definitie eruit kan zien:
En zo kunt u deze toepassen:
Past u de + operator toe op Tijdobjecten, dan zal Python __add__ aanroepen. Drukt u het resultaat af, dan roept Python __str__ aan. Er gebeurt dus heel wat achter de coulissen!
Het gedrag van een operator veranderen, zodat deze werkt met een door gebruiker gedefinieerd type, wordt operator overbelasting genoemd. Voor elke operator in Python bestaat een overeenkomstige speciale methode zoals __add__. Meer details zijn te vinden bij docs.python.org/ref/specialnames.html.
Oefening 17.4 Schrijf een {{{add} methode voor de klasse Punt.
17.8 Type-gebaseerd versturen
In de voorgaande paragraaf hebben we twee Timeobjecten toegevoegd maar u zou wellicht een geheel getal willen toevoegen aan een Tijdobject. De volgende versie van __add__ controleert het type van other en roept of voeg_tijd_toe of verhoging aan:
1 # in klasse Tijd:
2
3 def __add__(self, other):
4 if isinstance(other, Tijd):
5 return self.voeg_tijd_toe(other)
6 else:
7 return self.verhoging(other)
8
9 def add_tijd(self, other):
10 seconden = self.tijd_naar_int() + other.tijd_naar_int()
11 return int_naar_tijd(seconden)
12
13 def verhoging(self, seconden):
14 seconden += self.tijd_naar_int()
15 return int_naar_tijd(seconden)
De ingebouwde functie isinstance krijgt een waarde en een klasseobject mee en geeft True terug als de waarde een instantie van een klasse is.
Is other een Tijdobject, dan roept __add__, add_tijd aan. Anders wordt aangenomen dat de parameter een getal is en roept deze verhoging aan. Deze operatie wordt type-gebaseerd versturen genoemd, omdat deze de berekening naar een andere methode verstuurt, gebaseerd op het type van de argumenten.
Hierbij de voorbeelden die de + operator gebruiken met verschillende typen:
Helaas is deze inrichting van de optelling niet verwisselbaar. Is een geheel getal de eerste operand dan krijgt u:
Het probleem is, dat in plaats van te vragen aan het Tijdobject een geheel getal op te tellen, vraagt Python aan een geheel getal om een Tijdobject op te tellen en deze weet niet hoe dat moet. Maar er bestaat een slimme oplossing voor dit probleem: de speciale methode __radd__; dit staat voor "right-side add" (aan de rechterkant toevoegen). Deze methode wordt aangeroepen als een Tijdobject aan de rechterkant verschijnt van de + operator. Hierbij de definitie:
En zo wordt deze toegepast:
Oefening 17.5 Schrijf een add methode voor Punten die of voor een Puntobject of voor een tupel werkt:
• Is de tweede operand een Punt, dan moet de methode een nieuw Punt teruggeven waarbij de x coördinaat de som is van de x coördinaten van de operands en voor de y coördinaten geldt hetzelfde.
• Is de tweede operand een tupel, dan moet de methode het eerste element van de tupel optellen bij de x coördinaat en het tweede element optellen bij de y coördinaat en een nieuw Punt teruggeven met het resultaat.
17.9 Polymorfisme
Type-gebaseerd versturen is handig als u het nodig hebt maar (gelukkig) is dit niet altijd nodig. U kunt dit vaak voorkomen door functies te schrijven die correct omgaan met argumenten van verschillende typen.
Veel van de functies die we hebben geschreven voor strings zullen werken voor elk soort reeksen. Bijvoorbeeld: in paragraaf 11.1 hebben we histogram gebruikt om het aantal keren te tellen dat een letter voorkomt in een woord.
Deze functie werkt ook voor lijsten, tupels en zelfs voor woordenboeken, zolang als de elementen van s te hashen zijn, zodat deze bruikbaar zijn als sleutels in d.
Functies die werken met verschillende typen worden polymorf (veelvormig) genoemd. Polymorfisme kan hergebruik van code ondersteunen. Bijvoorbeeld de ingebouwde functie sum; deze telt elementen uit een reeks op en werkt zolang de elementen uit de reeks het optellen ondersteunen.
Aangezien Tijdobjecten een add methode leveren werken ze met sum:
In het algemeen geldt dat als alle operaties in een functie werken met een gegeven type, dan werkt de functie met dat type.
De beste soort Polymorfisme is de onbedoelde soort, waarbij u ontdekt dat een functie, die u al geschreven had, kan worden gebruikt voor een type dat niet gepland was.
17.10 Debuggen
Attributen toevoegen aan objecten is toegestaan op iedere plaats in het programma waar code wordt uitgevoerd, maar bent u een voorstander van de typetheorie, dan is het een dubieuze praktijk om objecten te hebben van hetzelfde type, met verschillende sets aan attributen. Een doorgaans goed idee is het initialiseren van alle attributen voor objecten met de init methode.
Weet u niet zeker of een object een specifiek attribuut heeft, dan kunt u de ingebouwde functie hasattr gebruiken (zie paragraaf 15.7).
Een andere manier van benaderen van attributen van een object is via het speciale attribuut __dict__; dit is een woordenboek die attribuutnamen (als strings) koppelt aan waarden:
Voor debug doeleinden zult u het wellicht handig vinden om deze functie bij de hand te hebben:
print_attributen doorloopt de items in het woordenboek met de objecten en drukt elke attribuutnaam af met zijn overeenkomstige waarde.
De ingebouwde functie getattr krijgt een object mee en een attribuutnaam (als een string) en geeft de waarde van het attribuut terug.
17.11 Woordenlijst
object-georiënteerde taal: Een taal die mogelijkheden biedt zoals een zinsbouw voor door de gebruiker gedefinieerde klassen en methodes, die object-georiënteerd programmeren ondersteunt.
object-georiënteerd programmeren: Een manier van programmeren waarbij gegevens en de operaties, die deze bewerken, zijn georganiseerd in klassen en methodes.
methode: Een functie die in een klassedefinitie wordt gedefinieerd en wordt aangeroepen op verzoek van die klasse.
onderwerp: Het object waarmee een methode wordt aangeroepen.
operator overbelasting: Het gedrag van een operator, zoals +, veranderen, zodanig dat deze werkt met een door de gebruiker gedefinieerd type.
type-gebaseerd versturen: Een programmeerpatroon dat het type van een operand controleert en verschillende functies aanroept voor verschillende types.
Polymorfisme: Heeft betrekking op een functie die kan werken met meerdere typen.
17.12 Oefeningen
Oefening 17.6 Deze Oefening is een waarschuwing voor één van de meest voorkomende, en moeilijk te vinden, fouten in Python.
Schrijf een definitie voor een klasse genaamd Kangoeroe met de volgende methoden:
(a) Een __init__ methode die een attribuut genaamd inhoud_buidel als een lege lijst initialiseert.
(b) Een methode genaamd stop_in_buidel die een object van een willekeurig type meekrijgt en deze aan inhoud_buidel toevoegt.
(c) Een __str__ methode die een string weergave van het Kangoeroe object teruggeeft en de inhoud van de buidel.
Test uw code door twee Kangoeroe objecten aan te maken; deze toe te kennen aan de variabelen genaamd kangoe and roe en daarna roe toe te voegen aan de inhoud van de buidel van kangoe.
Download thinkpython.com/code/BadKangaroo.py. Dit bevat een oplossing voor het voorgaande probleem maar dan met een grote vervelende fout. Zoek de fout op en verbeter die. Loopt u vast dan kunt u thinkpython.com/code/GoodKangaroo.py downloaden; deze legt het probleem uit en laat een oplossing zien.
Oefening 17.7 Visual is een Pythonmodule die 3-D plaatjes levert. Deze wordt niet altijd meegeleverd bij de Python installatie. Het kan dus zijn dat u deze moet installeren vanuit de software bibliotheek of vanaf vpython.org.
Het volgende voorbeeld maakt een 3-D ruimte aan die 256 units breed, lang en hoog is en het plaatst het "centrum" op het punt (128, 128, 128). Daarna tekent deze code een blauwe bol.
color is een RGB tupel, dat wil zeggen, de elementen zijn Rood-Groen-Blauw met niveaus tussen 0.0 en 1.0 (Zie nl.wikipedia.org/wiki/RGB-kleursysteem).
Draait u deze code, dan ziet u een venster met een zwarte achtergrond en een blauwe bol. Draait u het scrollwieltje van de muis dan zoomt u in of uit. U kunt één en ander laten roteren door te slepen met de rechtermuisknop ingedrukt, maar met maar één bol op het scherm is het lastig om enig verschil te zien.
De volgende lus maakt een kubus van de bollen:
- Plaats deze code in een script en zorg ervoor dat dat werkt.
- Pas het programma zodanig aan dat elke bol in de kubus de kleur heeft aangenomen die overeenkomt met zijn positie in de RGB ruimte. Let erop dat de coördinaten in het bereik van 0–255 vallen, maar de RGB tupels vallen in het bereik van 0.0–1.0.
Download thinkpython.com/code/color_list.py en gebruik de functie read_colors om een lijst met beschikbare kleuren, hun namen en RGB waarden op uw systeem aan te maken. Teken voor elke genoemde kleur een bol in de positie die overeenkomt met zijn RGB waarden.
Mijn oplossing is te zien op thinkpython.com/code/color_space.py.