11  Container

Julia bietet eine große Auswahl von Containertypen mit weitgehend ähnlichem Interface an. Wir stellen hier Tuple, Range und Dict vor, im nächsten Kapitel dann Array, Vector und Matrix.

Diese Container sind:

for x  container ... end
x = container[i]

und einige sind auch

Weiterhin gibt es eine Reihe gemeinsamer Funktionen, z.B.

11.1 Tupeln

Ein Tupel ist ein nicht mutierbarer Container von Elementen. Es ist also nicht möglich, neue Elemente dazuzufügen oder den Wert eines Elements zu ändern.

t = (33, 4.5, "Hello")

@show   t[2]               # indizierbar

for i  t println(i) end   # iterierbar
t[2] = 4.5
33
4.5
Hello

Ein Tupel ist ein inhomogener Typ. Jedes Element hat seinen eigenen Typ und das zeigt sich auch im Typ des Tupels:

typeof(t)
Tuple{Int64, Float64, String}

Man verwendet Tupel gerne als Rückgabewerte von Funktionen, um mehr als ein Objekt zurückzulieferen.

# Ganzzahldivision und Rest: 
# Quotient und Rest werden den Variablen q und r zugewiesen

q, r = divrem(71, 6)
@show  q  r;
q = 11
r = 5

Wie man hier sieht, kann man in bestimmten Konstrukten die Klammern auch weglassen. Dieses implict tuple packing/unpacking verwendet man auch gerne in Mehrfachzuweisungen:

x, y, z = 12, 17, 203
(12, 17, 203)
y
17

Manche Funktionen bestehen auf Tupeln als Argument oder geben immer Tupeln zurück. Dann braucht man manchmal ein Tupel aus einem Element.

Das notiert man so:

x = (13,)         # ein 1-Element-Tupel
(13,)

Das Komma - und nicht die Klammern – macht das Tupel.

x= (13)         # kein Tupel
13

11.2 Ranges

Wir haben range-Objekte schon in numerischen for-Schleifen verwendet.

r = 1:1000
typeof(r)
UnitRange{Int64}

Es gibt verschiedene range-Typen. Wie man sieht, sind es über den Zahlentyp parametrisierte Typen und UnitRange ist z.B. ein range mit der Schrittweite 1. Ihre Konstruktoren heißen in der Regel range().

Der Doppelpunkt ist eine spezielle Syntax.

  • a:b wird vom Parser umgesetzt zu range(a, b)
  • a:b:c wird umgesetzt zu range(a, c, step=b)

Ranges sind offensichtlich iterierbar, nicht mutierbar aber indizierbar.

(3:100)[20]   # das zwanzigste Element
22

Wir erinnern an die Semantik der for-Schleife: for i in 1:1000 heißt nicht:

  • ‘Die Schleifenvariable i wird bei jedem Durchlauf um eins erhöht’ sondern
  • ‘Der Schleifenvariable werden nacheinander die Werte 1,2,3,…,1000 aus dem Container zugewiesen’.

Allerdings wäre es sehr ineffektiv, diesen Container tatsächlich explizit anzulegen.

  • Ranges sind “lazy” Vektoren, die nie wirklich irgendwo als konkrete Liste abgespeichert werden. Das macht sie als Iteratoren in for-Schleifen so nützlich: speichersparend und schnell.
  • Sie sind ‘Rezepte’ oder Generatoren, die auf die Abfrage ‘Gib mir dein nächstes Element!’ antworten.
  • Tatsächlich ist der Muttertyp AbstractRange ein Subtyp von AbstractVector.

Das Macro @allocated gibt aus, wieviel Bytes an Speicher bei der Auswertung eines Ausdrucks alloziert wurden.

@allocated r = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
224
@allocated r = 1:20
0

Zum Umwandeln in einen ‘richtigen’ Vektor dient die Funktion collect().

collect(20:-3:1)
7-element Vector{Int64}:
 20
 17
 14
 11
  8
  5
  2

Recht nützlich, z.B. beim Vorbereiten von Daten zum Plotten, ist der range-Typ LinRange.

LinRange(2, 50, 300)
300-element LinRange{Float64, Int64}:
 2.0, 2.16054, 2.32107, 2.48161, 2.64214, …, 49.5184, 49.6789, 49.8395, 50.0

LinRange(start, stop, n) erzeugt eine äquidistante Liste von n Werten von denen der erste und der letzte die vorgegebenen Grenzen sind. Mit collect() kann man bei Bedarf auch daraus den entsprechenden Vektor gewinnen.

11.3 Dictionaries

  • Dictionaries (deutsch: “assoziative Liste” oder “Zuordnungstabelle” oder …) sind spezielle Container.
  • Einträge in einem Vektor v sind durch einen Index 1,2,3…. addressierbar: v[i]
  • Einträge in einem dictionary sind durch allgemeinere keys addressierbar.
  • Ein dictionary ist eine Ansammlung von key-value-Paaren.
  • Damit haben dictionaries in Julia den parametrisierten Typ Dict{S,T}, wobei S der Typ der keys und T der Typ der values ist

Man kann sie explizit anlegen:

# Einwohner 2020 in Millionen, Quelle: wikipedia

EW = Dict("Berlin" => 3.66,  "Hamburg" => 1.85, 
          "München" => 1.49, "Köln" => 1.08)
Dict{String, Float64} with 4 entries:
  "München" => 1.49
  "Köln"    => 1.08
  "Berlin"  => 3.66
  "Hamburg" => 1.85
typeof(EW)
Dict{String, Float64}

und mit den keys indizieren:

EW["Berlin"]
3.66

Das Abfragen eines nicht existierenden keys ist natürlich ein Fehler.

EW["Leipzig"]
KeyError: key "Leipzig" not found
Stacktrace:
 [1] getindex(h::Dict{String, Float64}, key::String)
   @ Base ./dict.jl:477
 [2] top-level scope
   @ ~/Julia/23/Book-ansipatch/chapters/6_ArraysEtcP1.qmd:191

Man kann ja auch vorher mal anfragen…

haskey(EW, "Leipzig")
false

… oder die Funktion get(dict, key, default) benutzen, die bei nicht existierendem Key keinen Fehler wirft sondern das 3. Argument zurückgibt.

@show get(EW, "Leipzig", -1)   get(EW, "Berlin", -1);
get(EW, "Leipzig", -1) = -1
get(EW, "Berlin", -1) = 3.66

Man kann sich auch alle keys und values als spezielle Container geben lassen.

keys(EW)
KeySet for a Dict{String, Float64} with 4 entries. Keys:
  "München"
  "Köln"
  "Berlin"
  "Hamburg"
values(EW)
ValueIterator for a Dict{String, Float64} with 4 entries. Values:
  1.49
  1.08
  3.66
  1.85

Man kann über die keys iterieren…

for i in keys(EW)
    n = EW[i]
    println("Die Stadt $i hat $n Millionen Einwohner.")
end
Die Stadt München hat 1.49 Millionen Einwohner.
Die Stadt Köln hat 1.08 Millionen Einwohner.
Die Stadt Berlin hat 3.66 Millionen Einwohner.
Die Stadt Hamburg hat 1.85 Millionen Einwohner.

odere gleich über key-value-Paare.

for (stadt, ew)  EW
    println("$stadt : $ew  Mill.")
end
München : 1.49  Mill.
Köln : 1.08  Mill.
Berlin : 3.66  Mill.
Hamburg : 1.85  Mill.

11.3.1 Erweitern und Modifizieren

Man kann in ein Dict zusätzliche key-value-Paare eintragen…

EW["Leipzig"] = 0.52
EW["Dresden"] = 0.52 
EW
Dict{String, Float64} with 6 entries:
  "Dresden" => 0.52
  "München" => 1.49
  "Köln"    => 1.08
  "Berlin"  => 3.66
  "Leipzig" => 0.52
  "Hamburg" => 1.85

und einen value ändern.

# Oh, das war bei Leipzig die Zahl von 2010, nicht 2020

EW["Leipzig"] = 0.597
EW
Dict{String, Float64} with 6 entries:
  "Dresden" => 0.52
  "München" => 1.49
  "Köln"    => 1.08
  "Berlin"  => 3.66
  "Leipzig" => 0.597
  "Hamburg" => 1.85

Ein Paar kann über seinen key auch gelöscht werden.

delete!(EW, "Dresden")
Dict{String, Float64} with 5 entries:
  "München" => 1.49
  "Köln"    => 1.08
  "Berlin"  => 3.66
  "Leipzig" => 0.597
  "Hamburg" => 1.85

Zahlreiche Funktionen können mit Dicts wie mit anderen Containern arbeiten.

maximum(values(EW))
3.66

11.3.2 Anlegen eines leeren Dictionaries

Ohne Typspezifikation …

d1 = Dict()
Dict{Any, Any}()

und mit Typspezifikation:

d2 = Dict{String, Int}()
Dict{String, Int64}()

11.3.3 Umwandlung in Vektoren: collect()

  • keys(dict) und values(dict) sind spezielle Datentypen.
  • Die Funktion collect() macht daraus eine Liste vom Typ Vector.
  • collect(dict) liefert eine Liste vom Typ Vector{Pair{S,T}}
collect(EW)
5-element Vector{Pair{String, Float64}}:
 "München" => 1.49
    "Köln" => 1.08
  "Berlin" => 3.66
 "Leipzig" => 0.597
 "Hamburg" => 1.85
collect(keys(EW)), collect(values(EW))
(["München", "Köln", "Berlin", "Leipzig", "Hamburg"], [1.49, 1.08, 3.66, 0.597, 1.85])

11.3.4 Geordnetes Iterieren über ein Dictionary

Wir sortieren die Keys. Als Strings werden sie alphabetisch sortiert. Mit dem rev-Parameter wird rückwärts sortiert.

for k in sort(collect(keys(EW)), rev = true)
    n = EW[k]
    println("$k hat $n Millionen Einw.  ")
end
München hat 1.49 Millionen Einw.  
Leipzig hat 0.597 Millionen Einw.  
Köln hat 1.08 Millionen Einw.  
Hamburg hat 1.85 Millionen Einw.  
Berlin hat 3.66 Millionen Einw.  

Wir sortieren collect(dict). Das ist ein Vektor von Paaren. Mit by definieren wir, wonach zu sortieren ist: nach dem 2. Element des Paares.

for (k,v) in sort(collect(EW), by = pair -> last(pair), rev=false)
    println("$k hat $v Mill. EW")
end
Leipzig hat 0.597 Mill. EW
Köln hat 1.08 Mill. EW
München hat 1.49 Mill. EW
Hamburg hat 1.85 Mill. EW
Berlin hat 3.66 Mill. EW

11.3.5 Eine Anwendung von Dictionaries: Zählen von Häufigkeiten

Wir machen ‘experimentelle Stochastik’ mit 2 Würfeln:

Gegeben sei l, eine Liste mit den Ergebnissen von 100 000 Pasch-Würfen, also 100 000 Zahlen zwischen 2 und 12.

Wie häufig sind die Zahlen 2 bis 12?

Wir (lassen) würfeln:


l = rand(1:6, 100_000) .+ rand(1:6, 100_000)
100000-element Vector{Int64}:
 9
 7
 2
 6
 9
 8
 9
 6
 2
 6
 ⋮
 7
 2
 7
 8
 7
 7
 6
 7
 6

Wir zählen mit Hilfe eines Dictionaries die Häufigkeiten der Ereignisse. Dazu nehmen wir das Ereignis als key und seine Häufigkeit als value.

# In diesem Fall könnte man das auch mit einem einfachen Vektor
# lösen. Eine bessere Illustration wäre z.B. Worthäufigkeit in
# einem Text. Dann ist i keine ganze Zahl sondern ein Wort=String

d = Dict{Int,Int}()     # das Dict zum 'reinzählen'

for i in l                    # für jedes i wird d[i] erhöht.      
    d[i] = get(d, i, 0) + 1   
end
d
Dict{Int64, Int64} with 11 entries:
  5  => 10983
  12 => 2824
  8  => 13923
  6  => 13999
  11 => 5451
  9  => 11210
  3  => 5635
  7  => 16596
  4  => 8257
  2  => 2819
  10 => 8303

Das Ergebnis:

using Plots

plot(collect(keys(d)), collect(values(d)), seriestype=:scatter)
Das Erklär-Bild dazu:

https://math.stackexchange.com/questions/1204396/why-is-the-sum-of-the-rolls-of-two-dices-a-binomial-distribution-what-is-define