Göm menyn

Mer om datatyper

Förutom listor och strängar finns det en hel del andra användbara datatyper i Python. Vi kommer här gå igenom datatyperna tuple och dictionary.

Tupler

En tupel (eng. tuple) kan ses som en konstant lista. Storleken och innehållet kan inte förändras utan en ny kopia måste skapas med de nya förändringarna. Detta kommer att beskrivas i mer detalj i nästa kapitel.

Tupler ser ut och fungerar som vanliga listor, men skrivs med parenteser istället för hakparenteser. Tupler kan också skrivas utan parenteser, även om det inte är helt rekommenderat då det inte är lika tydligt.

Dokumentation

Här kommer några exempel på hur tupler fungerar.

>>> tup = (1, 2, 3)
>>> tup2 = 1, 2, 3
>>> tup2
(1, 2, 3)
>>> tup[1]
2
>>> len(tup)
3
>>> for elem in tup:
...     print(elem, end="*")
...
1*2*3*

>>> tup[1] = 'hello'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment 

>>> tp2 = 'a', 'b', 'c'
>>> tp2
('a', 'b', 'c')
>>> 1,
(1,)

>>> persons = [("Linus", 50), ("Steve", 56)]
>>> for name, age in persons:
...     print(name, age)
...
Linus 50
Steve 56

Användning av tupler

Eftersom en tupel aldrig ändras kommer den alltid att ha kvar samma storlek och innehåll som när den skapades. Detta gör att många interna rutiner kan köras snabbare. Här ser vi ett trivialt exempel där det är över 6 gånger snabbare att skapa en tupel än en lista:

$ python3 -mtimeit '["fee", "fie", "fo", "fum"]'
10000000 loops, best of 5: 28.3 nsec per loop
$ python3 -mtimeit '("fee", "fie", "fo", "fum")'
50000000 loops, best of 5: 4.4 nsec per loop

Tupler är alltså mer effektiva än listor och lämpar sig för tillfällen då man har konstanta sekvenser, t.ex.:

months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', \
          'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')

Ibland är det användbart att utnyttja tupler där man har strukturerad data och informationen på olika positioner har olika roller, och listor när man har sekvenser med data av samma typ.

Exempel, med rollerna "år, månad, dag":

date = (2012, 9, 11)
dates = [(2012, 8, 27), (2012, 8, 29), (2012, 9, 3)]

Dock måste man i detta fall (oavsett om man använder tupler eller listor) komma ihåg korrekt index:

year = date[0]
month = date[1]
day = date[2]

Den här användningen av mer eller mindre godtyckliga siffror som index blir inte alltid särskilt intuitiv eller lättläst, vilket vi ska återkomma till senare.

Tupler med ett (1) element

Hur skriver man en tupel med 1 element? Vi kan prova så här:

>>> (12)
12

Det blev visst ingen tupel. Har man ett uttryck inom parenteserna blir det bara ett värde, eftersom det finns en grundregel att vi alltid kan sätta parenteser runt vilket uttryck som helst:

>>> (12 * 12)
144
>>> 3 / (12 * 12) 
0.020833333333333332

På den sista raden vill vi ju inte dela 3 med en tupel. Vill vi skriva en tupel med ett element får vi istället använda en specialsyntax som fick uppfinnas bara för detta:

>>> (12,)
(12,)

Dictionaries

Dictionaries är en sorts uppslagstabeller där vi kan associera nycklar med värden I exemplet nedan skapar vi en uppslagstabell som lagrar åldern för ett antal personer. Nycklar är då namn (som lagras som strängar), och värden är åldrar (som lagras som heltal).

Man kan också se dem som en sorts listor där indexen som används för uppslagning inte måste vara heltal, utan kan vara till exempel strängar.

Dokumentation

>>> ages = {'alfred': 45, 'benjamin': 39, 'caesar': 19} 
>>> ages['alfred']
45
>>> ages['david'] = 4711
>>> ages
{'alfred': 45, 'benjamin': 39, 'caesar': 19, 'david': 4711} 
>>> len(ages)
4
>>> 'benjamin' in ages 
True
>>> 'edward' in ages 
False

>>> ages['edward']
Traceback (most recent call last):
File "<stdin>", line 1, in <module> 
KeyError: 'edward'

>>> for elem in ages:
...    print(elem, end="*")
...
alfred*benjamin*caesar*david*

Samma nyckel kan inte förekomma två gånger -- av precis samma anledning som en lista inte kan innehålla samma index två gånger. I en lista är listan[1] en enda specifik position som lagrar ett enda värde, och i en dictionary är dictionary["alfred"] en enda specifik position som lagrar ett enda värde.

Däremot kan själva värdet vara vad som helst, t.ex. en lista. En lista innehåller flera värden, men är i sig ett (sammansatt) värde.

>>> birthdates = { "edward": [1983, 12, 31] }

Dictionaries eller tupler?

Ovan använde vi dictionaries för att lagra enskilda egenskaper hos ett godtyckligt antal personer. Men precis som tupler kan dictionaries också användas även om vi vet alla nycklar i förväg. Då kan vi till exempel välja mellan att representera en enskild person så här:

>>> person = ("Jonas", 1973, "Linköping")
>>> person = { "name": "Jonas", "born": 1973, "city": "Linköping" }

Det finns också en alternativ syntax för att skapa nya dictionaries:

>>> person = dict(name="Jonas", born=1973, city="Linköping")
>>> dict["name"]
Jonas

Notera att man här slipper använda strängar när man skapar sin dictionary -- men att man fortfarande måste använda dem när man ska komma åt informationen.

Vilket är bäst?

Som grundregel är det mycket viktigt att koden man skriver blir lättläst. Att ha namn och inte index gör koden betydligt mer läsbar -- jämför person[2] mot person["city"]; vilken av dem har en mer uppenbar betydelse? Vilken är lättast att läsa i någon annans kod (eller i den egna koden efter några veckor)?

Därför ska man alltid vara väldigt försiktig med att använda tupler som indexeras direkt med siffror, som in person[2].

Vid vissa tillfällen kan dock tuplerna vara mindre problematiska än generellt sett. För datum kan man t.ex. tänka sig att det finns en mer uppenbar indexering som kopplas direkt till den vanliga ordningen år-månad-dag, så att det är mindre jobbigt att hålla reda på att dagar har index 2.

Överkurs:

Python har också en namedtuple-typ som kan användas för att skapa en form av tupler med namngivna fält (som fortfarande är icke ändringsbara). Här kommer man på ett relativt enkelt sätt närmare många andra språks record-typer där man också skriver person.name och inte person["name"] för att komma åt information.

from collections import namedtuple

# Vi skapar en ny sorts tupel med namnet "Person"
# och plats för värden med namn "name", "born" och "city"
Person = namedtuple('Person', 'name born city')

# Skapa en ny person
person = Person(name="Jonas", born=1973, city=Linköping)
print(person.name)

Ordning på nycklar i ett dictionary

När man itererar över ett dictionary (t.ex. för att skriva ut det) är ordningen oftast ointressant, och för att göra vissa optimeringar möjliga har äldre versioner av Python inga garantier för att någon ordning bevaras:

>>> {1: 4, 'a': 47, (1, 2): 'hejsan'}
{'a': 47, 1: 4, (1, 2): 'hejsan'}

Nyare versioner av Python har däremot garanterat att man itererar över nycklar i samma ordning som de adderades:

>>> tabell = { "x": 12, "c": 34, "b": 56 }
>>> tabell
{'x': 12, 'c': 34, 'b': 56}
>>> tabell["a"] = 7
>>> tabell
{'x': 12, 'c': 34, 'b': 56, 'a': 7}

Intern lagring av dictionaries -- hashtabeller

Hur ska en dictionary lagras? Man skulle kunna tänka sig att ha en lista av par:

>>> tabell = [ ("x", 12), ("c", 34), ("b", 56) ]

Men då skulle det vara jobbigt att slå upp elementen eftersom man skulle få söka genom hela listan från början till slut. Med 100 eller 10000 olika nycklar i listan kan detta ta väldigt lång tid.

Istället använder man sig av en så kallad hashtabell. Där är grundtanken att det finns en algoritm som beräknar ett hashvärde för varje nyckel.

>>> "x".__hash__()
8116367903174400156
>>> "c".__hash__()
-9064716270301218589
>>> "b".__hash__()
-330292164206847844

Oj, de hashvärdena blev stora. Men man kan "skära ner dem" till mer hanterbar storlek, beroende på hur många nycklar man egentligen har i sin tabell:

>>> "x".__hash__() % 7
0
>>> "c".__hash__() % 7
1
>>> "b".__hash__() % 7
6

Detta värde kan sedan användas som ett sorts internt index för att snabbt veta var man ska placera ett värde i en intern tabell. Det finns en del mer komplikationer att ta hand om, bland annat eftersom det kan hända att flera olika nycklar får samma hashvärde, men det kommer att diskuteras i en senare kurs om datastrukturer. Just nu räcker det att veta att ett hashvärde behöver kunna beräknas.

I andra språk kan strukturer som liknar dictionaries även kallas för hash map, map och associativa arrayer.

Nycklarnas datatyp -- inga muterbara nycklar

Även om många datatyper kan vara nycklar i dictionaries så kan inte alla vara det. Om man till exempel försöker skapa en dictionary med en lista som nyckel kommer python att klaga.

Anledningen till detta är att listor är muterbara -- man kan ändra innehållet i dem. Om man ändrar innehållet skulle listan få ett nytt hashvärde, och det ställer till med stora komplikationer i en hashtabell (elementet på index 6 ska nu plötsligt ligga på index 3 istället).

För att ett värde ska kunna användas som en nyckel får det alltså inte vara muterbart. Strängar är inte muterbara -- man kan inte ändra dem, bara skapa nya strängar med modifierade värden. Tupler är inte heller muterbara i sig själva, så de kan vara nycklar så länge de inte innehåller muterbara värden.


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2021-12-03