Im Konzept Vectors haben wir gesagt, dass „Arrays das Herzstück der Sprache Julia sind" und dass ein Vektor ein eindimensionales Array ist.
Vor diesem Hintergrund dürfen wir wohl zu Recht hoffen, dass die Sprache viele vielseitige und mächtige Möglichkeiten bietet, mit Vektoren Dinge zu tun, was auch immer das heißen mag.
Ein Hinweis zur Terminologie: Obwohl es in diesem Dokument viel um „Vektoren" geht, gilt vieles davon auch für jeden iterierbaren Typ: Ranges, Tupel, Mengen und einige andere.
Einige sehr einfache Funktionen nehmen einen Vektor als Eingabe und geben (bei eindimensionaler Eingabe) einen Skalar zurück.
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
Wenn wir beim Konzept zu mehrdimensionalen Arrays ankommen, wird klarer, dass es sich hier um Dimensionsreduktion handelt und nicht unbedingt um die Rückgabe eines Skalars. Wenn dir das nichts sagt, mach dir darüber erst einmal keine Gedanken.
Es gibt noch viele weitere Funktionen dieser Art.
Ein paar Beispiele findest du im Konzept Statistics.
Es gibt auch Funktionen, die auf mehrere Vektoren gleichzeitig wirken, zum Beispiel das (sehr nützliche) zip.
julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])
# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
(1, 'a', "I")
(2, 'b', "make")
(3, 'c', "tuples")
zip() nimmt eine beliebige Anzahl vektorähnlicher Eingaben und gibt einen Iterator von Tupeln zurück.
Die Eingaben haben normalerweise alle dieselbe Länge. Ist eine kürzer, werden die anderen auf die kürzeste Länge abgeschnitten: vielleicht genau das, was du beabsichtigt hast, aber häufiger ein Bug in deinem Code.
Angenommen, du hast einen numerischen Vektor und möchtest von jedem Wert 0,5 subtrahieren.
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)
Das schlägt fehl. Wie sieht es also aus, wenn wir einen anderen Vektor subtrahieren?
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
Das funktioniert, ist aber ziemlich mühsam und verbraucht viel Speicher, je länger die Vektoren werden.
Je nachdem, wie weit du im Lehrplan schon fortgeschritten bist, fallen dir wahrscheinlich noch andere Ansätze ein:
[x - 0.5 for x in v] liefert das gewünschte Ergebnis (Python-Stil).map(x -> x - 0.5, v) funktioniert ebenfalls (Haskell-Stil, aber in vielen Sprachen üblich).Zum Glück hat Julia einen „magischen" Punkt, mit dem sich dieses Problem ganz einfach lösen lässt: v .- 0.5 ist alles, was du brauchst.
Der nächste Abschnitt erklärt, warum.
v - 0.5 schlägt also fehl, aber v .- 0.5 funktioniert. Wir müssen verstehen, was der Punkt bewirkt.
Er bewirkt zwei Dinge, die zusammen das gewünschte Ergebnis liefern.
Erstens: Ein Punkt vor einem Infix-Operator bedeutet „wende diese Operation auf jedes Element einzeln an".
Ebenso „vektorisiert" ein Punkt nach einem Funktionsnamen die Funktion, selbst wenn sie für skalare Eingaben geschrieben wurde.
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
Nebenbei bemerkt sind Infix-Operatoren eigentlich nur syntaktischer Zucker für die zugrunde liegende Funktion.
Das bedeutet zum Beispiel, dass [1, 5, 10] .% 3 vom Interpreter zu mod.([1, 5, 10], 3) umgeschrieben wird und die mod.-Syntax dann ausgeführt wird (beide Varianten liefern [1, 2, 1]).
In einem vorherigen Beispiel haben wir gesehen, dass wir Vektoren gleicher Länge subtrahieren können. Bedenke aber bitte, dass .- ein sicherer Operator als - ist, weil er die elementweise Absicht deutlich macht.
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
Was ist mit Vektoren unterschiedlicher Länge?
julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size
julia> v .- [0.5,]
3-element Vector{Float64}:
0.7
1.0
1.2
Im Allgemeinen sind ungleiche Längen ein Fehler, außer wenn eine die Länge 1 hat (technisch gesehen eine „Singleton"-Dimension).
Singletons wie [0.5,] oder einfach 0.5 werden automatisch durch Wiederholung auf die nötige Länge erweitert.
Das ist das Herzstück des broadcasting.
Wer sich wegen dieser „Wiederholung" Sorgen um den Speicherverbrauch macht, kann aufatmen: Sie ist sehr effizient umgesetzt und kopiert die Werte nicht tatsächlich im Speicher.
Programmierer, die Broadcasting aus anderen Sprachen kennen, sollten wissen, dass Julias Ansatz (größtenteils) dem von NumPy ähnelt, aber deutlich weniger tolerant gegenüber Größenunterschieden ist als R.
Wenn der Speicherverbrauch ein Thema ist, sind Operationen an Ort und Stelle eine übliche Methode, um Allokationen zu reduzieren.
Die Broadcasting-Operationen in den obigen Beispielen erzeugen jedoch einen neuen Vector, anstatt das Original zu verändern.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
1
2
3
Um v an Ort und Stelle zu verändern, muss die Änderung per Broadcast erfolgen.
julia> v .= v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v .+= 1 # equivalent operation to above
3-element Vector{Int64}:
3
4
5
julia> v
3-element Vector{Int64}:
3
4
5
Aber Vorsicht! Der Punkt vor dem Zuweisungsoperator .= ist wichtig.
julia> v = [1, 2, 3];
julia> v = v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
2
3
4
Das scheint auf die gleiche Weise funktioniert zu haben, aber hier hat v .+ 1 einen neuen Vektor [2, 3, 4] erzeugt und ihn dann der Variable v zugewiesen. Der ursprüngliche Vektor [1, 2, 3] bleibt im Speicher und wird von der Garbage Collection abgeräumt.
Dadurch wird am Ende doppelt so viel Speicher verwendet wie im vorherigen Beispiel, das den für den ursprünglichen Vektor reservierten Speicher wiederverwendet.
Ebenso gibt es verwandte Feinheiten beim Broadcasting mit Vektoren gleicher Größe.
Gegeben seien zum Beispiel zwei Vektoren v und w derselben Größe:
v .= w erzeugt eine copy von w an der Speicherstelle von v. Weitere Änderungen an v wirken sich nicht auf w aus und umgekehrt.v = w erzeugt einen weiteren Zeiger auf die Speicherstelle von w unter dem Namen v. Weitere Änderungen an v zeigen sich auch in w und umgekehrt.Dieser Unterabschnitt ist ziemlich mathelastig, daher wird nicht erwartet, dass die meisten Lernenden ihn wirklich verstehen. Er ist aber eine nützliche Warnung, die beim Debuggen helfen kann, wenn du unerwartete Fehlermeldungen siehst.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
julia> u * v
1-element Vector{Int64}:
14
julia> v * u
3×3 Matrix{Int64}:
1 2 3
2 4 6
3 6 9
Wenn du zufällig aus der linearen Algebra kommst, dann gilt: (1) Du bist keine typische Nutzerin bzw. kein typischer Nutzer von Exercism (aber hier sehr willkommen!) und (2) du erkennst vielleicht, dass v ein Spaltenvektor ist, u ein Zeilenvektor, u * v das innere Produkt und v * u das äußere Produkt.
Julia folgt den Regeln der Mathematik, hier wie überall.
Für alle anderen: Verstehe bitte einfach, warum wir empfehlen, für elementweise Berechnungen immer Operatoren mit Punkt zu verwenden: v .* v funktioniert genau so, wie du es erwarten würdest, und ergibt [1, 4, 9].
Wie du Elemente eines Vektors über die Indexnummer auswählst, war schon in früheren Konzepten Thema.
a = collect('A':'Z') # => 26-element Vector{Char}
# index with an integer
a[2] # => 'B'
# index with a range
a[12:2:18] # => ['L', 'N', 'P, 'R']
# index with another vector
a[ [1, 3, 5] ] # => ['A', 'C', 'E']
Es ist auch möglich, Elemente auszuwählen, die einen bestimmten logischen Ausdruck erfüllen (technisch gesehen ein „Prädikat"). Das erfordert normalerweise Broadcasting.
julia> a[a .< 'D']
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
Bei komplexeren Ausdrücken neigen die Punkte dazu, sich zu häufen (aber sie sind klein und leicht zu tippen).
julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)
Zur Erinnerung: Der „Vektor" kann tatsächlich jedes passende geordnete iterierbare Objekt sein, zum Beispiel ein Range:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
Intern wird das Prädikat in einen BitVector umgewandelt, der dann als Index verwendet wird.
julia> condition = a .< 'D'
26-element BitVector:
1
1
1
0
# display truncated
julia> a[condition]
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)