|
Hoofdstuk 15 Klassen en objecten
15.1 Zelf gedefinieerde typen
Veel van de ingebouwde typen uit Python zijn in de vorige hoofdstukken gebruikt. Nu is het de beurt aan een nieuw type. Als voorbeeld zullen we een type aanmaken genaamd Punt; deze vertegenwoordigt een punt in een twee dimensionale omgeving.
In wiskundige notaties worden punten vaak geschreven tussen haakjes en een komma tussen de coördinaten. Bijvoorbeeld: (0, 0) vertegenwoordigt het beginpunt en (x, y) vertegenwoordigt het punt dat x eenheden naar rechts en y eenheden naar boven, ten opzichte van het beginpunt ligt.
We kunnen op verschillende manieren punten definiëren in Python:
• We kunnen de coördinaten afzonderlijk in twee variabelen, x en y opslaan.
• We kunnen de coördinaten als elementen in een lijst of tupel opslaan.
• We kunnen een nieuw type aanmaken om punten als objecten weer te geven.
Een nieuw type aanmaken is een beetje ingewikkelder dan de andere opties maar biedt veel voordelen, die dadelijk duidelijk worden.
Een zelf gedefinieerd type wordt wel een Klasse (class) genoemd. Een klassedefinitie lijkt hierop:
Deze kop geeft aan dat de nieuwe klasse een Punt is van het soort object, dat een ingebouwd type is.
De body is een documentstring die uitleg geeft over de functie van de klasse. U kunt variabelen en functies definiëren binnenin een klassedefinitie maar we komen hier later op terug.
Het definiëren van een klasse genaamd Punt maakt een klasseobject aan.
Omdat Punt is gedefinieerd op het hoogste niveau, is zijn "volledige naam" __main__.Punt.
Het klasseobject is net een fabriek die objecten aanmaakt. Om een Punt aan te maken roept u Punt aan alsof het een functie is.
De antwoordwaarde is een verwijzing naar een Punt object, die we aan blanco toe wijzen. Een nieuw object aanmaken wordt instantiation (concretisering) genoemd en het object is een instantie van een klasse.
Wanneer u een instantie print, vertelt Python u tot welke klasse deze behoort en waar deze in het geheugen is opgeslagen; het voorvoegsel 0x betekent dat het volgende getal hexadecimaal is.
15.2 Attributen
U kunt waarden aan een instantie toekennen met behulp van de punt-notatie:
Deze zinsbouw komt overeen met de zinsbouw voor het selecteren van een variabele uit een module zoals math.pi of string.whitespace. In dit geval echter kennen we de waarden toe aan benoemde elementen van een object. Deze elementen worden attributen genoemd.
Het volgende diagram geeft het resultaat weer van deze toewijzing. Een statusdiagram dat een object en zijn attributen weergeeft wordt een objectdiagram genoemd:
De variabele blanco verwijst naar een Punt object dat twee attributen bevat. Elk attribuut verwijst naar een floating-point getal.
U kunt de waarde van een attribuut lezen door gebruik te maken van dezelfde zinsbouw:
De expressie blanco.x betekent: "Ga naar het object blanco; verwijs naar en haal de waarde van x op". In dit geval kennen we de waarde toe aan een variabele genaamd x. Er bestaat geen conflict tussen de variabele x en het attribuut x.
U kunt de punt-notatie gebruiken als onderdeel van iedere expressie. Bijvoorbeeld:
U kunt een instantie doorgeven als een argument op de gebruikelijke manier. Bijvoorbeeld:
print_punt krijgt een punt als argument mee en geeft dit weer in een wiskundige notatie. Om dit aan te roepen geeft u blanco als een argument mee:
Binnenin de functie is p een alias voor blanco, dus als de functie p wijzigt, verandert blanco ook.
Oefening 15.1 Schrijf een functie genaamd afstand die de twee punten als argumenten meekrijgt en de afstand tussen die twee teruggeeft.
15.3 Rechthoeken
Soms ligt het voor de hand wat de attributen voor een object zijn maar in andere gevallen moet u zelf een beslissing nemen. Bijvoorbeeld: Stel u voor dat u een klasse ontwerpt die rechthoeken vertegenwoordigt. Welke attributen zou u gebruiken om de locatie en de afmeting van een rechthoek te specificeren? U kunt de hoek negeren om het simpel te houden en aannemen dat de rechthoek of verticaal of horizontaal is.
Er bestaan tenminste twee mogelijkheden:
• U kunt een hoek van de rechthoek (of het centrum), de breedte en de hoogte specificeren.
• U kunt de tegenovergestelde hoeken specificeren.
Op dit moment is het moeilijk te zeggen welke mogelijkheid beter is, dus zullen we de eerste mogelijkheid uitwerken, gewoon als voorbeeld.
Hierbij de klasse definitie:
de documentstring heeft de volgende attributen: breedte en hoogte zijn getallen, hoek is een Punt object die de linker onderhoek aanduidt.
Om een rechthoek af te beelden moet u een instantie aanmaken voor een rechthoekobject en waarden toekennen aan de attributen:
De expressie doos.hoek.x betekent: "Ga naar het object doos; verwijs naar en selecteer het attribuut genaamd hoek, ga vervolgens naar dat object en selecteer het attribuut genaamd x".
De figuur geeft de status van dit object weer:
Een object dat een attribuut is van een ander object is ingesloten.
15.4 Instantie als antwoordwaarde
Functies kunnen instanties teruggeven. Bijvoorbeeld: zoek_het_midden krijgt een Driehoek als een argument mee en geeft een Punt terug die de coördinaten van het midden van de Rechthoek bevat:
Hierbij een voorbeeld die doos als een argument doorgeeft en het resultaat, Punt, toekent aan midden:
15.5 Objecten zijn te wijzigen
U kunt de staat van een object wijzigen door iets aan zijn attributen toe te wijzen. Bijvoorbeeld: de grootte van een rechthoek wijzigen zodat de positie veranderd kan worden door de waarden van de breedte of de hoogte aan te passen:
U kunt ook een functie schrijven die de objecten aanpast. Bijvoorbeeld: groei_driehoek krijgt een rechthoekobject en twee getallen dbreedte en dhoogte deze getallen worden opgeteld bij de breedte en de hoogte van de rechthoek:
Hierbij een voorbeeld dat het effect demonstreert:
Binnenin de functie is recht een alias voor doos, dus als u de functie recht aanpast, wijzigt doos.
Oefening 15.2 Schrijf een functie genaamd verplaats_rechthoek die een rechthoek en twee getallen dx}} en {{{dy meekrijgt. Deze moet de locatie van de rechthoek veranderen door dx op te tellen bij de x coördinaat van de hoek en dy op te tellen bij de y coördinaat van de hoek.
15.6 Kopiëren
Het gebruik van aliassen kan een programma moeilijk leesbaar maken omdat veranderingen op één plaats onverwachte wijzigingen op een andere plaats tot gevolg heeft. Het is moeilijk om alle variabelen bij te houden die mogelijk naar een ander gegeven object verwijzen..
Kopiëren van een object is vaak een alternatief voor een alias. De kopie module bevat een functie genaamd copy die een object kan dupliceren:
p1 en p2 bevatten dezelfde gegevens maar ze zijn niet hetzelfde Punt.
De is operator geeft aan dat p1 en p2 niet hetzelfde object zijn; dat is ook wat we verwachten. Maar u zou kunnen verwachten dat == wel waar zou zijn dus een True op zou leveren, omdat deze punten dezelfde gegevens bevatten. Echter voor instanties is het standaard gedrag van de == operator hetzelfde als de is operator. Deze controleert de identiteit van het object, niet de gelijkwaardigheid van het object. Dit gedrag kan veranderd worden, later zullen we zien hoe.
Gebruikt u copy.copy om een rechthoek te dupliceren, dan zult u zien dat deze het object rechthoek kopieert maar niet het ingesloten object Punt.
Zie hier hoe het object diagram eruit ziet:
Deze bewerking wordt een oppervlakkige kopie genoemd, omdat deze het object en bijbehorende referenties kopieert maar niet de ingesloten objecten.
Voor de meeste applicaties is dit precies wat u zou willen. In dit voorbeeld, het aanroepen van groei_rechthoek zal één van de rechthoeken de andere niet beïnvloeden maar het aanroepen van verplaats_rechthoek op één van de twee zal beiden beïnvloeden! Dit gedrag is verwarrend en foutgevoelig.
Gelukkig heeft de copy module een methode genaamd deepcopy die niet alleen het object kopieert, maar ook de objecten waar deze naar verwijst en de objecten waar deze vervolgens naar verwijzen, enzovoorts. Daarom is het niet verrassend dat deze bewerking een diepe kopie wordt genoemd.
doos3 en doos zijn totaal gescheiden objecten.
Oefening 15.3 Schrijf een versie van verplaats_rechthoek die een nieuwe rechthoek aanmaakt en een nieuwe rechthoek teruggeeft, in plaats van de oude aan te passen.
15.7 Debuggen
Zodra u met object gaat werken, zult u waarschijnlijk nieuwe uitzonderingen ontdekken. Probeert u een attribuut te benaderen dat niet bestaat dan krijgt u een AttributeError:
Bent u er niet zeker van tot welk type het object behoort, dan kunt u dat vragen:
Weet u niet zeker of een object een specifiek attribuut heeft, dan kunt u de ingebouwde functie gebruiken hasattr:
Het eerste argument kan elk object zijn; het tweede argument is een string die de naam bevat van het attribuut.
15.8 Woordenlijst
klasse: Een door de gebruiker gedefinieerd type. Een klassedefinitie maakt een nieuw klasseobject aan.
klasseobject: Een object dat informatie bevat over een door de gebruiker gedefinieerd type. Het klasseobject kan gebruikt worden om instanties van het type te maken.
instantie: Een object dat tot een klasse behoort.
attribuut: Een van de benoemde waarden verbonden met een object.
ingesloten (object): Een object dat is opgeslagen als een attribuut van een ander object.
oppervlakkige kopie: Om de inhoud van een object te kopiëren, inclusief alle verwijzingen naar ingesloten objecten. Geëffectueerd door de "copy" functie in de "copy" module.
diepe kopie: Om de inhoud van een object te kopiëren tezamen met de ingesloten objecten en ieder object daar weer binnenin, enzovoorts. Geëffectueerd door de "deepcopy" functie in de "copy" module.
objectdiagram: Een diagram dat de objecten, hun attributen en de waarden van de attributen weergeeft.
15.9 Oefeningen
Oefening 15.4 World.py, dit is een onderdeel van Swampy (zie hoofdstuk 4) en bevat een klassedefinitie voor een door de gebruiker gedefinieerd type genaamd World. U kunt dit als volgt importeren:
from World import World
Deze versie van de import instructie importeert de World klasse uit de World module. De volgende code maakt een World object aan en roept de mainloop methode aan, die wacht op de gebruiker.
Een venster moet verschijnen met een titelbalk en een leeg vierkant. We zullen het venster gebruiken voor het tekenen van Punten, Rechthoeken en andere figuren. Voeg de volgende regels toe vóór de aanroep van mainloop en voer het programma opnieuw uit.
U moet een groene rechthoek met een zwarte omtrek zien. De eerste regel maakt het doek aan, die in het venster als een wit vierkant verschijnt. Het doekobject levert methoden zoals rechthoek voor het tekenen van verschillende figuren.
bdoos is een lijst die een "begrensde doos" van de rechthoek vertegenwoordigt. Het eerste paar coördinaten zijn die van de linker onderhoek van de rechthoek, het tweede paar zijn die van de rechter bovenhoek.
U kunt een cirkel als volgt tekenen:
doek.cirkel([-25,0], 70, omtrek=None, vulling='red')
De eerste parameter is het coördinatenpaar voor het middelpunt van de cirkel, de tweede parameter is de straal.
Voegt u deze regels toe aan het programma, dan is het resultaat de nationale vlag van Bangladesh (zie nl.wikipedia.org/wiki/Vlaggen_van_de_wereld).
Schrijf een functie genaamd teken_rechthoek die een doek en een rechthoek meekrijgt als argumenten en een afbeelding van een rechthoek op het doek tekent.
Voeg een attribuut, genaamd kleur, toe aan uw rechthoekobjecten en pas teken_rechthoek aan zodat deze het attribuut kleur gebruikt als opvulkleur.
Schrijf een functie genaamd teken_punt die een doek en een Punt als argumenten meekrijgt en een afbeelding van een punt op het doek tekent.
Definieer een nieuwe klasse genaamd Cirkel met geschikte attributen en maak een aantal Cirkelobjecten. Schrijf een functie genaamd teken_cirkel die een cirkel op het doek tekent.
- Schrijf een programma die de nationale vlag van de Tsjechische republiek tekent. Tip: U kunt een polygoon als volgt tekenen:
Ik heb een klein programmaatje geschreven dat alle beschikbare kleuren opsomt; dit is te downloaden bij thinkpython.com/code/color_list.py.