--- engine: julia --- ```{julia} #| error: false #| echo: false #| output: false using InteractiveUtils ``` # 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: - **iterierbar:** Man kann über die Elemente des Containers iterieren: ```julia for x ∈ container ... end ``` - **indizierbar:** Man kann auf Elemente über ihren Index zugreifen: ```julia x = container[i] ``` und einige sind auch - **mutierbar**: Man kann Elemente hinzufügen, entfernen und ändern. Weiterhin gibt es eine Reihe gemeinsamer Funktionen, z.B. - `length(container)` --- Anzahl der Elemente - `eltype(container)` --- Typ der Elemente - `isempty(container)` --- Test, ob Container leer ist - `empty!(container)` --- leert Container (nur wenn mutierbar) ## 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. ```{julia} t = (33, 4.5, "Hello") @show t[2] # indizierbar for i ∈ t println(i) end # iterierbar ``` Ein Tupel ist ein **inhomogener** Typ. Jedes Element hat seinen eigenen Typ und das zeigt sich auch im Typ des Tupels: ```{julia} typeof(t) ``` Man verwendet Tupel gerne als Rückgabewerte von Funktionen, um mehr als ein Objekt zurückzulieferen. ```{julia} # Ganzzahldivision und Rest: # Quotient und Rest werden den Variablen q und r zugewiesen q, r = divrem(71, 6) @show q r; ``` Wie man hier sieht, kann man in bestimmten Konstrukten die Klammern auch weglassen. Dieses *implict tuple packing/unpacking* verwendet man auch gerne in Mehrfachzuweisungen: ```{julia} x, y, z = 12, 17, 203 ``` ```{julia} y ``` 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: ```{julia} x = (13,) # ein 1-Element-Tupel ``` Das Komma - und nicht die Klammern -- macht das Tupel. ```{julia} x= (13) # kein Tupel ``` ## Ranges Wir haben *range*-Objekte schon in numerischen `for`-Schleifen verwendet. ```{julia} r = 1:1000 typeof(r) ``` 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. ```{julia} (3:100)[20] # das zwanzigste Element ``` 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. ```{julia} @allocated [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] ``` ```{julia} @allocated 1:20 ``` Zum Umwandeln in einen 'richtigen' Vektor dient die Funktion `collect()`. ```{julia} collect(20:-3:1) ``` Recht nützlich, z.B. beim Vorbereiten von Daten zum Plotten, ist der *range*-Typ `LinRange`. ```{julia} LinRange(2, 50, 300) ``` `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. ## 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: ```{julia} # Einwohner 2020 in Millionen, Quelle: wikipedia EW = Dict("Berlin" => 3.66, "Hamburg" => 1.85, "München" => 1.49, "Köln" => 1.08) ``` ```{julia} typeof(EW) ``` und mit den _keys_ indizieren: ```{julia} EW["Berlin"] ``` Das Abfragen eines nicht existierenden _keys_ ist natürlich ein Fehler. ```{julia} EW["Leipzig"] ``` Man kann ja auch vorher mal anfragen... ```{julia} haskey(EW, "Leipzig") ``` ... oder die Funktion `get(dict, key, default)` benutzen, die bei nicht existierendem Key keinen Fehler wirft sondern das 3. Argument zurückgibt. ```{julia} @show get(EW, "Leipzig", -1) get(EW, "Berlin", -1); ``` Man kann sich auch alle `keys` und `values` als spezielle Container geben lassen. ```{julia} keys(EW) ``` ```{julia} values(EW) ``` Man kann über die `keys` iterieren... ```{julia} for i in keys(EW) n = EW[i] println("Die Stadt $i hat $n Millionen Einwohner.") end ``` odere gleich über `key-value`-Paare. ```{julia} for (stadt, ew) ∈ EW println("$stadt : $ew Mill.") end ``` ### Erweitern und Modifizieren Man kann in ein `Dict` zusätzliche `key-value`-Paare eintragen... ```{julia} EW["Leipzig"] = 0.52 EW["Dresden"] = 0.52 EW ``` und einen `value` ändern. ```{julia} # Oh, das war bei Leipzig die Zahl von 2010, nicht 2020 EW["Leipzig"] = 0.597 EW ``` Ein Paar kann über seinen `key` auch gelöscht werden. ```{julia} delete!(EW, "Dresden") ``` Zahlreiche Funktionen können mit `Dicts` wie mit anderen Containern arbeiten. ```{julia} maximum(values(EW)) ``` ### Anlegen eines leeren Dictionaries Ohne Typspezifikation ... ```{julia} d1 = Dict() ``` und mit Typspezifikation: ```{julia} d2 = Dict{String, Int}() ``` ### 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}}` ```{julia} collect(EW) ``` ```{julia} collect(keys(EW)), collect(values(EW)) ``` ### Geordnetes Iterieren über ein Dictionary Wir sortieren die Keys. Als Strings werden sie alphabetisch sortiert. Mit dem `rev`-Parameter wird rückwärts sortiert. ```{julia} for k in sort(collect(keys(EW)), rev = true) n = EW[k] println("$k hat $n Millionen Einw. ") end ``` Wir sortieren `collect(dict)`. Das ist ein Vektor von Paaren. Mit `by` definieren wir, wonach zu sortieren ist: nach dem 2. Element des Paares. ```{julia} for (k,v) in sort(collect(EW), by = pair -> last(pair), rev=false) println("$k hat $v Mill. EW") end ``` ### 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: ```{julia} l = rand(1:6, 100_000) .+ rand(1:6, 100_000) ``` 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`. ```{julia} # 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 ``` Das Ergebnis: ```{julia} 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](https://math.stackexchange.com/questions/1204396/why-is-the-sum-of-the-rolls-of-two-dices-a-binomial-distribution-what-is-define)