Nu ik de basis filosofie van object oriëntatie heb uitgelegd, kan ik
beschrijven hoe object oriëntatie werkt in Python. Het begint met het
creëren van nieuwe klassen middels het gereserveerde woord class
.
class
Een “class” (ik zal vanaf dit punt meestal het woord “class” gebruiken
in plaats van “klasse,” omdat het refereert aan het gereserveerde woord
dat Python gebruikt) kun je beschouwen als een nieuw data type. Wanneer
een class gecreëerd is, kun je instanties van de class toekennen aan
variabelen. Om eenvoudig te beginnen, zal ik een class creëren die een
punt in een 2-dimensionale ruimte beschrijft. Ik noem dit de class
Punt
(class benaming kent dezelfde eisen als de benaming van
variabelen, en het is de conventie dat de naam van een class begint met
een hoofdletter). Het creëren van de class Punt
in Python is
ongelofelijk eenvoudig:
class Punt:
pass
Het gereserveerde woord pass
in de class definitie betekent “doe
niks.” Dit gereserveerde woord kun je overal gebruiken waar je een
commando moet plaatsen, maar waar je nog niks hebt om er neer te zetten.
Je mag het niet leeg laten, of alleen een commentaar regel schrijven.
Maar zodra je statements toevoegt, kun je het woord pass
verwijderen.
Om een object te creëren dat een instantie van de class is, ken ik aan een variabele de naam van de class toe, met haakjes erachter, alsof het de aanroep van een functie is (je kunt argumenten tussen de haakjes zetten, maar dat bediscussieer ik later in dit hoofdstuk).
class Punt:
pass
p = Punt()
print( type( p ) )
Natuurlijk is een punt meer dan alleen een object. Een punt heeft een x en een y coördinaat. Omdat Python “soft types” gebruikt, kun je waardes aan attributen toekennen om ze te creëren. Dit doe je in een speciale initialisatie methode in de class.
__init__()
De initialisatie methode van een class heeft de naam __init__
(twee
“underscores,” gevolgd door het woord init
, gevolgd door nog twee
“underscores”). Zelfs als je de methode __init__()
niet expliciet voor
een class definieert, bestaat hij toch. Je kunt de __init__()
methode
gebruiken om alles te initialiseren waarvan je wilt dat het bestaat bij
het instantiëren van de class.
In het geval van Punt
, moet de __init__()
methode ervoor zorgen dat
iedere instantie van Punt
een x
en een y
coördinaat heeft. Dit
implementeer je als volgt:
class Punt:
def __init__( self ):
self.x = 0.0
self.y = 0.0
p = Punt()
print( "({}, {})".format( p.x, p.y ) )
Bestudeer bovenstaande code goed. Je ziet dat de __init__()
methode
net zo gedefinieerd is als je een functie zou definiëren, maar binnen de
class definitie.
__init__()
krijgt één parameter, die self
genoemd is. Iedere methode
die je definieert krijgt altijd minstens één parameter, die gevuld wordt
met een referentie aan het object waarvoor je de methode aanroept. Het
is de gewoonte dat deze eerste parameter altijd self
genoemd wordt.
Dat is niet verplicht, maar iedereen doet het zo (zelfs in het
Nederlands). Als je vergeet parameters op te nemen, krijg je een runtime
error. Als je vergeet self
op te nemen als eerste parameter, maar je
hebt wel andere parameters gespecificeerd, dan zal Python de eerste van
je parameters vullen met een referentie naar het object, en krijg je
daarna waarschijnlijk alsnog een runtime error (omdat je niet had
verwacht dat dat zou gebeuren).
In de __init__()
methode voor Punt
, krijgt de instantie van Punt
twee attributen, die je kunt beschouwen als variabelen die een deel van
het object zijn. Deze worden x
en y
genoemd, en omdat ze een deel
zijn van het object, refereer je aan ze als self.x
en self.y
. Ze
krijgen beide de waarde 0.0, wat betekent dat ze floats zijn.
Om aan deze attributen te refereren als het object eenmaal bestaat,
gebruik je de syntax <object>.<attribuut>
, zoals je kunt zien in de
laatste regel van bovenstaande code, waar het zojuist gecreëerde object
getoond wordt middels een print()
statement.
Je vraagt je misschien af of je alleen attributen kun creëren in de
__init__()
methode. Het antwoord is: nee, je kunt attributen ook
creëren in andere methodes, en zelfs buiten de definitie van de class.
class Punt:
def __init__( self ):
self.x = 0.0
self.y = 0.0
p = Punt()
p.z = 0.0
print( "({}, {}, {})".format( p.x, p.y, p.z ) )
De meeste Python programmeurs (mijzelf incluis) vinden wat er gebeurt in
de code hierboven bijzonder lelijk. Het is een goed gebruik om alle
attributen die je aan een object wilt toekennen uitsluitend te creëren
in de __init__()
methode (hoewel je hun waardes elders kunt wijzigen),
zodat je weet dat iedere instantie van de class deze attributen heeft,
en geen instantie méér dan deze heeft.
Als je een versie van een class wilt hebben met extra attributen, kun je
“inheritance” gebruiken om een nieuwe class te creëren op basis van de
bestaande class, die deze extra attributen heeft. Ik beschrijf dat in
een toekomstig hoofdstuk. Vooralsnog moet je ervoor zorg dragen dat
iedere class alle benodigde attributen gedefinieerd krijgt in de
__init__()
methode.
Net als andere methodes kan de __init__()
methode argumenten krijgen.
Je kunt die argumenten gebruiken om (sommige van) de attributen een
waarde te geven. Bijvoorbeeld, als ik een instantie van Punt
onmiddellijk waardes wil geven voor de x
en y
coördinaten, kan ik de
volgende class definitie gebruiken:
class Punt:
def __init__( self, x, y ):
self.x = x
self.y = y
p = Punt( 3.5, 5.0 )
print( "({}, {})".format( p.x, p.y ) )
__init__()
heeft nu drie parameters. De eerste is nog steeds self
,
aangezien dat altijd de eerste parameter moet zijn. De tweede en derde
heten respectievelijk x
en y
. Ik had ze ook anders mogen noemen
(binnen de beperkingen die gelden voor variabele namen), maar ik koos
x
en y
als de meest voor-de-hand-liggende benamingen. Ik ken x
toe
aan self.x
, en y
aan self.y
.
Ik roep nu de instantie van een punt aan met waardes voor de x
en y
coördinaten als argumenten. Het eerste argument komt bij de methode
binnen als de tweede parameter, en het tweede argument als de derde
parameter, aangezien de eerste parameter altijd gebruikt wordt om de
referentie naar het object zelf door te geven.
Als je het meegeven van zulke argumenten optioneel wilt maken, kun je de parameters default waardes geven middels een assignment in de parameter specificatie. Dat doe je als volgt:
class Punt:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
p1 = Punt()
print( "({}, {})".format( p1.x, p1.y ) )
p2 = Punt( 3.5, 5.0 )
print( "({}, {})".format( p2.x, p2.y ) )
Creëer een list van alle punten met integer coördinaten, waarbij zowel
de x
als de y
coördinaat waardes tussen 0 en 3 kunnen aannemen.
__repr__()
en __str__()
In de code hierboven toon ik de attributen van punten. Maar wat gebeurt er als ik het punt zelf probeer te printen?
class Punt:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
p = Punt( 3.5, 5.0 )
print( p )
Ik neem aan dat je het dan met me eens bent dat het getoonde resultaat
(in feite de plaats in het computergeheugen waar het object zich
bevindt) weinig informatief is. Als ik een punt print, wil ik de
coördinaten zien. Python biedt een voorgedefinieerde methode waarmee ik
dat kan regelen, namelijk de methode __repr__()
. __repr__()
moet een
string retourneren, en die string wordt getoond als geprobeerd wordt het
object te tonen.
class Punt:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
def __repr__( self ):
return "({}, {})".format( self.x, self.y )
p = Punt( 3.5, 5.0 )
print( p )
Dat ziet er een stuk beter uit.
Python kent nog een tweede methode om een string versie van een object
te creëren, namelijk __str__()
. __str__()
is hetzelfde als
__repr__()
, maar wordt alleen gebruikt als het object geprint wordt,
of als argument gebruikt wordt in de format()
methode. Als __str__()
niet gedefinieerd is, wordt __repr__()
in plaats ervan gebruikt (maar
niet vice versa). Als __str__()
wel gedefinieerd is, dan kun je ervoor
zorgen dat iets anders gebeurt wanneer het object getoond wordt middels
de print()
methode, en wanneer het getoond wordt op andere manieren.
Je denkt nu misschien: “welke andere manieren?” De belangrijkste “andere manier” om objecten te tonen is in de command shell, als je alleen de naam van de variabele ingeeft die het object bevat.
Het is de gewoonte dat de __repr__()
methode een string retourneert
die ieder detail van een object bevat, zodat je (indien nodig) aan de
hand van deze string het object opnieuw zou kunnen instantiëren, terwijl
__str__()
een string retourneert die een netjes geformatteerde, goed
leesbare versie van de meest belangrijke informatie van een object
bevat. In veel gevallen kunnen deze strings hetzelfde zijn.
Veel programmeurs negeren __repr__()
en definiëren alleen __str__()
.
Ik denk dat dat verkeerd om is: je zou altijd __repr__()
moeten
definiëren, terwijl __str__()
optioneel is. Als je __repr__()
gebruikt, zorg er dan voor dat je alle details van een object
retourneert. Als je besluit om sommige details weg te laten, kun je
beter __str__()
gebruiken.
Breid de class Punt
uit met een kleur
attribuut, waarbij kleur
een
getal is tussen 0 en \(2^{24}-1\). Zorg ervoor dat de kleur in de
__init__()
methode een waarde krijgt, en in de __repr__()
methode
geretourneerd wordt.