[StartPagina] [IndexVanTitels] [IndexVanWoorden

Afbeeldingen/vorige.png
Vorige

Afbeeldingen/home.png
Terug naar inhoudsopgave

Afbeeldingen/volgende.png
Volgende

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:

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:

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:

   1 class Tijd(object):
   2       """stelt de tijd van de dag voor.
   3           attributen: uur, minuut, seconde"""
   4 def print_tijd(tijd):
   5       print '%.2d:%.2d:%.2d' % (tiid.uur, tijd.minuut, tijd.seconde)

Bij het aanroepen van deze functie moet u een Tijd object als argument meegeven:

   1 >>> start = Tijd()
   2 >>> start.uur = 9
   3 >>> start.minuut = 45
   4 >>> start.seconde = 00
   5 >>> print_tijd(start)
   6 09:45:00

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.

   1 class Tijd(object):
   2       def print_tijd(tijd):
   3            print '%.2d:%.2d:%.2d' % (tijd.uur, tijd.minuut, tijd.seconde)

We kunnen op twee manieren print_tijd aanroepen. De eerste (en minst gebruikelijke) manier is om de zinsbouw van een functie te gebruiken:

   1 >>> Tijd.print_tijd(start)
   2 09:45:00

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:

   1 >>> start.print_tijd()
   2 09:45:00

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:

   1 class tijd(object):
   2        def print_tijd(self):
   3             print '%.2d:%.2d:%.2d' % (self.uur, self.minuut, self.seconde)

De reden voor deze afpraak is een impliciete metafoor:

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:

   1 # In de klasse tijd:
   2        def verhoging(tijd, seconden):
   3             seconden += self.tijd_naar_int()
   4             return int_naar_tijd(seconden)

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:

   1 >>> start.print_tijd()
   2 09:45:00
   3 >>> einde = start.verhoging(1337)
   4 >>> einde.print_tijd()
   5 10:07:17

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:

   1 >>> einde = start.verhoging(1337, 460)
   2 TypeError: verhoging() takes exactly 2 arguments (3 given)

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:

   1 # in klasse tijd:
   2        def is_na(self, other):
   3            return self.tijd_naar_int() > other.tijd_naar_int()

Om deze methode te gebruiken, moet u die door middel van één object aanroepen en het andere object als argument meegeven:

   1 >>> einde.is_na(start)
   2 True

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:

   1 # inside class Tijd:
   2        def __init__(self, uur=0, minuut=0, seconde=0):
   3            self.uur = uur
   4            self.minuut = minuut
   5            self.seconde = seconde

Het is gebruikelijk dat de parameters van __init__ dezelfde namen hebben als hun attributen. De instructie

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.

   1 >>> tijd = Tijd()
   2 >>> tijd.print_tijd()
   3 00:00:00

Als u één argument meegeeft wordt uur overschreven:

   1 >>> tijd = Tijd(9)
   2 >>> tijd.print_tijd()
   3 09:00:00

Geeft u twee argumenten mee, dan overschrijven ze uur en minuut.

   1 >>> tijd = Tijd(9, 45)
   2 >>> tijd.print_tijd()
   3 09:45:00

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:

   1 # in class Tijd:
   2      def __str__(self):
   3            return '%.2d:%.2d:%.2d' % (self.uur, self.minuut, self.seconde)

Wanneer u een object print gebruikt, roept Python de str methode aan:

   1 >>> tijd = Tijd(9, 45)
   2 >>> print tijd 
   3 09:45:00

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:

   1 # in klasse Tijd:
   2      def __add__(self, andere):
   3            seconden = self.tijd_naar_int() + andere.tijd_naar_int()
   4            return int_naar_tijd(seconden )

En zo kunt u deze toepassen:

   1 >>> start = Tijd(9, 45)
   2 >>> duur = Tijd(1, 35)
   3 >>> print start + duur
   4 11:20:00

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:

   1 >>> start = Tijd(9, 45)
   2 >>> duur = Tijd(1, 35)
   3 >>> print start + duur
   4 11:20:00
   5 >>> print start + 1337
   6 10:07:17

Helaas is deze inrichting van de optelling niet verwisselbaar. Is een geheel getal de eerste operand dan krijgt u:

   1 >>> print 1337 + start
   2 TypeError: unsupported operand type(s) for +: 'int' and 'instance'

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:

   1 # in klasse Tijd:
   2      def __radd__(self, other):
   3            return self.__add__(other)

En zo wordt deze toegepast:

   1 >>> print 1337 + start
   2 10:07:17

Oefening 17.5 Schrijf een add methode voor Punten die of voor een Puntobject of voor een tupel werkt:

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.

   1 def histogram(s):
   2      d = woordenboek()
   3      for c in s:
   4            if c not in d:
   5                 d[c] = 1
   6            else:
   7                 d[c] = d[c]+1
   8      return d

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.

   1 >>> t = ['spam', 'ei', 'spam', 'spam', 'ham', 'spam']
   2 >>> histogram(t)
   3 {'ham': 1, 'ei': 1, 'spam': 4}

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:

   1 >>> t1 = Tijd (7, 43)
   2 >>> t2 = Tijd (7, 41)
   3 >>> t3 = Tijd (7, 37)
   4 >>> totaal = sum([t1, t2, t3])
   5 >>> print totaal
   6 23:01:00

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:

   1 >>> p = Punt(3, 4)
   2 >>> print p.__dict__
   3 {'y': 4, 'x': 3}

Voor debug doeleinden zult u het wellicht handig vinden om deze functie bij de hand te hebben:

   1 def print_attributen(obj):
   2       for attr in obj.__dict__:
   3            print attr, getattr(obj, attr)

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.

  1. 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.

  2. 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.

   1 from visual import *
   2 
   3 scene.range = (256, 256, 256)
   4 scene.centrum = (128, 128, 128)
   5 
   6 kleur = (0.1, 0.1, 0.9)                      # voornamelijk blauw
   7 sphere(pos=scene.centrum, radius=128, color=kleur)

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:

   1 t = range(0, 256, 51)
   2 for x in t:
   3       for y in t:
   4            for z in t:
   5                  pos = x, y, z
   6                  sphere(pos=pos, radius=10, color=kleur)
  1. Plaats deze code in een script en zorg ervoor dat dat werkt.
  2. 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.
  3. 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.

Afbeeldingen/vorige.png
Vorige

Afbeeldingen/home.png
Terug naar inhoudsopgave

Afbeeldingen/volgende.png
Volgende


2022-09-08 16:56