Onderdelen


5. Datastructuren

Dit hoofdstuk gaat wat dieper in op een aantal zaken die al genoemd zijn; ook worden enkele nieuwe begrippen geïntroduceerd.


5.1 Meer over lists

Het datatype list heeft nog meer methods. Hier vind je alle methods van listobjecten:

append( x)
Voeg een item toe aan het eind van de list; hetzelfde als a[len(a):] = [x].

extend( L)
Breid de list uit door er alle items uit de meegegeven list aan toe te voegen; hetzelfde als a[len(a):] = L.

insert( i, x)
Voeg een item in op de gegeven positie in de list. Het eerste argument is de index van het element waarvóór het nieuwe element ingevoegd zal worden, dus a.insert(0, x) voegt x aan het begin van de list in, en a.insert(len(a), x) is hetzelfde als a.append(x).

remove( x)
Verwijder het eerste item met waarde x uit de list. Als zo'n item niet bestaat, wordt een foutmelding gegeven.

pop( [i])
Verwijder het item dat op de gegeven positie in de list gevonden wordt, uit de list; het item wordt als resultaat teruggegeven. Als geen index gespecificeerd is, geeft a.pop() het laatste item in de list terug; ook nu wordt het item verwijderd uit de list. (De vierkante haken om de i in de method signature geven aan dat het argument optioneel is, niet dat je daar vierkante haken moet intypen. Je zult deze notatiewijze vaak tegekomen in de Python Library Reference.)

index( x)
Geef de index terug van het eerste item in de list met waarde x. Als zo'n item niet bestaat, wordt een foutmelding gegeven.

count( x)
Geef als resultaat het aantal keren terug dat x in de list voorkomt.

sort( )
Sorteer de items in de list. (Deze operatie wordt uitgevoerd op de list zelf (in place); er wordt geen kopie teruggegeven).

reverse( )
Draai de volgorde van de list om. (Deze operatie wordt uitgevoerd op de lijst zelf; er wordt geen kopie teruggegeven).

Het volgende voorbeeld laat het gebruik van de meeste methods van het listobject zien:

>>> a = [66.25, 333, 333, 1, 1234.5]
>>> print a.count(333), a.count(66.25), a.count('x')
2 1 0
>>> a.insert(2, -1)
>>> a.append(333)
>>> a
[66.25, 333, -1, 333, 1, 1234.5, 333]
>>> a.index(333)
1
>>> a.remove(333)
>>> a
[66.25, -1, 333, 1, 1234.5, 333]
>>> a.reverse()
>>> a
[333, 1234.5, 1, 333, -1, 66.25]
>>> a.sort()
>>> a
[-1, 1, 66.25, 333, 333, 1234.5]


5.1.1 Lists gebruiken als stacks

Met de methods die voor het listobject beschikbaar zijn, is het zeer eenvoudig om lists als stacks te gebruiken, waarbij het laatste element dat toegevoegd werd, het eerste is dat ervanaf gehaald en teruggegeven wordt (last-in, first-out). Om een item toe te voegen op de stack, gebruik je append(). Om er een element vanaf te halen, gebruik je pop() zonder expliciet een index mee te geven. Bijvoorbeeld:

>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
>>> stack.pop()
6
>>> stack.pop()
5
>>> stack
[3, 4]


5.1.2 Lists gebruiken als queues

Ook kun je lists heel goed gebruiken als queues (wachtrijen), waarbij het eerste element wat werd toegevoegd, ook het eerste element is wat eruitgehaald wordt (first-in, first-out). Om een item achteraan de queue toe te voegen, gebruik je append(). Om een item vooraan de queue eruit te halen, gebruik je pop() met index 0. Bijvoorbeeld:

>>> queue = ["Eric", "John", "Michael"]
>>> queue.append("Terry")           # Terry sluit aan
>>> queue.append("Graham")          # Graham sluit aan
>>> queue.pop(0)
'Eric'
>>> queue.pop(0)
'John'
>>> queue
['Michael', 'Terry', 'Graham']


5.1.3 Tools voor functional programming

Er zijn drie ingebouwde functies beschikbaar die zeer nuttig zijn voor gebruik met lists: filter(), map(), and reduce().

"filter(functie, sequence)" geeft een sequence terug (van hetzelfde sequencetype, indien mogelijk) die bestaat uit alle items uit sequence waarvoor functie(item) waar is. Zo kun je priemgetallen bijvoorbeeld als volgt bepalen:

>>> def f(x): return x % 2 != 0 and x % 3 != 0
...
>>> filter(f, range(2, 25))
[5, 7, 11, 13, 17, 19, 23]

"map(functie, sequence)" roept functie(item)aan voor ieder item in sequence, met als resultaat een list van de teruggegeven waarden. Bereken bijvoorbeeld een aantal derdemachten:

>>> def cube(x): return x*x*x
...
>>> map(cube, range(1, 11))
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]

Het is mogelijk om meer dan 1 sequence mee te geven; de functie moet dan wel evenveel argumenten hebben als er sequences meegegeven worden. De functie wordt dan aangeroepen met het corresponderende item uit iedere sequence (of met None als de ene sequence korter is dan de andere). Bijvoorbeeld:

>>> seq = range(8)
>>> def add(x, y): return x+y
...
>>> map(add, seq, seq)
[0, 2, 4, 6, 8, 10, 12, 14]

"reduce(functie, sequence)" geeft één enkele waarde terug, die berekend wordt door de functie functie, die twee parameters moet hebben, eerst aan te roepen voor de eerste twee items van sequence, daarna voor het resultaat van die berekening en het volgende item, enzovoort. Zo kun je de som van de getallen van 1 tot en met 10 als volgt berekenen:

>>> def add(x,y): return x+y
...
>>> reduce(add, range(1, 11))
55

Als de sequence maar één item bevat, wordt dat item teruggegeven; als de sequence leeg is, wordt een exception gegenereerd.

Met behulp van een derde argument kan de startwaarde aangegeven worden. In zo'n geval wordt voor een lege sequence de startwaarde teruggegeven. In het algemeen wordt de functie eerst toegepast op de startwaarde en het eerste item uit de sequence, daarna op het resultaat daarvan en het volgende item, enzovoort. Bijvoorbeeld:

>>> def sum(seq):
...     def add(x,y): return x+y
...     return reduce(add, seq, 0)
... 
>>> sum(range(1, 11))
55
>>> sum([])
0

Het is overigens niet de bedoeling om de in dit voorbeeld gedefinieerde versie van sum()te gebruiken voor optellingen: aangezien de noodzaak tot het sommeren van getallen zo vaak voorkomt, is er een ingebouwde functie sum(sequence) voor ontwikkeld, die exact hetzelfde werkt. (Nieuw in versie 2.3).

5.1.4 List comprehensions

List comprehensions voorzien in een kernachtige methode om lists te creëren zonder je toevlucht te hoeven nemen tot het gebruik van map(), filter() en/of lambda. De definitie van de list die eruit volgt, is meestal inzichtelijker dan bij lists die op één van die manieren tot stand zijn gekomen. Iedere list comprehension bestaat uit een expressie gevolgd door een for-clause, gevolgd door nul of meer for- of if-clauses. Het resultaat van een list comprehension is de list die ontstaat na het evalueren van de expressie in de context van de eropvolgende for- en if-clauses. Als het resultaat van de expressie een tuple is, moeten er haken omheen gezet worden.

>>> versfruit = ['  banaan', '  framboos ', 'passievrucht  ']
>>> [wapen.strip() for wapen in versfruit]
['banaan', 'framboos', 'passievrucht']
>>> vec = [2, 4, 6]
>>> [3*x for x in vec]
[6, 12, 18]
>>> [3*x for x in vec if x > 3]
[12, 18]
>>> [3*x for x in vec if x < 2]
[]
>>> [[x,x**2] for x in vec]
[[2, 4], [4, 16], [6, 36]]
>>> [x, x**2 for x in vec]      # error – voor tuples moeten haakjes toegevoegd worden
  File "<stdin>", line 1, in ?
    [x, x**2 for x in vec]
               ^
SyntaxError: invalid syntax
>>> [(x, x**2) for x in vec]
[(2, 4), (4, 16), (6, 36)]
>>> vec1 = [2, 4, 6]
>>> vec2 = [4, 3, -9]
>>> [x*y for x in vec1 for y in vec2]
[8, 6, -18, 16, 12, -36, 24, 18, -54]
>>> [x+y for x in vec1 for y in vec2]
[6, 5, -7, 8, 7, -5, 10, 9, -3]
>>> [vec1[i]*vec2[i] for i in range(len(vec1))]
[8, 12, -54]

List comprehensions zijn veel flexibeler dan map(), en kunnen gebruikt worden met functies met meer dan één argument, en met geneste functies:

>>> [str(round(355/113.0, i)) for i in range(1,6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']


5.2 Het statement del

Het is ook mogelijk om een item te verwijderen op basis van zijn index in plaats van zijn waarde: nl. met behulp van het statement del. Je kunt dit statement ook gebruiken om slices uit een list te verwijderen (wat we hiervoor opgelost hebben door een lege list aan de slice toe te kennen). Bijvoorbeeld:

>>> a = [-1, 1, 66.25, 333, 333, 1234.5]
>>> del a[0]
>>> a
[1, 66.25, 333, 333, 1234.5]
>>> del a[2:4]
>>> a
[1, 66.25, 1234.5]

del kun je ook gebruiken om complete variabelen op te ruimen:

>>> del a

Na aanroep van dit statement levert een referentie aan de naam a een foutmelding op (in ieder geval totdat er een nieuwe waarde aan a wordt toegekend). We zullen later nog andere toepassingen voor del bekijken.


5.3 Tuples en sequences

We hebben al gezien dat lists en strings veel eigenschappen gemeen hebben, zoals het gebruik van indexen en slicing. Lists en strings zijn twee voorbeelden van sequence-datatypes. Aangezien Python nog steeds in ontwikkeling is, kunnen er in de toekomst nieuwe sequencetypes bijkomen. Op dit moment is er nog een andere standaardsequence: de tuple.

Een tuple bestaat uit een aantal waarden die van elkaar gescheiden worden door komma's, bijvoorbeeld:

>>> t = 12345, 54321, 'hallo!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hallo!')
>>> # Tuples kunnen genest worden:
... u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hallo!'), (1, 2, 3, 4, 5))

Zoals je ziet, worden tuples in uitvoer altijd tussen haken gezet, zodat geneste tuples op de juiste wijze geïnterpreteerd worden. Bij het invoeren worden ze geaccepteerd met of zonder haken eromheen; overigens zijn de haken meestal sowieso nodig (omdat tuples vaak deel uitmaken van een andere expressie).

Tuples kunnen op veel verschillende manieren gebruikt worden. Bijvoorbeeld: (x, y) coördinaatparen, medewerkerrecords uit een database, etc. Net als strings kunnen tuples niet gewijzigd worden: het is niet mogelijk om een waarde toe te kennen aan een individueel item in een tuple (dit effect kun je echter wel gedeeltelijk simuleren door middel van slicen en concateneren). Het is wel mogelijk om tuples te creëren die mutable objecten bevatten, zoals lists.

Het creëren van een tuple met 0 of 1 items vormt een speciaal probleem: in de syntax zijn een paar extra eigenaardigheidjes ingebouwd om dit mogelijk te maken. Lege tuples worden gedeclareerd met een paar lege haakjes; een tuple met slechts 1 item wordt gevormd door de waarde gevolgd door een komma (het is niet voldoende om één enkele waarde tussen haakjes te zetten). Lelijk, maar wel effectief. Bijvoorbeeld:

>>> empty = ()
>>> singleton = 'hallo',    # <-- let op de komma achter de waarde
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hallo',)

Het statement t = 12345, 54321, 'hallo!' is een voorbeeld van tuple packing (het verpakken als tuple): de waarden 12345, 54321 en 'hello!' worden verpakt in een tuple. Het omgekeerde is ook mogelijk:

>>> x, y, z = t

Dit heet, heel toepasselijk, sequence unpacking (het uitpakken van een sequence). Dit kan alleen als de lijst van variabelen links evenveel elementen heeft als de sequence rechts. Merk op dat multiple assignment in feite gewoon een combinatie is van tuple packing en sequence unpacking!

De twee operaties zijn overigens niet volledig symmetrisch: het inpakken van meerdere waarden heeft altijd een tuple als resultaat, terwijl uitpakken voor iedere type sequence werkt.


5.4 Dictionaries

Een ander nuttig ingebouwd datatype in Python is de dictionary. Dictionaries staan in sommige andere talen bekend als “associative memories” of “associatieve arrays”. Anders dan sequences, die geïndexeerd worden met behulp van een getallenreeks, worden dictionaries geïndexeerd op basis van keys (sleutels). Elk onveranderlijk type kan als key gebruikt worden: strings en getallen kunnen altijd als key dienen. Tuples kunnen als key gebruikt worden als ze alleen strings, getallen, of andere tuples bevatten; als een tuple ook maar één veranderlijk object bevat (rechtstreeks of indirect), kan hij niet als key gebruikt worden. Lists kunnen niet als key gebruikt worden, aangezien lists gewijzigd kunnen worden door middel van de append() en extend() methods, en ook door slicing en assignments op basis van indexen.

Je kunt een dictionary het best opvatten als een ongeordende verzameling van key:value pairs (sleutel:waardeparen) zonder vaste volgorde, waarbij de keys uniek moeten zijn (binnen de dictionary waarin ze zich bevinden). Een lege dictionary maak je aan met een paar accolades: {}. Beginwaarden geef je aan door tussen de accolades een lijst van key:value pairs op te nemen die door komma's van elkaar gescheiden worden; dit is ook de notatie die in uitvoer gebruikt wordt voor dictionaries .

De belangrijkste operaties op een dictionary zijn het opslaan van een waarde met een bepaalde key, en het opvragen van een waarde gegeven zijn key. Met del is het ook mogelijk om een key:value pair te verwijderen. Als je een waarde probeert op te slaan met een key die al in gebruik is, vervalt de vorige waarde die met die key geassocieerd was. Het opvragen van een waarde met een niet-bestaande key levert een foutmelding op.

De method keys() van het dictionaryobject geeft een list terug van alle keys die in de dictionary gebruikt worden, in willekeurige volgorde (als je een gesorteerde list wilt hebben, kun je de method sort() toepassen op de list met keys). Gebruik de method has_key() om te controleren of een bepaalde key voorkomt in de dictionary.

Hier volgt een klein voorbeeld van het gebruik van dictionaries:

>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'sape': 4139, 'guido': 4127, 'jack': 4098}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'guido': 4127, 'irv': 4127, 'jack': 4098}
>>> tel.keys()
['guido', 'irv', 'jack']
>>> tel.has_key('guido')
True

De constructor dict() creëert directories op basis van een list van key-value pairs (aangeboden in de vorm van tuples). Als de paren volgens een bepaald patroon geconstrueerd zijn, kan de list gespecificeerd worden met behulp van een list comprehension:

>>> dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
{'sape': 4139, 'jack': 4098, 'guido': 4127}
>>> dict([(x, x**2) for x in vec])     # gebruik een list comprehension
{2: 4, 4: 16, 6: 36}


5.5 Iteratietechnieken (looping)

Als je itereert (looping) over een dictionary, kun je keys en hun corresponderende waarden gelijktijdig ophalen met behulp van de method iteritems().

>>> knights = {'gallahad': 'de oprechte', 'robin': 'de dappere'}
>>> for k, v in knights.iteritems():
...     print k, v
...
gallahad de oprechte
robin de dappere

Bij het itereren over een sequence kunnen de index en de bijbehorende waarde van de huidige positie gelijktijdig opgehaald worden met behulp van de functie enumerate().

 
>>> for i, v in enumerate(['boter', 'kaas', 'eieren']):
...     print i, v
...
0 boter
1 kaas
2 eieren

Om gelijktijdig over twee of meer sequences te kunnen itereren, kunnen de items in de sequences paarsgewijs benaderd worden met de functie zip().

>>> vragen = ['Hoe heet je', 'Waar ben je naar op zoek', 'Wat is je lievelingskleur']
>>> antwoorden = ['Lancelot', 'De heilige graal', 'Blauw']
>>> for q, a in zip(vragen, antwoorden):
...     print '%s?  %s.' % (q, a)
...     
Hoe heet je?  Lancelot.
Waar ben je naar op zoek?  De heilige graal.
Wat is je lievelingskleur?  Blauw.


5.6 Meer over condities

De condities die gebruikt worden in de while-en if-statements hierboven, kunnen, naast de gebruikelijke vergelijkingsoperatoren, ook nog andere operatoren bevatten.

De vergelijkingsoperatoren in en not in gaan na of een waarde al dan niet voorkomt in een sequence. De operatoren is en is not bepalen of twee objecten gelijk zijn; dit is alleen van belang voor mutable objecten zoals lists. Alle vergelijkingsoperatoren hebben gelijke prioriteit; deze is lager dan die van alle numerieke operatoren.

Vergelijkingen kunnen in een reeks gekoppeld worden. Bijvoorbeeld: a < b == c test of a kleiner is dan b en of b gelijk is c.

Vergelijkingen kunnen gecombineerd worden met de Boolean operatoren and en or, en de uitkomst van een vergelijking kan (net als iedere willekeurige Boolean expressie) van een negatie voorzien worden met not. De Boolean operatoren hebben op hun beurt weer een lagere prioriteit dan vergelijkingsoperatoren; bij de Boolean operatoren onderling heeft not de hoogste prioriteit en or de laagste, zodat A and not B or C hetzelfde is als (A and (not B)) or C. Uiteraard kun je met behulp van haakjes de prioriteit naar believen aanpassen.

De Boolean operatoren and en or zijn zogenaamde “short-circuit operators” (kortgesloten operatoren): hun argumenten worden van links naar rechts geëvalueerd, en de evaluatie wordt afgebroken zodra de uitkomst bekend is. Bijvoorbeeld: als A en C waar zijn, maar B is onwaar, dan wordt in de expressie A and B and C de expressie C niet geëvalueerd, aangezien de uitkomst na evaluatie van A and B al vaststaat. In het algemeen is de teruggegeven waarde van een kortgesloten operator gelijk aan het laatste geëvalueerde element (bij gebruik als een algemene waarde, niet als een Boolean).

Het is mogelijk om het resultaat van een vergelijking of andere Boolean expressie toe te kennen aan een variabele. Bijvoorbeeld,

>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'

Merk op dat in Python, anders dan in C, er binnen expressies geen toekenning kan plaatsvinden. C-programmeurs zullen daar niet blij mee zijn, maar hierdoor wordt wel een type problemen vermeden wat je in C-programma's tegenkomt: “=” intypen in een expressie terwijl je “==” bedoelde.


5.7 Sequences vergelijken met andere types

Sequenceobjecten mogen vergeleken worden met andere objecten van hetzelfde sequencetype. Bij de vergelijking wordt gekeken naar de lexicografische volgorde: eerst worden de eerste items van beide sequences met elkaar vergeleken, en als ze niet hetzelfde zijn staat daarmee de uitkomst van de vergelijking vast; zijn ze wel hetzelfde, dan worden de volgende items uit beide sequences, en zo verder totdat tenminste één van de sequences geen elementen meer heeft om te vergelijken. Als twee items zelf sequences van hetzelfde type zijn, wordt de lexicografische vergelijking recursief uitgevoerd. Als alle items in twee sequences als identiek uit de vergelijking komen, worden de sequences geacht identiek te zijn. Als het begin van de ene sequence overeenkomt met de andere, wordt de kortste sequence als de kleinste beschouwd. De lexicografische sortering voor strings maakt gebruik van de ASCII-volgorde voor individuele karakters. Enkele voorbeelden van vergelijkingen tussen sequences van hetzelfde type:

(1, 2, 3)              < (1, 2, 4)
[1, 2, 3]              < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4)           < (1, 2, 4)
(1, 2)                 < (1, 2, -1)
(1, 2, 3)             == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab'))   < (1, 2, ('abc', 'a'), 4)

Merk op dat het mogelijk is om objecten van een verschillend type te vergelijken. De uitkomst is vooraf bepaald, maar wel willekeurig: de typen worden gesorteerd op basis van hun naam. Zodoende is een lijst altijd kleiner dan een string, een string is altijd kleiner dan een tuple, etcetera. Numerieke typen worden vergeleken op basis van hun numerieke waarde; dus 0 is gelijk aan 0.0, etc.5.1



Voetnoten

... etc.5.1
Vertrouw niet al te veel op de regels voor het vergelijken van objecten van een verschillend type: mogelijk worden deze aangepast in een toekomstige versie van de taal.