Files
JuliaKurs23/nb/6_ArraysEtcP1.ipynb
T
2024-04-08 14:20:54 +02:00

17 KiB

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:
for x  container ... end
  • indizierbar: Man kann auf Elemente über ihren Index zugreifen:
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.

In [ ]:
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:

In [ ]:
typeof(t)

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

In [ ]:
# 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:

In [ ]:
x, y, z = 12, 17, 203
In [ ]:
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:

In [ ]:
x = (13,)         # ein 1-Element-Tupel

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

In [ ]:
x= (13)         # kein Tupel

Ranges

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

In [ ]:
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.

In [ ]:
(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.

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

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

In [ ]:
collect(20:-3:1)

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

In [ ]:
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:

In [ ]:
# Einwohner 2020 in Millionen, Quelle: wikipedia

EW = Dict("Berlin" => 3.66,  "Hamburg" => 1.85, 
          "München" => 1.49, "Köln" => 1.08)
In [ ]:
typeof(EW)

und mit den keys indizieren:

In [ ]:
EW["Berlin"]

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

In [ ]:
EW["Leipzig"]

Man kann ja auch vorher mal anfragen...

In [ ]:
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.

In [ ]:
@show get(EW, "Leipzig", -1)   get(EW, "Berlin", -1);

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

In [ ]:
keys(EW)
In [ ]:
values(EW)

Man kann über die keys iterieren...

In [ ]:
for i in keys(EW)
    n = EW[i]
    println("Die Stadt $i hat $n Millionen Einwohner.")
end

odere gleich über key-value-Paare.

In [ ]:
for (stadt, ew)  EW
    println("$stadt : $ew  Mill.")
end

Erweitern und Modifizieren

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

In [ ]:
EW["Leipzig"] = 0.52
EW["Dresden"] = 0.52 
EW

und einen value ändern.

In [ ]:
# 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.

In [ ]:
delete!(EW, "Dresden")

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

In [ ]:
maximum(values(EW))

Anlegen eines leeren Dictionaries

Ohne Typspezifikation ...

In [ ]:
d1 = Dict()

und mit Typspezifikation:

In [ ]:
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}}
In [ ]:
collect(EW)
In [ ]:
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.

In [ ]:
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.

In [ ]:
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:

In [ ]:
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.

In [ ]:
# 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:

In [ ]:
using Plots

plot(collect(keys(d)), collect(values(d)), seriestype=:scatter)