Previous Page

Up One Level

Next Page

Python Tutorial

Contents

Index

Vorige: 8. Fouten en exceptions Omhoog: Python Tutorial Volgende: 10. Korte rondleiding door de Standard Library
Onderdelen
  • 9.1 Een paar opmerkingen over terminologie
  • 9.2 Scopes en namespaces in Python
  • 9.3 Een eerste blik op classes
    • 9.3.1 Syntax van classdefinities
    • 9.3.2 Classobjecten
    • 9.3.3 Instance-objecten
    • 9.3.4 Methodobjecten
  • 9.4 Losse opmerkingen
  • 9.5 Inheritance
    • 9.5.1 Multiple Inheritance
  • 9.6 Private variabelen
  • 9.7 Losse eindjes
  • 9.8 Exceptions zijn ook classes
  • 9.9 Iterators
  • 9.10 Generators


9. Classes

Het class-mechanisme van Python voegt classes toe als onderdeel van de taal, met een minimum aan nieuwe syntax en semantiek. Het is een mengeling van de class-mechanismen die je vindt in C++ en Modula-3. Net als bij modules, trekken classes in Python geen absolute grens tussen de definitie en de gebruiker, maar vertrouwen er min of meer op dat de gebruiker de beleefdheid heeft om de definitie met rust te laten. De belangrijkste karakteristieken van classes blijven echter onverminderd overeind: het class inheritance mechanisme staat het hebben van meerdere base classes (multiple inheritance) toe, een afgeleide class kan alle methods van zijn base class(es) overriden, en methods kunnen de gelijknamige base class method aanroepen. Objecten kunnen een onbepaalde hoeveelheid private data bevatten.

Om met C++ te spreken: alle class members (inclusief de data members) zijn public, en alle memberfuncties zijn virtual. Er zijn geen speciale constructors of destructors. Net als in in Modula-3, zijn er geen verkorte notaties voorhanden voor het refereren aan de members van een object vanuit zijn methods: methods worden gedeclareerd met als eerste argument een expliciete representatie van het object zelf, die bij het aanroepen van de method impliciet wordt meegegeven. Net als in Smalltalk zijn classes zelf ook objecten, zij het in de ruimste zin van het woord; in Python zijn alle datatypes objecten. Hierdoor wordt importeren en hernoemen mogelijk. Anders dan in C++ en Modula-3 kunnen ingebouwde types gebruikt worden als base classes die door de gebruiker uitgebreid kunnen worden. En, net als in in C++ maar anders dan in Modula-3 kunnen de meeste ingebouwde operatoren met een specifieke syntax (bijvoorbeeld aritmetische operatoren) hergedefinieerd worden voor class instances.


9.1 Een paar opmerkingen over terminologie

Bij gebrek aan een algemeen geaccepteerde terminologie op het gebied van object-oriëntatie en classes, zullen we hier en daar gebruikmaken van termen afkomstig uit Smalltalk en C++. (We hadden beter Modula-3 termen kunnen gebruiken, aangezien de object-georiënteerde semantiek daarvan dichterbij Python ligt dan die van C++, maar we gaan er vanuit dat slechts weinig lezers daarvan gehoord hebben.)

We moeten ook waarschuwen voor een teminologie-valkuil waar met name lezers met OO-ervaring in kunnen vallen: het woord “object” betekent in Python niet altijd “class instance”. Net als in C++ en Modula-3, en anders dan in Smalltalk, zijn niet alle Python-types classes: de basale ingebouwde types zoals integers en lists bijvoorbeeld, evenals enkele wat exotischere types zoals file-objecten. Echter, alle Python-types delen een stuk gemeenschappelijke semantiek wat zich het beste laat omschrijven met het woord “object”.

Objecten hebben een identiteit, en meerdere namen (in meerdere scopes) kunnen aan hetzelfde object gebonden worden. Dit principe staat in andere talen bekend als aliasing. Dit wordt vaak niet begrepen bij een eerste blik op Python, en kun je ook met een gerust hart vergeten zolang je alleen werkt met immutable basale types (getallen, strings, tuples). Aliasing heeft echter ook een (bedoeld!) effect op de semantiek van code die betrekking heeft op mutable objecten zoals lists, dictionaries, en de meeste types die entiteiten buiten het programma representeren (bestanden, windows in een GUI, etc.). Meestal kun je daar in je programma je voordeel mee doen, aangezien aliases zich in sommige opzichten gedragen als pointers. Zo is het bijvoorbeeld zeer efficiënt om een object mee te geven als argument, aangezien door de implementatie alleen de pointer wordt meegegeven; als de functie waaraan het object wordt meegegeven, het object wijzigt, ziet degene die de functie heeft aangeroepen, de wijzigingen dus terug in het object. Hiermee wordt de noodzaak van twee verschillende mechanismen voor het doorgeven van argumenten (à la Pascal) geëlimineerd.


9.2 Scopes en namespaces in Python

Voor we classes introduceren, zullen we eerst het een en ander vertellen over regels ten aanzien van scopes in Python. Classdefinities halen enkele aardige trucjes uit met namespaces, en je moet dus weten hoe scopes en namespaces werken om volledig te begrijpen wat er precies gebeurt. Overigens is enige kennis over dit onderwerp nuttig voor iedere gevorderde Python-programmeur.

Laten we beginnen met een aantal definities.

Een namespace is een vertaling van namen naar objecten. De meeste namespaces worden op dit moment geïmplementeerd als Python-dictionaries, maar dat merk je normaal gesproken nergens aan (behalve aan de performance), en het is mogelijk dat dit in de toekomst wijzigt. Voorbeelde van namespaces zijn: de verzameling van ingebouwde namen (functies als abs(), en namen van ingebouwde exceptions); namen van globale variabelen in een module; en namen van lokale namen in een functieaanroep. In zekere zin vormt de verzameling van attributen van een object ook een namespace. Het belangrijkste punt om te onthouden ten aanzien van namespaces, is dat er totaal geen relatie is tussen de namen in verschillende namespaces; zo kunnen twee verschillende modules bijvoorbeeld een functie “maximize” bevatten zonder dat dit tot verwarring leidt – programmeurs die de modules gebruiken, moeten de functie in aanroepen vooraf laten gaan door de modulenaam.

We gebruiken het woord attribuut overigens voor iedere naam die volgt op een punt (".") -- in de expressie z.real, is real bijvoorbeeld een attribuut van het object z. Strikt genomen is een referentie aan een naam in een module een referentie aan een attribuut: in de expressie modname.funcname, is modname een moduleobject, en is funcname een attribuut van het moduleobject. In dit geval is er toevallig een 1:1 vertaling mogelijk tussen de attributen van de module en de globale namen die gedefinieerd zijn in de module: ze delen dezelfde namespace! 9.1

Attributen kunnen read-only (alleen lezen) of writable (overschrijfbaar) zijn. In het laatste geval is het mogelijk om een waarde aan een attribuut toe te kennen (“assignment”). Attributen van een module zijn writable: je kunt dus schrijven "modnaam.het_antwoord = 42". Writable attributen kunnen ook verwijderd worden met het statement del. Bijvoorbeeld: met "del modnaam.het_antwoord" verwijder je het attribuut het_antwoord uit het object genaamd modnaam.

Namespaces worden op verschillende momenten gecreëerd en hebben een verschillende levensduur. De namespace die de ingebouwde namen bevat, wordt aangemaakt bij het opstarten van de Python-interpreter, en wordt nooit weggegooid. De globale namespace van een module wordt aangemaakt bij het inlezen van de moduledefinitie; normaal gesproken blijven namespaces van modules eveneens in leven totdat de interpreter afgesloten wordt. De statements die uitgevoerd worden in een interpretersessie (ingelezen vanuit een scriptbestand, of interactief ingelezen) worden gezien als onderdeel van een module met de naam __main__, waardoor ze hun eigen globale namespace hebben. (De ingebouwde namen wonen eigenlijk ook in een module; deze heet __builtin__.)

De lokale namespace voor een functie wordt aangemaakt wanneer de functie wordt aangeroepen, en wordt weer verwijderd als de functie eindigt, of een exception genereert die niet binnen de functie wordt afgehandeld (“vergeten” is overigens een nauwkeurigere omschrijving van wat er gebeurt dan “verwijderen”). Uiteraard hebben recursieve aanroepen ieder hun eigen lokale namespace.

Een scope is het tekstuele gebied binnen een Python-programma waar een namespace rechtstreeks benaderbaar is. “Rechtstreeks benaderbaar'' betekent in dit verband dat referenties zonder voorafgaande kwalificatie in die namespace zullen zoeken naar de bijbehorende naam.

Hoewel scopes statisch bepaald worden, worden ze dynamisch gebruikt. Op ieder moment tijdens de executie zijn er op zijn minst drie geneste scopes waarvan de namespaces rechtstreeks benaderbaar zijn: de binnenste scope, die als eerste doorzocht wordt, bevat de lokale namen; de namespaces van eventuele functies die hem omspannen, en die van binnen naar buiten doorzocht worden; de middelste scope, die daarna onderzocht wordt, bevat de globale namen van de huidige module; en de buitenste scope, die als laatste doorzocht wordt, bevat alle ingebouwde namen.

Als een naam als globaal gedeclareerd wordt, springen alle referenties en assignments rechtstreeks naar de middelste scope, die de globale namen van de module bevatten. Anders zijn alle variabelen die buiten de binnenste scope gevonden worden, read-only.

Meestal refereert de lokale scope aan de lokale namen uit de (tekstueel) huidige functie. Buiten functies referereert de lokale scope aan dezelfde namespace als de globale scope: de namespace van de module. Als de module classdefinities bevat, wordt daarmee nog een namespace aan de lokale scope toegevoegd.

Het is belangrijk dat je je realiseert dat scopes tekstueel bepaald worden: de globale scope van een functie die gedefinieerd wordt in een module, is de namespace van die module, ongeacht waarvandaan of met welke alias de functie aangeroepen wordt. Aan de andere kant wordt de uiteindelijke naamherleiding dynamisch (tijdens run-time) uitgevoerd; in de definitie van de taal neigt men echter steeds meer naar het statisch herleiden van namen, dus al bij het compuleren – vertrouw dus niet teveel op dynamische herleiding! (Om precies te zijn: lokale variabelen worden al statisch herleid.)

Een bijzonder trekje van Python is dat assignments altijd in de binnenste scope plaatsvinden. Als er een assignment gedaan wordt, wordt er geen data gekopieerd; er wordt alleen een naam aan een object gebonden. Dit gaat ook op voor verwijderingen: met het statement "del x" verwijder je de binding tussen x en de namespace die gerefereerd wordt door de lokale scope. Sterker nog, alle operaties die nieuwe namen introduceren, gebruiken daarvoor de lokale scope: dit geldt in het bijzonder voor import-statements en functiedefinities, die de modulenaam of de functienaam aan de lokale scope binden. (Met het global-statement kun je aangeven dat bepaalde variabelen in de global scope bestaan.)


9.3 Een eerste blik op classes

Voor classes introduceren we enige nieuwe syntax, drie nieuwe objecttypes, en wat nieuwe semantiek.


9.3.1 Syntax van classdefinities

In zijn eenvoudigste vorm ziet een classdefinitie er als volgt uit:

class ClassNaam:
    <statement-1>
    .
    .
    .
    <statement-N>

Classdefinities moeten, net als functiedefinities (def-statements) uitgevoerd worden voordat ze enig effect sorteren. (Het is ook mogelijk om een classdefinitie op te nemen in een tak van een if-statement, of in een functie.)

In de praktijk zijn de statements in een classdefinitie meestal functiedefinities, maar andere statements zijn ook toegestaan, wat soms nuttig kan zijn -- wel komen daar later nog op terug. Voor functiedefinities in een class wordt doorgaans een specifiek soort argumentenlijst gehanteerd, waarvan de vorm bepaald wordt door de aanroepconventies voor methods – ook dit wordt later uitgelegd.

Als de interpreter een classdefinitie binnengaat, wordt een nieuwe namespace aangemaakt en als lokale scope gebruikt – alle toekenningen aan lokale variabelen vinden dus plaats in deze nieuwe namespace; in het bijzonder wordt bij functiedefinities de naam van de nieuwe functie opgenomen als onderdeel van de nieuwe namespace.

Als de classdefinitie volledig doorlopen en op de normale manier verlaten wordt (dat wil zeggen, via het einde), wordt een classobject aangemaakt. Dit is niets anders dan een wrapper voor de inhoud van de namespace die door de classdefinitie gecreëerd werd; in de volgende paragraaf zullen we classobjecten nader bekijken. De oorspronkelijke lokale scope (d.w.z., de scope die actief was voordat de interpreter de classdefinitie binnenging) wordt opnieuw actief, en het classobject wordt in deze scope gebonden aan de naam van de class zoals gegeven in de classdefinitie-header (ClassNaam in het voorbeeld).


9.3.2 Classobjecten

Classobjecten ondersteunen twee types operaties: referenties aan attributen, en instantiëring.

Referenties aan attributen gebruiken de standaardsyntax die voor alle referenties aan attributen in Python gebruikt wordt: object.naam. Als geldige namen worden beschouwd alle namen die in de namespace van de class aanwezig waren toen het classobject aangemaakt werd. Dus, als de classdefinitie er zo uitzag:

class MijnClass:
    "Een simpele voorbeeldclass"
    i = 12345
    def f(self):
        return 'hallo wereld'

dan zijn MijnClass.i en MijnClass.f geldige attribuutreferenties, die respectievelijk een integer en een methodobject teruggeven. Je kunt ook waarden toekennen aan attributen van een class, dus je kunt de waarde van MijnClass.i veranderen met behulp van een assignment. Ook __doc__ is een geldig attribuut, dat de docstring behorende bij de class teruggeeft: "Een simpele voorbeeldclass".

Voor de instantiëring van een class gebruiken we een functieachtige notatie. Het is alsof het classobject een parameterloze functie is die een nieuwe instance van de class teruggeeft. Bijvoorbeeld (uitgaande van de class hierboven):

x = MijnClass()

maakt een nieuwe instance van de class aan, en kent deze toe aan de lokale variabele x.

De instantiëring (het “aanroepen” van een classobject) maakt een leeg object aan. Veel classes geven er de voorkeur aan om nieuwe objecten bij hun creatie een welomschreven initiële toestand mee te geven. Voor dat doel kan een class de speciale method __init__() definiëren, als volgt:

    def __init__(self):
        self.data = []

Als een class een __init__() method definieert, wordt __init__() automatisch tijdens de instantiëring aangeroepen voor de nieuwe instance. In dit voorbeeld kun je dus een nieuwe, geïnitialiseerde instance verkrijgen door:

x = MyClass()

Uiteraard mag de __init__() method arguments hebben, om zijn flexibiliteit te vergroten. In dat geval worden eventuele aan de instantiëringsoperator meegegeven argumenten doorgegeven aan __init__(). Bijvoorbeeld:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
... 
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)


9.3.3 Instance-objecten

Wat kunnen we nu eigenlijk met instance-objecten? De enige operaties die door instance-objecten begrepen worden, zijn attribuutreferenties. Er zijn twee soorten geldige attribuutnamen.

De eerste soort zullen we data-attributen noemen. Deze komen overeen met “instance-variabelen” in Smalltalk, en met “data members” in C++. Data- attributen hoeven niet gedeclareerd te worden; net als lokale variabelen, ontstaan ze op het moment dat er voor de eerste keer een waarde aan wordt toegekend. Dus, als x de instance van MijnClass is die we hierboven aangemaakt hebben, dan drukt het volgende stukje code de waarde 16, af, waarna deze verdwijnt zonder een spoor achter te laten:

x.teller = 1
while x.teller < 10:
    x.teller = x.teller * 2
print x.teller
del x.teller

Het tweede type attribuutreferentie wat begrepen wordt door instance-objecten, zijn methods. Een method is een functie die “bij een object hoort”. (In Python wordt de term method niet alleen in verband met class instances gebruikt: andere objecttypes kunnen ook methods hebben. Listobjecten, bijvoorbeeld, hebben methods genaamd append, insert, remove, sort, enzovoort. In de rest van dit hoofdstuk zullen we de term method echter uitsluitend gebruiken voor methods van instance-objecten, tenzij anders aangegeven.)

Welke methodnamen geldig zijn voor een instanceobject, hangt af van zijn class. By definition, all attributes of a class that are (user-defined) function objects define corresponding methods of its instances. In ons voorbeeld is x.f dus een geldige referentie aan een method, aangezien MijnClass.f een functie is; x.i is dat echter niet, omdat MijnClass.i geen functie is. Let op: x.f is niet hetzelfde als MijnClass.f – het is een methodobject, geen functieobject.


9.3.4 Methodobjecten

Normaal gesproken wordt een method rechtstreeks aangeroepen:

x.f()

In onze voorbeeld, krijgen we hierop de string 'hello world' terug. De rechtstreekse aanroep is echter geen noodzaak: x.f is een methodobject, en kan opgeslagen worden voor later gebruik. Zo zal:

xf = x.f
while True:
    print xf()

"hallo wereld" blijven afdrukken tot het einde der tijden.

Wat gebeurt er precies als er een method aangeroepen wordt? Misschien is het je opgevallen dat we hierboven x.f() zonder argument hebben aangeroepen, hoewel in de functiedefinitie voor f wel een argument gespecificeerd werd. Wat is er met dat argument gebeurd? Python zal toch zeker wel een exception genereren als een functie met een verplicht attribuut aangeroepen wordt zonder attributen – of dat argument nou gebruikt wordt of niet...

Misschien heb je het antwoord al geraden: het speciale van methods is nou juist dat het object zelf als eerste argument aan de functie wordt meegegeven. In ons voorbeeld is de aanroep x.f() exact hetzelfde als MijnClass.f(x). In het algemeen komt het aanroepen van een method met een lijst van n argumenten overeen met het aanroepen van de corresponderende functie met een argumentenlijst die gecreëerd wordt door het object zelf in te voegen vóór het eerste argument in de lijst.

Als je nog niet helemaal begrijpt hoe methods werken, werkt een kijkje achter de schermen van de implementatie wellicht verhelderend. Wanneer gerefereerd wordt aan een instance-attribuut, en dat attribuut is geen data-attribuut, dan wordt zijn class verder doorzocht: als de naam een functieobject aanduidt, wordt een methodobject gecreëerd door (een pointer naar) het instanceobject samen met het zojuist gevonden functieobject samen te verpakken tot een abstract object: het methodobject. Wanneer het methodobject met een argumentenlijst wordt aangeroepen, wordt het weer uitgepakt; er wordt een nieuwe argumentenlijst geconstrueerd op basis van het instanceobject en de oorspronkelijke argumentenlijst, en het functieobject wordt aangeroepen met de nieuwe argumentenlijst.


9.4 Losse opmerkingen

Data-attributen overriden gelijknamige methodattributen; om onvoorziene naamgevingsconflicten, die moeilijk op te sporten bugs kunnen veroorzaken in grote programma's, te voorkomen, is het verstandig om de een of andere conventie te hanteren waardoor de kans op dit soort conflicten geminimaliseerd wordt. Mogelijke conventies zijn het beginnen van methodnamen met hoofdletters, het hanteren van een uniek voorvoegsel voor namen van data-attributen (wellicht alleen een underscore), of het gebruik van werkwoorden voor methods en zelfstandige naamwoorden voor data-attributen.

Data-attributen kunnen zowel door methods als door gewone gebruikers (clients) van een object. Met andere woorden: classes zijn niet geschikt om zuivere abstracte datatypes mee te implementeren. In feite is er niets in Python waarmee je data hiding kunt afdwingen – alles is gebaseerd op conventies. (Aan de andere kant kan de Python-implementatie, die geschreven is in C, alle implementatiedetails volledig afschermen en, indien nodig, de toegang tot een object bewaken; hier kun je gebruik van maken bij het schrijven van Python-extensies in C.)

Clients moeten zeer voorzichtig zijn met het gebruik van data-attributen – door over de data-attributen heen te walsen, kunnen ze gemakkelijk allerlei onveranderlijke grootheden overhoop halen die door de methods in stand gehouden moeten worden. Merk op dat clients eigen data-attributen aan een instance-object kunnen toevoegen, zonder de geldigheid van de methods aan te tasten, zolang ze erin slagen naamgevingsconflicten te vermijden – ook hier kan een naamgevingsconventie je een kopzorgen besparen.

Er is geen verkorte notatie voor het refereren aan data-attributen (of andere methods!) vanuit een method. Wij zijn van mening dat de leesbaarheid van methods hierdoor feitelijk vergroot wordt: al bladerend door de code van een method zul je lokale variabelen en instancevariabelen niet met elkaar verwarren.

Het is conventie om het eerste argument van een method self te noemen. Meer dan dat is het niet: de naam self heeft absoluut geen speciale betekenis voor Python. (Hou er echter wel rekening mee dat, als je deze conventie niet volgt, je code minder leesbaar wordt voor andere Python-programmeurs; verder is het niet ondenkbaar dat class browser applicaties waar je wellicht gebruik van maakt, zich voor hun werking baseren op deze conventie.)

Ieder functieobject dat een classattribuut is, definieert een method voor instances van die class. Het is geen vereiste dat de functie tekstueel in de classdefinitie is ingesloten: het is voldoen om het functieobject toe te kennen aan een lokale variabele in de class. Zie bijvoorbeeld:

# Functie die buiten de class gedefinieerd is
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1
    def g(self):
        return 'hallo wereld'
    h = g

f, g and h zijn nu allemaal attributen van class C die refereren aan functieobjecten, en als gevolg daarvan zijn ze dus ook allemaal methods van instances van C – waarbij h exact hetzelfde is als g. Merk op dat deze praktijken doorgaans als enig nut hebben dat ze verwarring zaaien bij de lezer van het programma.

Een method kan andere methods aanroepen met behulp van de methodattributen van zijn self-argument:

class Bag:
    def __init__(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)

Een method kan globale namen op dezelfde wijze refereren als gewone functies. De globale scope van een method is de module die de classdefinitie bevat. (De class zelf wordt nooit als globale scope gebruikt!) Hoewel er zelden een goede reden is om globale data te gebruiken in een method, zijn er vele legitieme manieren van gebruik te bedenken voor de globale scope: als je functies en modules importeert in de globale scope, kunnen deze rechtstreeks gebruikt worden door methods; dit geldt ook voor functies en classes die gedefinieerd zijn in de globale scope. Normaal gesproken is de class die de method bevat, zelf gedefinieerd in de globale scope, en in de volgende paragraaf zullen we enkele goede redenen ontdekken waarom een method aan zijn eigen class zou willen refereren!




9.5 Inheritance

Uiteraard zou een taalkenmerk niet de naam “class” verdienen als hij geen inheritance zou ondersteunen. De syntax voor het definiëren van een afgeleide class ziet er zo uit:

class AfgeleideClassNaam(BaseClassNaam):
    <statement-1>
    .
    .
    .
    <statement-N>

De naam BaseClassNaam moet bekend (=gedefinieerd) zijn in de scope die de definitie van de afgeleide class bevat. In plaats van een baseclassnaam, is een expressie ook toegestaan. Dit komt van pas als de baseclass is een andere module gedefinieerd is:

class AfgeleideClassNaam(modulenaam.BaseClassNaam):

De uitvoering van de definitie van een afgeleide class verloopt op dezelfde manier als bij een baseclass. Als het classobject gecreëerd wordt, wordt onthouden wat de baseclass is. Deze wordt gebruikt voor het herleidenvan attribuutreferenties: als een gerefereerd attribuut niet gevonden wordt in de afgeleide class, wordt verder gezocht in de baseclass. Deze regel wordt recursief toegepast als de baseclass zelf weer afgeleid is van een andere class.

Er is verder niets bijzonders aan het instantiëren van een afgeleide class: met AfgeleideClassNaam() creëer je een nieuwe instance van de class. Methodreferenties worden als volgt herleid: het corresponderende classattribuut wordt opgezocht, eventueel door af te dalen in de keten van baseclasses, indien nodig; de referentie is geldig als deze actie een functieobject oplevert.

Afgeleide classes kunnen methods uit hun baseclasses overriden. Aangezien er voor methods geen speciale privileges gelden bij het aanroepen van andere methods van hetzelfde object, kan het zijn dat een baseclassmethod die een andere in dezelfde class gedefinieerde method aanroept, uiteindelijk uitkomt bij een method in een afgeleide class die de betreffende baseclassmethod overridet (voor C++-programmerrs: in Python zijn alle methods virtual.)

Als je in een afgeleide class een baseclassmethod overridet, heb je wellicht eerder de bedoeling om de method uit te breiden dan om hem simpelweg te vervangen. Het is heel eenvoudig om de baseclassmethod rechtstreeks aan te roepen: je schrijft dat "BaseClassNaam.methodnaam(self, argumenten)". Zo nu en dan kan dat ook voor clients nuttig zijn. (Merk op dat dit alleen werkt als de baseclass rechtstreeks in de globale scope wordt gedefinieerd of geïmporteerd.)


9.5.1 Multiple inheritance

Daarnaast biedt Python ondersteuning voor een beperkte vorm van multiple inheritance. Een classdefinitie met meerdere baseclasses ziet er als volgt uit:

class AfgeleideClassNaam(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

De enige regel die we moeten leren kennen om de semantiek hiervan te kunnen begrijpen, is de herleidingsregel die gebruikt wordt voor het refereren aan classattributen: “depth-first, left-to-right”. Dit betekent dat, als een gerefereerd attribuut niet gevonden wordt in AfgeleideClassNaam, verder gezocht wordt in Base1, daarna (recursief) in de baseclasses van Base1; pas als het attribuut daar niet gevonden wordt, wordt verder gezocht in Base2, enzovoort.

(Sommige mensen vinden dat een “breadth first” aanpak – waarbij je eerst Base2 en Base3 doorzoekt voordat je verdergaat met de baseclasses van Base1 – natuurlijker aanvoelt. Hiervoor is het echter een vereiste dat je weet of een bepaald attribuut van Base1 feitelijk gedefinieerd is in Base1 of in één van zijn baseclasses, voordat je kunt bepalen wat de consequenties zijn van een naamgevingsconflict van dat attribuut met een attribuut in Base2. De “depth-first” regel maakt geen onderscheid tussen directe en indirecte attributen van Base1.)

Het zal zijn duidelijk dat ondoordacht gebruik van multiple inheritance een nachtmerrie voor de onderhoudbaarheid van je programmatuur betekent, gegeven het feit dat Python het van naamgevingsconventies moet hebben om naamgevingsconflicten te vermijden. Een bekend probleem met multiple inheritance ontstaat wanneer een class afgeleid wordt van twee classes die toevallig dezelfde baseclass hebben. Hoewel het op zich niet moeilijk is om uit te vinden wat er in zo'n geval gebeurt (de instance krijgt één enkele kopie van de “instancevariabelen” oftewel data-attributen die door de gemeenschappelijke baseclass gebruikt worden), is het niet erg aannemelijk dat deze semantiek op één of andere manier nuttig is.


9.6 Private variabelen

Python heeft een beperkte ondersteuning van private identifiers in classes. Identifiers in de vorm __spam (dus voorafgegaan door tenminste underscores, en afgesloten met hoogstens één underscore) wordt tekstueel vervangen door _classnaam__spam, waarbij classnaam de naam van de huidige class is (exclusief evt. voorafgaande underscores). Deze name mangling staat los van de syntactische positie van de identifier, dus je kunt hiermee class-private instance- en classvariabelen, methods, en globals definiëren; je kunt zelfs instancevariabelen die in deze class private zijn, opslaan in instances van andere classes. Hou er rekening mee dat namen die, nadat deze bewerking erop is losgelaten, langer zijn dan 255 karakters, afgekapt kunnen worden. Buiten classes, of als de classnaam uitsluitend uit underscores bestaat, treedt geen mangling op.

Name mangling is bedoeld om een class op eenvoudige wijze “private” instancevariabelen en -methods te laten definiëren, zonder zich druk te hoeven maken over instancevariabelen die door afgeleide classes gedefinieerd worden, of over code buiten de class die probeert te knoeien met zijn instancevariabelen. Merk op dat de regels voor mangling met name ontworpen zijn op het voorkomen van ongelukken; voor iemand die vastberaden genoeg is, is het nog steeds mogelijk om toegang te krijgen tot en wijzigingen aan te brengen in een variabele die als private gemarkeerd is. Onder bijzonder omstandigheden kan dat zelfs erg nuttig zijn, bijvoorbeeld in de debugger, en dat is één van de redenen waarom dat achterdeurtje niet dichtgetimmerd is. (Mini-bug: als je een afgeleide class creëert met dezelfde naam als zijn baseclass, kun je rechtstreeks toegang krijgen tot de private variabelen van de base class.)

Hou er wel rekening mee dat in code die meegegeven wordt aan exec, eval() of evalfile() , de classnaam van de aanroepende class niet gezien wordt als de huidige class. Dit is vergelijkbaar met het effect van het global-statement; het effect daarvan is eveneens beperkt tot stukken code die gezamenlijk naar butecode gecompileerd worden. Deze beperking geldt ook voor getattr(), setattr() en delattr(), evenals voor het rechtstreeks refereren aan __dict__.


9.7 Losse eindjes

Zo nu en dan kan het nuttig zijn om een datatype à la “records” in Pascal, of “structs” in C te hebben: het gaat dan puur om het bundelen van een aantal (afzonderlijke benoemde) data items. Een lege classdefinitie is hier uitstekend voor te gebruiken:

class Medewerker:
    pass

x = Medewerker() # Maak een leeg Medewerker-record aan

# Vul de velden van het record in
x.naam = 'Mr. X'
x.afd = 'IT'
x.salaris = 1000

Als een stuk Python-code een bepaald abstract datatype verwacht, kun je daar vaak een class aan meegeven die de methods van het betreffende datatype emuleert. Als je bijvoorbeeld een functie hebt die de opmaak van gegevens uit een file-object verzorgt, kun je een class definiëren die beschikt over de methods read() en readline(), maar die zijn gegevens in een stringbuffer bewaart in plaats van in een bestand; vervolgens kun je een instance van deze class als argument meegeven aan de functie.

Methodobjecten in instances hebben ook attributen: m.im_self is het object waar de method onderdeel van uitmaakt, en m.im_func is het functieobject wat correspondeert met de method.


9.8 Exceptions zijn ook classes

User-defined exceptions worden eveneens geïdentificeerd door classes. Met gebruikmaking van dit mechanisme kun je uitbreidbare hiërarchieën van exceptions opzetten.

Er zijn twee nieuwe geldige vormen voor het raise-statement:

raise Class, instance

raise instance

In de eerste vorm moet instance een instance zijn van Class, of van een class die ervan afgeleid is. De tweede vorm is een verkorte notatie van:

raise instance.__class__, instance

Een class in een except-clause is compatibel met een exception als het dezelfde class is, of een baseclass daarvan (andersom werkt het niet – een except clause met een afgeleide class is niet compatibel met een baseclass). De volgende code print bijvoorbeeld B, C, D in die volgorde uit:

class B:
    pass
class C(B):
    pass
class D(C):
    pass

for c in [B, C, D]:
    try:
        raise c()
    except D:
        print "D"
    except C:
        print "C"
    except B:
        print "B"

Merk op dat als de except-clauses omgedraaid waren (met "except B" als eerste), de uitvoer “B, B, B” geweest zou zijn – Python neemt de eerste except-clause die matcht met de exception.

Bij het afdrukken van een foutmelding voor een niet-afgehandelde exception die als class bestaat, wordt eerste de classnaam afgedrukt, daarna een dubbele punt en een spatie, en tenslotte de exceptioninstance, geconverteerd naar een string met behulp van de ingebouwde functie str().


9.9 Iterators

Zo langzamerhand is het je misschien opgevallen dat je over de meeste containerobjecten kunt itereren met behulp van een for-statement:

for element in [1, 2, 3]:
    print element
for element in (1, 2, 3):
    print element
for key in {'een':1, 'twee':2}:
    print key
for char in "123":
    print char
for line in open("mijnbestand.txt"):
    print line

Deze manier van benaderen is duidelijk, beknopt en handig. Het gebruik van iterators is wijdverbreid in Python, en zorgt voor een uniforme benadering van iteraties over verzamelingen binnen Python. Achter de schermen roept het for-statement iter() aan met het containerobject. De functie geeft een iteratorobject terug dat de next() definieert; deze method benadert de elementen in de container één voor één. Als hij alle elementen gehad heeft, genereert next() een StopIteration-exception, waardoor de for-loop weet dat hij zichzelf kan beëindigen. Dit voorbeeld toont de werking ervan:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> it.next()
'a'
>>> it.next()
'b'
>>> it.next()
'c'
>>> it.next()

Traceback (most recent call last):
  File "<pyshell#6>", line 1, in -toplevel-
    it.next()
StopIteration

Als je het mechanisme achter het iterator-protocol eenmaal gezien hebt, kun je op eenvoudige wijze iteratorgedrag toevoegen aan je eigen classes. Definieer daarvoor een __iter__() -method, die een object met de method next() teruggeeft. Als de class al next() definieert, kan __iter__() gewoon self teruggeven:

>>> class Omgekeerd:
    "Iterator om in omgekeerde volgorde te itereren over een sequence"
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def next(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

>>> for char in Omgekeerd('spam'):
        print char

m
a
p
s


9.10 Generators

Generators zijn een simpel en krachtig stuk gereedschap voor het creëren van iterators. Ze worden hetzelfde gecodeerd als gewone functies, maar gebruiken het statement yield in plaats van return om data terug te geven. Iedere keer als next() wordt aangeroepen, gaat de generator verder waar hij was gebleven (hij onthoudt alle waarden, evenals het laatst uitgevoerde statement). We laten met een voorbeeld zien hoe eenvoudig het maken van een generator kan zijn:

>>> def keerom(data):
        for index in range(len(data)-1, -1, -1):
            yield data[index]
                
>>> for char in keerom('golf'):
        print char

f
l
o
g

Alles wat je met generators kunt doen, kun je ook doen met op classes gebaseerde iterators, zoals beschreven in de vorige paragraaf. Generators zijn compacter dan iterators omdat de __iter__()- en next()-methods automatisch gecreëerd worden.

Een andere belangrijke eigenschap is dat de lokale variabelen en de toestand tijdens de uitvoering automatisch bewaard worden voor de volgende aanroep. Daardoor was de functie gemakkelijker en veel duidelijker om te schrijven, dan wanneer we een benadering met class variabelen als self.index en self.data hadden gevolgd.

Naast het automatisch creëren van methods en het opslaan van de execution state, wordt, wanneer het einde van de generator bereikt wordt, automatisch de StopIteration-exception gegenereerd. Alles bij elkaar maken deze kenmerken het ons even gemakkelijk om iterators te creëren als om een gewone functie te maken.



Voetnoten

... namespace!9.1
Afgezien hiervan: module-objecten hebben een verborgen read-only attribuut, “__dict__”, dat de dictionary teruggeeft die gebruikt wordt voor de namespace van de method; de naam __dict__ is een attribuut maar geen globale naam. Het is duidelijk dat het gebruik hiervan een schending van de abstracties van namespace-implementatie is; dit gebruik moet dan ook beperkt worden tot zaken als post-mortem debuggers.

Previous Page

Up One Level

Next Page

Python Tutorial

Contents

Index

Vorige: 8. Fouten en exceptions Omhoog: Python Tutorial Volgende: 10. Korte rondleiding door de Standard Library
Release 2.3.4, documentation updated on May 20, 2004.
See About this document... for information on suggesting changes.