Trilhas
/
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 "arrays estão no coração da linguagem Julia" e que um vetor é um array unidimensional.

Diante disso, é 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 observação sobre terminologia: embora este documento fale muito de "vetores", boa parte dele também vale para qualquer tipo iterável: intervalos, tuplas, conjuntos e vários outros.

Funções que esperam um vetor como entrada

Algumas funções bem simples recebem um vetor como entrada e (para entradas unidimensionais) retornam uma saída escalar.

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

Quando chegarmos ao Conceito sobre arrays multidimensionais, vai ficar mais claro que isso é redução de dimensão, e não necessariamente o retorno de um escalar. Se isso não fizer sentido para você, não se preocupe com isso por enquanto.

Existem muitas outras funções desse tipo. Veja o Conceito Statistics para alguns exemplos.

Também existem 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 entradas do tipo vetor e retorna um iterador de tuplas.

Normalmente, todas as entradas têm o mesmo comprimento. Se uma delas for mais curta, as outras são truncadas para o comprimento da mais curta: talvez seja o que você queria, mas, mais comumente, é um bug no seu código.

Aritmética

Suponha que você tenha um vetor numérico e queira subtrair 0.5 de 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 fizermos a subtração com outro vetor?

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

Funciona, mas é bem tedioso e consome muita memória conforme os vetores ficam maiores.

Dependendo de até onde você chegou no programa de estudos, você provavelmente consegue pensar em outras abordagens:

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

Felizmente, Julia tem um ponto "mágico" que resolve esse problema de forma bem simples: v .- 0.5 é tudo o que você precisa.

A próxima seção explica por quê.

Broadcasting

Então, v - 0.5 falha, mas v .- 0.5 funciona, e precisamos entender o que esse ponto está fazendo.

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

1) Aplicação elemento a elemento

Primeiro, adicionar um ponto antes de qualquer operador infixo significa "aplicar essa operação a cada elemento separadamente".

Do mesmo modo, adicionar um ponto depois do nome de uma função a "vetoriza", mesmo que a função tenha sido escrita para entradas escalares.

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

Como um parêntese, operadores infixos são, na verdade, apenas açúcar sintático para a função subjacente.

Isso significa que, por exemplo, o intérprete traduz [1, 5, 10] .% 3 para mod.([1, 5, 10], 3), e então a sintaxe mod. é executada (as duas versões retornam [1, 2, 1]).

2) Expansão de singleton

Vimos em um exemplo anterior que podemos subtrair vetores de mesmo comprimento, mas entenda 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 no caso de 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 geram um erro, exceto quando um deles tem comprimento 1 (tecnicamente, uma dimensão "singleton").

Singletons como [0.5,] ou simplesmente 0.5 são expandidos automaticamente até o comprimento necessário por meio de repetição. É nisso que consiste o broadcasting.

Quem estiver preocupado com o uso de memória por causa dessa "repetição" pode ficar tranquilo: ela é implementada de uma forma muito eficiente que, na prática, não copia os valores na memória.

Quem já conhece broadcasting em outras linguagens deve notar que a abordagem de Julia é (em grande parte) parecida com a do NumPy, mas bem menos tolerante a incompatibilidades de tamanho do que a do R.

Broadcasting in-place

Se o uso de memória é uma preocupação, as operações in-place são uma forma comum de tentar reduzir alocações. No entanto, as operações de broadcasting dos exemplos acima criam um novo Vector em vez de modificar 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 in-place, a modificação precisa ser aplicada 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 atenção! O ponto antes do operador de atribuição .= faz diferença.

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 que funcionou do mesmo jeito, mas aqui v .+ 1 criou um novo vetor [2, 3, 4] e depois o atribuiu à variável v, deixando o vetor inicial [1, 2, 3] na memória para ser coletado pelo garbage collector. No fim, isso usa o dobro de memória do que o exemplo anterior, que reaproveita a memória alocada para o vetor inicial.

Da mesma forma, há sutilezas parecidas ao fazer 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. Mudanças posteriores em v não afetam w, e vice-versa.
  • v = w produz outro ponteiro para o local de memória de w, com o nome v. Mudanças posteriores em v se refletem em w, e vice-versa.

Operadores sem ponto: uma história de advertência

Esta subseção é bastante pesada em matemática, então não se espera que a maioria dos estudantes entenda tudo de verdade. Ainda assim, é um aviso útil que pode ajudar a depurar quando você vir 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 você por acaso tem formação em álgebra linear, então (1) você não é um usuário típico do Exercism (mas você é muito bem-vindo aqui!) e (2) talvez reconheça que v é um vetor coluna, u é um vetor linha, u * v é o produto interno e v * u é o produto externo. Julia segue as regras da matemática, nisto como em tudo.

Para todos os outros: entenda apenas por que recomendamos que você use sempre operadores com ponto para cálculos elemento a elemento: v .* v funciona exatamente como você esperaria, dando [1, 4, 9].

Indexação

A seleção de elementos de um vetor pelo número do índice já foi discutida 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 alguma expressão lógica (tecnicamente, um "predicado"). Isso geralmente exige 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 se multiplicar (mas são pequenos e fáceis de digitar).

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)

Lembrete: 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 em um BitVector, que então é 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 O link abre em uma nova janela ou aba

Aprenda Operações com vetores