Tracce
/
Julia
Julia
/
Programma
/
Operazioni sui vettori
Op

Operazioni sui vettori in Julia

2 esercizi

Informazioni su Operazioni sui vettori

Nel concetto Vectors, abbiamo detto che «gli array sono al cuore del linguaggio Julia» e che un vettore è un array monodimensionale.

Detto questo, possiamo ragionevolmente sperare che il linguaggio offra molti modi versatili e potenti per fare cose con i vettori, qualunque cosa significhi.

Una nota sulla terminologia: anche se questo documento parla molto di «vettori», buona parte di ciò che dice vale anche per qualsiasi tipo iterabile: intervalli, tuple, insiemi e vari altri.

Funzioni che si aspettano un vettore in input

Alcune funzioni molto semplici prendono in input un vettore e (per input monodimensionali) restituiscono un output scalare.

v = [2, 3, 4]
length(v)  # => 3
sum(v)  # => 9

Quando arriveremo al concetto sugli array multidimensionali, diventerà più chiaro che si tratta di riduzione di dimensionalità piuttosto che di un ritorno a uno scalare. Se per te non ha senso, per ora non preoccupartene.

Ci sono molte altre funzioni di questo tipo. Dai un'occhiata al concetto Statistics per qualche esempio.

Ci sono anche funzioni che operano su più vettori, come la (utilissima) 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() accetta un numero arbitrario di input di tipo vettoriale e restituisce un iteratore di tuple.

Di solito gli input hanno tutti la stessa lunghezza. Se uno è più corto, gli altri vengono troncati alla lunghezza più breve: forse è quello che volevi, ma più spesso è un bug nel codice che hai scritto.

Aritmetica

Supponi di avere un vettore numerico e di voler sottrarre 0.5 da ogni valore.

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)

Questo non funziona, quindi che ne dici di sottrarre un altro vettore?

julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

Funziona, ma è piuttosto noioso e avido di memoria man mano che i vettori diventano più lunghi.

A seconda di quanto sei andato avanti nel programma, probabilmente ti verranno in mente altri approcci:

  • Scrivi un ciclo, anche se sarebbe verboso e goffo.
  • Usa una comprensione: [x - 0.5 for x in v] dà il risultato desiderato (in stile Python).
  • Usa una funzione di ordine superiore: funziona anche map(x -> x - 0.5, v) (in stile Haskell, ma comune in molti linguaggi).

Per fortuna, Julia ha un punto «magico» che risolve il problema in modo molto semplice: ti basta v .- 0.5.

La prossima sezione spiega perché.

Broadcasting

Quindi v - 0.5 fallisce, ma v .- 0.5 funziona, e dobbiamo capire cosa fa il punto.

Due cose, che combinate danno il risultato desiderato.

1) Applicazione elemento per elemento

Primo, aggiungere un punto prima di qualsiasi operatore infisso significa «applicare questa operazione a ciascun elemento separatamente».

Allo stesso modo, aggiungere un punto dopo il nome di una funzione la «vettorizza», anche se la funzione è stata scritta per input scalari.

julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

Per inciso, gli operatori infissi non sono altro che zucchero sintattico per la funzione sottostante.

Questo significa che, per esempio, l'interprete traduce [1, 5, 10] .% 3 in mod.([1, 5, 10], 3), e poi viene eseguita la sintassi mod. (entrambe le versioni restituiscono [1, 2, 1]).

2) Espansione dei singleton

Come abbiamo visto in un esempio precedente, possiamo sottrarre vettori di uguale lunghezza; tieni però presente che .- è un operatore più sicuro di -, perché rende esplicita l'intenzione di operare elemento per elemento.

julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

E che dire dei vettori di lunghezza diversa?

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

In generale, lunghezze diverse sono un errore, tranne quando una ha lunghezza 1 (tecnicamente, una dimensione «singleton»).

I singleton come [0.5,] o semplicemente 0.5 vengono espansi automaticamente alla lunghezza necessaria, ripetendoli. È questo il cuore del broadcasting.

Chi si preoccupa dell'uso di memoria causato da questa «ripetizione» può stare tranquillo: è implementata in modo molto efficiente e di fatto non copia i valori in memoria.

Chi ha familiarità con il broadcasting in altri linguaggi noterà che l'approccio di Julia è (per lo più) simile a quello di NumPy, ma molto meno tollerante di R verso le dimensioni non corrispondenti.

Broadcasting in-place

Se l'uso della memoria è un problema, le operazioni in-place sono un modo comune per cercare di ridurre le allocazioni. Tuttavia, le operazioni di broadcasting negli esempi precedenti creano un nuovo Vector invece di modificare l'originale.

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

Per modificare v in-place, la modifica deve essere eseguita tramite broadcasting.

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

Ma attenzione! Il punto prima dell'operatore di assegnazione .= è importante.

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

Sembra che abbia funzionato allo stesso modo, ma qui v .+ 1 ha creato un nuovo vettore [2, 3, 4] e poi lo ha assegnato alla variabile v, lasciando in memoria il vettore iniziale [1, 2, 3], che verrà poi eliminato dal garbage collector. Il risultato è che viene usata il doppio della memoria rispetto all'esempio precedente, che riutilizza la memoria allocata per il vettore iniziale.

Allo stesso modo, ci sono sottigliezze correlate quando si fa broadcasting con vettori della stessa dimensione. Per esempio, dati due vettori v e w della stessa dimensione:

  • v .= w produce una copy di w nella posizione di memoria di v. Ulteriori modifiche a v non influiscono su w e viceversa.
  • v = w crea un altro puntatore alla posizione di memoria di w, con il nome v. Ulteriori modifiche a v si riflettono su w e viceversa.

Operatori senza punto: un racconto ammonitore

Questa sottosezione è piuttosto densa di matematica, quindi non ci si aspetta che la maggior parte degli studenti la capisca davvero. Tuttavia, è un avvertimento utile che può aiutare nel debug quando vedi messaggi di errore inattesi.

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

Se per caso hai una formazione in algebra lineare, allora (1) non sei un utente tipico di Exercism (ma qui sei il benvenuto!) e (2) potresti riconoscere che v è un vettore colonna, u è un vettore riga, u * v è il prodotto interno e v * u è il prodotto esterno. Julia segue le regole della matematica, in questo come in ogni altra cosa.

Per tutti gli altri: cerca solo di capire perché consigliamo di usare sempre operatori con il punto per i calcoli elemento per elemento: v .* v funziona esattamente come ti aspetti, e dà [1, 4, 9].

Indicizzazione

La selezione di elementi di un vettore tramite il numero di indice è stata trattata nei concetti precedenti.

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']

Indicizzazione logica

È anche possibile selezionare gli elementi che soddisfano una certa espressione logica (tecnicamente, un «predicato»). Questo di solito richiede il 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)

Per espressioni più complesse i punti tendono a moltiplicarsi (ma sono piccoli e facili da digitare).

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)

Un promemoria: il «vettore» può in realtà essere qualsiasi iterabile ordinato adatto, come un intervallo:

julia> n = 3:10
3:10

julia> n[isodd.(n)]
4-element Vector{Int64}:
 3
 5
 7
 9

Internamente, il predicato viene convertito in un BitVector che poi viene usato come indice.

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)
Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Operazioni sui vettori