Percursos
/
Julia
Julia
/
Programa
/
Operações com vetores
Op

Operações com vetores em Julia

2 exercícios

Sobre Operações com vetores

No conceito Vectors, dissemos que «os arrays estão no coração da linguagem Julia» e que um vetor é um array unidimensional.

Assim sendo, é razoável esperar que a linguagem ofereça muitas formas versáteis e poderosas de fazer coisas com vetores, seja lá o que isso signifique.

Uma nota sobre terminologia: embora este documento fale muito de «vetores», grande parte do que aqui está também se aplica a qualquer tipo iterável: intervalos, tuplos, conjuntos e vários outros.

Funções que esperam um vetor como parâmetro de entrada

Algumas funções muito simples recebem um vetor como parâmetro de entrada e, no caso de um parâmetro de entrada unidimensional, devolvem um valor escalar.

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

Quando chegarmos ao conceito de arrays multidimensionais, ficará mais claro que isto é uma redução de dimensão e não necessariamente o retorno de um escalar. Se isso não fizer sentido para ti, por agora não te preocupes com isso.

Há muitas mais funções deste tipo. Vê o conceito Statistics para encontrares alguns exemplos.

Há também funções que operam sobre vários vetores, como a (muito útil) 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() recebe um número arbitrário de parâmetros de entrada do tipo vetor e devolve um iterador de tuplos.

Normalmente, os parâmetros de entrada têm todos o mesmo comprimento. Se um for mais curto, os outros são truncados até ao comprimento do mais curto: talvez o que pretendias, mas mais frequentemente um bug no teu código.

Aritmética

Suponhamos que tens um vetor numérico e queres subtrair 0,5 a cada valor.

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)

Isso falha, então e se subtraírmos outro vetor?

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

Funciona, mas torna-se bastante fastidioso e consome muita memória à medida que os vetores ficam maiores.

Dependendo de até onde já chegaste no programa, provavelmente consegues pensar noutras abordagens:

  • Escrever um ciclo, embora isso fosse verboso e desajeitado.
  • Usar uma compreensão: [x - 0.5 for x in v] dá o resultado pretendido (ao estilo de Python).
  • Usar uma função de ordem superior: map(x -> x - 0.5, v) também funciona (ao estilo de Haskell, embora seja comum em muitas linguagens).

Felizmente, a Julia tem um ponto «mágico» que resolve este problema de forma muito simples: v .- 0.5 é tudo o que precisas.

A próxima secção explica porquê.

Broadcasting

Então, v - 0.5 falha, mas v .- 0.5 funciona, e precisamos de perceber o que o ponto está a fazer.

Duas coisas, que se combinam para dar o resultado pretendido.

1) Aplicação elemento a elemento

Em primeiro lugar, acrescentar um ponto antes de qualquer operador infixo significa «aplicar esta operação a cada elemento separadamente».

Da mesma forma, acrescentar um ponto depois do nome de uma função «vetoriza-a», mesmo que a função tenha sido escrita para parâmetros de entrada escalares.

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

A propósito, os operadores infixos não passam de açúcar sintático para a função subjacente.

Isto significa que, por exemplo, o intérprete traduz [1, 5, 10] .% 3 para mod.([1, 5, 10], 3), e a sintaxe mod. é depois executada (ambas as versões devolvem [1, 2, 1]).

2) Expansão de singleton

Vimos num exemplo anterior que podemos subtrair vetores do mesmo comprimento, mas tem em atenção que .- é um operador mais seguro do que -, porque deixa clara a intenção de operar elemento a elemento.

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

E quanto a vetores de comprimentos diferentes?

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

Em geral, comprimentos diferentes são um erro, exceto quando um tem comprimento 1 (tecnicamente, uma dimensão «singleton»).

Os singletons como [0.5,] ou apenas 0.5 são automaticamente expandidos até ao comprimento necessário, por repetição. É isto que está no coração do broadcasting.

Quem estiver preocupado com o consumo de memória causado por esta «repetição» pode ficar descansado: está implementada de forma muito eficiente e não copia realmente os valores na memória.

Os programadores que já conhecem o broadcasting de outras linguagens devem ter em conta que a abordagem da Julia é (na sua maioria) semelhante à do NumPy, mas muito menos tolerante a diferenças de tamanho do que a do R.

Broadcasting no local

Se o consumo de memória for uma preocupação, as operações no local são uma forma comum de tentar reduzir as alocações. No entanto, as operações de broadcasting dos exemplos acima criam um novo Vector em vez de modificarem o original.

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

Para modificar v no local, é preciso fazer a modificação com 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

Mas tem cuidado! O ponto antes do operador de atribuição .= é 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

Parece ter funcionado da mesma forma, mas aqui v .+ 1 criou um novo vetor [2, 3, 4] e depois atribuiu-o à variável v, deixando o vetor inicial [1, 2, 3] na memória à espera de ser recolhido. No final, isto usa o dobro da memória do exemplo anterior, que reutiliza a memória alocada para o vetor inicial.

Do mesmo modo, há subtilezas relacionadas quando se faz broadcasting com vetores do mesmo tamanho. Por exemplo, dados dois vetores v e w do mesmo tamanho:

  • v .= w produz uma copy de w no local de memória de v. Alterações posteriores em v não afetam w, e vice-versa.
  • v = w cria outro ponteiro para o local de memória de w, com o nome v. Alterações posteriores em v refletem-se em w, e vice-versa.

Operadores sem ponto: uma lição de cautela

Esta subsecção é bastante matemática, por isso não se espera que a maioria dos estudantes a compreenda realmente. No entanto, é um aviso útil que pode ajudar na depuração quando vires mensagens de erro inesperadas.

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 por acaso tens formação em álgebra linear, então (1) não és um utilizador típico do Exercism (mas és muito bem-vindo aqui!) e (2) podes reconhecer que v é um vetor coluna, u é um vetor linha, u * v é o produto interno e v * u é o produto externo. A Julia segue as regras da matemática, nisto como em tudo.

Para todos os outros: procura apenas perceber porque recomendamos que uses sempre operadores com ponto para cálculos elemento a elemento: v .* v funciona exatamente como esperarias, dando [1, 4, 9].

Indexação

A seleção de elementos de um vetor pelo número do índice já foi abordada em conceitos anteriores.

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

Indexação lógica

Também é possível selecionar elementos que satisfazem uma determinada expressão lógica (tecnicamente, um «predicado»). Isto normalmente requer 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)

Em expressões mais complexas, os pontos tendem a multiplicar-se (mas são pequenos e fáceis de escrever).

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)

Um lembrete de que o «vetor» pode, na verdade, ser qualquer iterável ordenado adequado, como um intervalo:

julia> n = 3:10
3:10

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

Internamente, o predicado é convertido num BitVector, que é depois usado como índice.

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)
Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Operações com vetores