3.2 Estruturas Nativas de Dados

Julia possui diversas estruturas de dados nativas. Elas são abstrações de dados que representam alguma forma de dado estruturado. Vamos cobrir os mais usados. Eles contém dados homogêneos ou heterogêneos. Uma vez que são coleções, podemos iterar sobre eles com os laços for.

Nós cobriremos String, Tuple, NamedTuple, UnitRange, Arrays, Pair, Dict, Symbol.

Quando você se depara com uma estrutura de dados em Julia, você pode encontrar métodos que a aceitam como um argumento por meio da função methodswith. Em Julia, a distinção entre métodos e funções é a seguinte: Cada função pode ter mútiplos métodos, como mostramos anteriormente. A função methodswith é boa de se ter por perto. Vejamos o que podemos fazer com uma String, por exemplo:

first(methodswith(String), 5)
[1] write(iod::HTTP.DebugRequest.IODebug, x::String) in HTTP.DebugRequest at /home/runner/.julia/packages/HTTP/aTjcj/src/IODebug.jl:38
[2] write(fp::FilePathsBase.SystemPath, x::Union{String, Vector{UInt8}}) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/qgXdE/src/system.jl:380
[3] write(fp::FilePathsBase.SystemPath, x::Union{String, Vector{UInt8}}, mode) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/qgXdE/src/system.jl:380
[4] write(buffer::FilePathsBase.FileBuffer, x::String) in FilePathsBase at /home/runner/.julia/packages/FilePathsBase/qgXdE/src/buffer.jl:84
[5] write(io::IO, s::Union{SubString{String}, String}) in Base at strings/io.jl:244

3.2.1 Fazendo Broadcasting de Operadores e Funções

Antes de mergulharmos nas estruturas de dados, precisamos conversar sobre broadcasting (também conhecido como vetorização) e o operador “dot” ..

Podemos vetorizar operações matemáticas como * (multiplicação) ou + (adição) usando o operador dot. Por exemplo, vetorizar adição implica em mudar + para .+:

[1, 2, 3] .+ 1
[2, 3, 4]

Também funciona automaticamente com funções. (Tecnicamente, as operações matemáticas, ou operadores infixos, também são funções, mas isso não é tão importante saber.) Lembra da nossa função logarithm?

logarithm.([1, 2, 3])
[0.0, 0.6931471805599569, 1.0986122886681282]

3.2.1.1 Funções com exclamação !

É uma convenção de Julia acrescentar uma exclamação ! a nomes de funções que modificam um ou mais de seus argumentos. Esta convenção avisa o usuário que a função não é pura, ou seja, que tem efeitos colaterais. Uma função com efeitos colaterais é útil quando você deseja atualizar uma grande estrutura de dados ou coleção de variáveis sem ter toda a sobrecarga da criação de uma nova instância.

Por exemplo, podemos criar uma função que adiciona 1 a cada elemento de um vetor V:

function add_one!(V)
    for i in 1:length(V)
        V[i] += 1
    end
    return nothing
end
my_data = [1, 2, 3]

add_one!(my_data)

my_data
[2, 3, 4]

3.2.2 String

Strings são representadas delimitadas por aspas duplas:

typeof("This is a string")
String

Também podemos escrever uma string multilinha:

text = "
This is a big multiline string.
As you can see.
It is still a String to Julia.
"

This is a big multiline string.
As you can see.
It is still a String to Julia.

Mas, geralmente, é mais claro usar aspas triplas:

s = """
    This is a big multiline string with a nested "quotation".
    As you can see.
    It is still a String to Julia.
    """
This is a big multiline string with a nested "quotation".
As you can see.
It is still a String to Julia.

Ao usar crases triplas, a tabulação e o marcador de nova linha no início são ignorados por Julia. Isso melhora a legibilidade do código porque você pode indentar o bloco em seu código-fonte sem que esses espaços acabem em sua string.

3.2.2.1 Concatenação de Strings

Uma operação comum de string é a concatenação de string. Suponha que você queira construir uma nova string que é a concatenação de duas ou mais strings. Isso é realizado em Julia com o operador * ou a função join. Este símbolo pode soar como uma escolha estranha e realmente é. Por enquanto, muitas bases de código em Julia estão usando este símbolo, então ele permanecerá na linguagem. Se você estiver interessado, pode ler uma discussão de 2015 sobre isso em https://github.com/JuliaLang/julia/issues/11030.

hello = "Hello"
goodbye = "Goodbye"

hello * goodbye
HelloGoodbye

Como você pode ver, está faltando um espaço entre hello e goodbye. Poderíamos concatenar uma string adicional " " com *, mas isso seria complicado para mais de duas strings. É onde a função join vem a calhar. Nós apenas passamos como argumentos as strings dentro dos colchetes [] e, em seguida, o separador:

join([hello, goodbye], " ")
Hello Goodbye

3.2.2.2 Interpolação de String

Concatenar strings pode ser complicado. Podemos ser muito mais expressivos com interpolação de string. Funciona assim: você especifica o que quer que seja incluído em sua string com o cifrão $. Aqui está o exemplo anterior, mas agora usando interpolação:

"$hello $goodbye"
Hello Goodbye

Isso funciona mesmo dentro de funções. Vamos revisitar nossa função test que foi definida em Section 3.1.5:

function test_interpolated(a, b)
    if a < b
        "$a is less than $b"
    elseif a > b
        "$a is greater than $b"
    else
        "$a is equal to $b"
    end
end

test_interpolated(3.14, 3.14)
3.14 is equal to 3.14

3.2.2.3 Manipulações de Strings

Existem várias funções para manipular strings em Julia. Vamos demonstrar as mais comuns. Além disso, observe que a maioria dessas funções aceita uma Expressão Regular (RegEx) como argumentos. Não cobriremos RegEx neste livro, mas te encorajamos a aprender sobre elas, especialmente se a maior parte de seu trabalho usa dados textuais.

Primeiro, vamos definir uma string para brincarmos:

julia_string = "Julia is an amazing opensource programming language"
Julia is an amazing opensource programming language
  1. occursin, startswith e endswith: São condicionais (retornam true ou false) se o primeiro argumento é um:

    • substring do segundo argumento

      occursin("Julia", julia_string)
      true
    • prefixo do segundo argumento

      startswith("Julia", julia_string)
      false
    • sufixo do segundo argumento

      endswith("Julia", julia_string)
      false
  2. lowercase, uppercase, titlecase e lowercasefirst:

    lowercase(julia_string)
    julia is an amazing opensource programming language
    uppercase(julia_string)
    JULIA IS AN AMAZING OPENSOURCE PROGRAMMING LANGUAGE
    titlecase(julia_string)
    Julia Is An Amazing Opensource Programming Language
    lowercasefirst(julia_string)
    julia is an amazing opensource programming language
  3. replace: introduz uma nova sintaxe, chamada de Pair

    replace(julia_string, "amazing" => "awesome")
    Julia is an awesome opensource programming language
  4. split: fatia uma string por um delimitador:

    split(julia_string, " ")
    SubString{String}["Julia", "is", "an", "amazing", "opensource", "programming", "language"]

3.2.2.4 Convertendo em/parseando Strings

Muitas vezes, precisamos converter tipos de variáveis em Julia. Para converter um número em uma string, podemos usar a função string:

my_number = 123
typeof(string(my_number))
String

Às vezes, queremos o oposto: converter uma string em um número (ou, como se diz no jargão, parsear essa string). Julia tem uma função útil para isso: parse.

typeof(parse(Int64, "123"))
Int64

Às vezes, queremos jogar pelo seguro com essas conversões. É aí que entra a função tryparse. Tem a mesma funcionalidade que parse mas retorna um valor do tipo solicitado ou nothing. Isso faz com que a tryparse seja útil quando buscamos evitar erros. Claro, você precisará lidar com todos aqueles valores nothing depois.

tryparse(Int64, "A very non-numeric string")
nothing

3.2.3 Tupla

Julia tem uma estrutura de dados chamada tupla. Ela é muito especial em Julia porque ela é frequentemente usada em relação às funções. Uma vez que as funções são um recurso importante em Julia, todo usuário precisa saber o básico das tuplas.

Uma tupla é um contâiner de tamanho fixo que pode conter vários tipos diferentes. Uma tupla é um objeto imutável, o que significa que não pode ser modificado após a instanciação. Para construir uma tupla, use parênteses () para delimitar o início e o fim, junto com vírgulas , como delimitadores entre valores:

my_tuple = (1, 3.14, "Julia")
(1, 3.14, "Julia")

Aqui, estamos criando uma tupla com três valores. Cada um dos valores é um tipo diferente. Podemos acessá-los por meio de indexação. Assim:

my_tuple[2]
3.14

Também podemos iterar sobre tuplas com a palavra-chave for. E até mesmo aplicar funções sobre tuplas. Mas nós nunca podemos mudar qualquer valor de uma tupla já que elas são imutáveis.

Você se lembra funções que retornam vários valores em Section 3.1.4.2? Vamos inspecionar o que nossa função add_multiply retorna:

return_multiple = add_multiply(1, 2)
typeof(return_multiple)
Tuple{Int64, Int64}

Isso ocorre porque return a, b é o mesmo que return (a, b):

1, 2
(1, 2)

Agora você pode ver porque tuplas e funções são frequentemente relacionadas.

Mais uma coisa para pensarmos sobre as tuplas. Quando você deseja passar mais de uma variável para uma função anônima, adivinhe o que você precisa usar? Tuplas!

map((x, y) -> x^y, 2, 3)
8

Ou ainda, mais do que dois argumentos:

map((x, y, z) -> x^y + z, 2, 3, 1)
9

3.2.4 Tupla Nomeada

Às vezes, você deseja nomear os valores contidos nas tuplas. É aí que entram as tuplas nomeadas. Sua funcionalidade é praticamente a mesma das tuplas: são imutáveis e podem conter todo tipo de valor.

A construção das tuplas nomeadas é ligeiramente diferente das tuplas. Você tem os familiares parênteses () e a vírgula , separadora de valor. Mas agora, você nomeia os valores:

my_namedtuple = (i=1, f=3.14, s="Julia")
(i = 1, f = 3.14, s = "Julia")

Podemos acessar os valores de uma tupla nomeada por meio da indexação como em tuplas regulares ou, alternativamente, acessá-los por seus nomes com o .:

my_namedtuple.s
Julia

Encerrando nossa discussão sobre tuplas nomeadas, há uma sintaxe rápida importante que você verá muito no código de Julia. Frequentemente, os usuários de Julia criam uma tupla nomeada usando o parêntese familiar () e vírgulas ,, mas sem nomear os valores. Para fazer isso, comece a construção da tupla nomeada especificando primeiro um ponto e vírgula ; antes dos valores. Isto é especialmente útil quando os valores que iriam compor a tupla nomeada já estão definidos em variáveis ou quando você deseja evitar linhas longas:

i = 1
f = 3.14
s = "Julia"

my_quick_namedtuple = (; i, f, s)
(i = 1, f = 3.14, s = "Julia")

3.2.5 Ranges

Uma range em Julia representa um intervalo entre os limites de início e parada. A sintaxe é start:stop:

1:10
1:10

Como você pode ver, nosso range instanciado é do tipo UnitRange{T} onde T é o tipo de dados contido dentro de UnitRange:

typeof(1:10)
UnitRange{Int64}

E, se recolhermos todos os valores, temos:

[x for x in 1:10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Também podemos construir ranges para outros tipos:

typeof(1.0:10.0)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}

Às vezes, queremos mudar o comportamento padrão do incremento do intervalo. Podemos fazer isso adicionando um incremento específico por meio da sintaxe da range start:step:stop. Por exemplo, suponha que queremos um range de Float64 que vá de 0 a 1 com passos do tamanho de 0.2:

0.0:0.2:1.0
0.0:0.2:1.0

Se você quer “materializar” a range, transformando-a em uma coleção, você pode usar a função collect:

collect(1:10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Assim, temos uma array do tipo especificado no range entre os limites que definimos. Já que estamos falando de arrays, vamos conversar sobre eles.

3.2.6 Array

Na sua forma mais básica, arrays contém múltiplos objetos. Por exemplo, elas podem armazenar múltiplos números em uma dimensão:

myarray = [1, 2, 3]
[1, 2, 3]

Na maioria das vezes você quer ter arrays de tipo único para evitar problemas de performance, mas observe que elas também podem conter objetos de diferentes tipos:

myarray = ["text", 1, :symbol]
Any["text", 1, :symbol]

Elas são o “pão com manteiga” da ciência de dados, porque as arrays são o que está por trás da maior parte do fluxo de trabalho em manipulação de dados e visualização de dados.

Portanto, arrays são uma estrutura de dados essencial.

3.2.6.1 Tipos de array

Vamos começar com os tipos de arrays. Existem vários, mas vamos nos concentrar nos dois mais usados em ciência de dados:

Observe aqui que T é o tipo da array subjacente. Então, por exemplo, Vector{Int64} é um Vector no qual todos os elementos são Int64, e Matrix{AbstractFloat} é uma Matrix em que todos os elementos são subtipos de AbstractFloat.

Na maioria das vezes, especialmente ao lidar com dados tabulares, estamos usando arrays unidimensionais ou bidimensionais. Ambos são tipos Array para Julia. Mas, podemos usar os apelidos úteis Vector e Matrix para uma sintaxe clara e concisa.

3.2.6.2 Construção de Array

Como construímos uma array? Nesta seção, começamos construindo arrays de uma forma mais baixo-nível. Isso pode ser necessário para escrever código de alto desempenho em algumas situações. No entanto, isso não é necessário na maioria das situações, e podemos, com segurança, usar métodos mais convenientes para criar arrays. Esses métodos mais convenientes serão descritos posteriormente nesta seção.

O construtor de baixo nível para arrays em Julia é o construtor padrão. Ele aceita o tipo de elemento como o parâmetro de tipo dentro dos colchetes {} e dentro do construtor você passará o tipo de elemento seguido pelas dimensões desejadas. É comum inicializar vetores e matrizes com elementos indefinidos usando o argumento para tipo undef. Um vetor de 10 elementos undef Float64 pode ser construído como:

my_vector = Vector{Float64}(undef, 10)
[0.0, 6.91151961776797e-310, 6.91151961245736e-310, 0.0, 6.91151961776797e-310, 6.91151961245736e-310, 0.0, 6.91151961776797e-310, 6.91151961245736e-310, 0.0]

Para matrizes, uma vez que estamos lidando com objetos bidimensionais, precisamos passar dois argumentos de dimensão dentro do construtor: um para linhas e outro para colunas. Por exemplo, uma matriz com 10 linhas e 2 colunas de elementos indefinidos undef pode ser instanciada como:

my_matrix = Matrix{Float64}(undef, 10, 2)
10×2 Matrix{Float64}:
 8.72578e-311  2.0237e-320
 1.28823e-231  3.40846e-313
 8.69183e-311  2.22507e-308
 2.23377e-308  3.39525e-313
 1.28825e-231  2.0316e-320
 2.2338e-308   0.0
 2.5765e-231   0.0
 8.69183e-311  0.0
 2.5765e-231   0.0
 1.33145e-315  6.91152e-310

Nós também temos algumas apelidos sintáticos para os elementos mais comuns na construção de arrays:

Para outros elementos, podemos primeiro instanciar uma array com elementos undef e usar a função fill! para preencher todos os elementos de uma array com o elemento desejado. Segue um exemplo com 3.14 (\(\pi\)):

my_matrix_π = Matrix{Float64}(undef, 2, 2)
fill!(my_matrix_π, 3.14)
2×2 Matrix{Float64}:
 3.14  3.14
 3.14  3.14

Também podemos criar arrays com literais de array. Por exemplo, segue uma matriz 2x2 de inteiros:

[[1 2]
 [3 4]]
2×2 Matrix{Int64}:
 1  2
 3  4

Literais de array também aceitam uma especificação de tipo antes dos colchetes []. Então, se quisermos a mesma array 2x2 de antes mas agora como floats, podemos:

Float64[[1 2]
        [3 4]]
2×2 Matrix{Float64}:
 1.0  2.0
 3.0  4.0

Também funciona para vetores:

Bool[0, 1, 0, 1]
Bool[0, 1, 0, 1]

Você pode até misturar e combinar literais de array com os construtores:

[ones(Int, 2, 2) zeros(Int, 2, 2)]
2×4 Matrix{Int64}:
 1  1  0  0
 1  1  0  0
[zeros(Int, 2, 2)
 ones(Int, 2, 2)]
4×2 Matrix{Int64}:
 0  0
 0  0
 1  1
 1  1
[ones(Int, 2, 2) [1; 2]
 [3 4]            5]
3×3 Matrix{Int64}:
 1  1  1
 1  1  2
 3  4  5

Outra maneira poderosa de criar uma array é escrever uma compreensão de array. Esta maneira de criar arrays é melhor na maioria dos casos: evita loops, indexação e outras operações sujeitas a erros. Você especifica o que deseja fazer dentro dos colchetes []. Por exemplo, digamos que queremos criar um vetor de quadrados de 1 a 10:

[x^2 for x in 1:10]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Eles também suportam múltiplas entradas:

[x*y for x in 1:10 for y in 1:2]
[1, 2, 2, 4, 3, 6, 4, 8, 5, 10, 6, 12, 7, 14, 8, 16, 9, 18, 10, 20]

E condicionais:

[x^2 for x in 1:10 if isodd(x)]
[1, 9, 25, 49, 81]

Tal como acontece com literais de array, você pode especificar o tipo desejado antes dos colchetes []:

Float64[x^2 for x in 1:10 if isodd(x)]
[1.0, 9.0, 25.0, 49.0, 81.0]

Finalmente, também podemos criar arrays com funções de concatenação. Concatenação é um termo padrão em programação e significa “acorrentar juntos.” Por exemplo, podemos concatenar strings com “aa” e “bb” para conseguir “aabb”:

"aa" * "bb"

aabb

E podemos concatenar arrays para criar novas arrays:

3.2.6.3 Inspeção de Arrays

Assim que tivermos arrays, o próximo passo lógico seria inspeciona-las. Existem várias funções úteis que permitem ao usuário ter uma visão de qualquer array.

É muito útil saber que tipo de elementos existem dentro de uma array. Fazemos isso com eltype:

eltype(my_matrix_π)
Float64

Depois de conhecer seus tipos, alguém pode se interessar nas dimensões da array. Julia tem várias funções para inspecionar as dimensões da array:

3.2.6.4 Indexação e Fatiamento de Array

Às vezes, queremos inspecionar apenas certas partes de uma array. Chamamos isso de indexação e fatiamento. Se você quiser uma observação particular de um vetor, ou uma linha ou coluna de uma matriz, você provavelmente precisará indexar uma array.

Primeiro, vou criar um vetor e uma matriz de exemplo para brincar:

my_example_vector = [1, 2, 3, 4, 5]

my_example_matrix = [[1 2 3]
                     [4 5 6]
                     [7 8 9]]

Vamos começar com vetores. Supondo que você queira o segundo elemento de um vetor. Você usa colchetes [] com o índice desejado dentro:

my_example_vector[2]
2

A mesma sintaxe segue com as matrizes. Mas, como as matrizes são arrays bidimensionais, temos que especificar ambas linhas e colunas. Vamos recuperar o elemento da segunda linha (primeira dimensão) e primeira coluna (segunda dimensão):

my_example_matrix[2, 1]
4

Júlia também possui palavras-chave convencionais para o primeiro e último elementos de uma array: begin e end. Por exemplo, o penúltimo elemento de um vetor pode ser recuperado como:

my_example_vector[end-1]
4

Isso também funciona para matrizes. Vamos recuperar o elemento da última linha e segunda coluna:

my_example_matrix[end, begin+1]
8

Muitas vezes, não estamos só interessados em apenas um elemento da array, mas em todo um subconjunto de elementos da array. Podemos fazer isso fatiando uma array. Usamos a mesma sintaxe de índice, mas adicionando dois pontos : para denotar os limites a partir dos quais estamos fatiando a array. Por exemplo, suponha que queremos obter do 2º ao 4º elemento de um vetor:

my_example_vector[2:4]
[2, 3, 4]

Poderíamos fazer o mesmo com matrizes. Particularmente com matrizes, se quisermos selecionar todos os elementos em uma dimensão seguinte, podemos fazer isso com apenas dois pontos :. Por exemplo, para obter todos os elementos da segunda linha:

my_example_matrix[2, :]
[4, 5, 6]

Você pode interpretar isso com algo como “pegue a 2ª linha e todas as colunas.”

Também suporta begin e end:

my_example_matrix[begin+1:end, end]
[6, 9]

3.2.6.5 Manipulações de Array

Existem várias formas para manipular uma array. O primeiro seria manipular um único elemento da array. Nós apenas indexamos a array pelo elemento desejado e procedemos com uma atribuição =:

my_example_matrix[2, 2] = 42
my_example_matrix
3×3 Matrix{Int64}:
 1   2  3
 4  42  6
 7   8  9

Ou, você pode manipular um determinado subconjunto de elementos da array. Nesse caso, precisamos fatiar a array e, em seguida, atribuir com =:

my_example_matrix[3, :] = [17, 16, 15]
my_example_matrix
3×3 Matrix{Int64}:
  1   2   3
  4  42   6
 17  16  15

Observe que tivemos que atribuir um vetor porque nossa array fatiada é do tipo Vector:

typeof(my_example_matrix[3, :])
Vector{Int64} (alias for Array{Int64, 1})

A segunda maneira de manipular uma array é alterando suas dimensões. Suponha que você tenha um vetor de 6 elementos e deseja torná-lo uma matriz 3x2. Você pode fazer isso com reshape, usando a array como o primeiro argumento e uma tupla de dimensões como segundo argumento:

six_vector = [1, 2, 3, 4, 5, 6]
tree_two_matrix = reshape(six_vector, (3, 2))
tree_two_matrix
3×2 Matrix{Int64}:
 1  4
 2  5
 3  6

Você pode convertê-la de volta em um vetor especificando uma tupla com apenas uma dimensão como o segundo argumento:

reshape(tree_two_matrix, (6, ))
[1, 2, 3, 4, 5, 6]

A terceira forma pela qual podemos manipular uma array é aplicando uma função sobre cada elemento da array. Aqui é onde o operador “dot” ., também conhecido como broadcasting, entra.

logarithm.(my_example_matrix)
3×3 Matrix{Float64}:
 0.0      0.693147  1.09861
 1.38629  3.73767   1.79176
 2.83321  2.77259   2.70805

O operador dot em Julia é extremamente versátil. Você pode até mesmo usá-lo para vetorizar operadores infixos:

my_example_matrix .+ 100
3×3 Matrix{Int64}:
 101  102  103
 104  142  106
 117  116  115

Uma alternativa para fazer o broadcasting de função sobre um vetor é usar map:

map(logarithm, my_example_matrix)
3×3 Matrix{Float64}:
 0.0      0.693147  1.09861
 1.38629  3.73767   1.79176
 2.83321  2.77259   2.70805

Para funções anônimas, map geralmente é mais legível. Por exemplo,

map(x -> 3x, my_example_matrix)
3×3 Matrix{Int64}:
  3    6   9
 12  126  18
 51   48  45

é bastante claro. No entanto, a mesma operação utilizando o operador de broadcast fica da seguinte forma:

(x -> 3x).(my_example_matrix)
3×3 Matrix{Int64}:
  3    6   9
 12  126  18
 51   48  45

Além disso, map funciona com fatiamento:

map(x -> x + 100, my_example_matrix[:, 3])
[103, 106, 115]

Finalmente, às vezes, e especialmente ao lidar com dados tabulares, queremos aplicar uma função sobre todos os elementos em uma dimensão específica de uma array. Isso pode ser feito com a função mapslices. Parecido com map, o primeiro argumento é a função e o segundo argumento é a array. A única mudança é que precisamos especificar o argumento dims para sinalizar em qual dimensão queremos transformar os elementos.

Por exemplo, vamos usar mapslice com a função sum em ambas as linhas (dims=1) e colunas (dims=2):

# rows
mapslices(sum, my_example_matrix; dims=1)
1×3 Matrix{Int64}:
 22  60  24
# columns
mapslices(sum, my_example_matrix; dims=2)
3×1 Matrix{Int64}:
  6
 52
 48

3.2.6.6 Iteração de array

Uma operação comum é iterar sobre uma array com um laço for. O laço for regular, quando aplicado sobre uma array retorna cada elemento.

O exemplo mais simples é com um vetor.

simple_vector = [1, 2, 3]

empty_vector = Int64[]

for i in simple_vector
    push!(empty_vector, i + 1)
end

empty_vector
[2, 3, 4]

Às vezes, você não quer iterar sobre cada elemento, mas sim sobre cada índice da array. Podemos usar a função eachindex combinada com um loop for para iterar sobre cada índice de array.

Novamente, vamos mostrar um exemplo com um vetor:

forty_twos = [42, 42, 42]

empty_vector = Int64[]

for i in eachindex(forty_twos)
    push!(empty_vector, i)
end

empty_vector
[1, 2, 3]

Nesse exemplo, o eachindex(forty_twos) retorna os índices de forty_twos, nomeadamente [1, 2, 3].

Da mesma forma, podemos iterar sobre matrizes. O laço for padrão itera primeiro sobre as colunas e depois sobre as linhas. Ele irá primeiro percorrer todos os elementos na coluna 1, da primeira à última linha, em seguida, ele se moverá para a coluna 2 de maneira semelhante até cobrir todas as colunas.

Para aqueles familiarizados com outras linguagens de programação: Julia, como a maioria das linguagens de programação científica, é “colunar.” Colunar significa que os elementos da coluna são armazenados lado a lado na memória13. Isso também significa que iterar sobre os elementos em uma coluna é muito mais rápido do que sobre os elementos em uma linha.

Ok, vamos mostrar isso em um exemplo:

column_major = [[1 3]
                [2 4]]

row_major = [[1 2]
             [3 4]]

Se fizermos um loop sobre o vetor armazenado de forma ordenada para as colunas, então o resultado também é ordenado:

indexes = Int64[]

for i in column_major
    push!(indexes, i)
end

indexes
[1, 2, 3, 4]

No entanto, o resultado não fica ordenado ao interarmos sobre a outra matriz:

indexes = Int64[]

for i in row_major
    push!(indexes, i)
end

indexes
[1, 3, 2, 4]

Muitas vezes é melhor usar funções especializadas para esses loops:

3.2.7 Par

Em comparação com a enorme seção sobre arrays, esta seção sobre pares será breve. Par é uma estrutura de dados que contém dois objetos (que em geral estão relacionados um ao outro). Construímos um par em Julia usando a seguinte sintaxe:

my_pair = "Julia" => 42
"Julia" => 42

Os elementos são armazenados nos campos first e second.

my_pair.first
Julia
my_pair.second
42

Mas, na maioria dos casos, é mais fácil usar first e last14:

first(my_pair)
Julia
last(my_pair)
42

Os pares serão muito usados na manipulação e visualização de dados, uma vez que ambos DataFrames.jl (Section 4) e Makie.jl (Section 5) aceitam objetos do tipo Pair em suas funções principais. Por exemplo, com DataFrames.jl veremos que :a => :b pode ser usado para renomear a coluna :a para :b.

3.2.8 Dict

Se você entendeu o que é um Pair, então Dict não será um problema. Para todos os propósitos práticos, Dicts são mapeamentos de chaves para valores. Por mapeamento, queremos dizer que se você der alguma chave a um Dict, então o Dict poderá lhe dizer qual valor pertence àquela chave. Chaves (keys) e valores (values) podem ser de qualquer tipo, mas normalmente keys são strings.

Existem duas maneiras de construir Dicts em Julia. A primeira é passando um vetor de tuplas como (key, value) para o construtor Dict:

name2number_map = Dict([("one", 1), ("two", 2)])
Dict{String, Int64} with 2 entries:
  "two" => 2
  "one" => 1

Existe uma sintaxe mais legível com base no tipo Pair descrito acima. Você também pode passar Pairs de key => values para o construtor Dict:

name2number_map = Dict("one" => 1, "two" => 2)
Dict{String, Int64} with 2 entries:
  "two" => 2
  "one" => 1

Você pode recuperar um value de um Dicts ao indexá-lo pela key correspondente:

name2number_map["one"]
1

Para adicionar uma nova entrada, você indexa o Dict pela key desejada e atribui um value com o operador de atribuição =:

name2number_map["three"] = 3
3

Se você quer checar se um Dict tem uma certa key você pode usar keys e in:

"two" in keys(name2number_map)
true

Para deletar uma key você pode usar a função delete!:

delete!(name2number_map, "three")
Dict{String, Int64} with 2 entries:
  "two" => 2
  "one" => 1

Ou, para excluir uma chave enquanto retorna seu valor, você pode usar pop!:

popped_value = pop!(name2number_map, "two")
2

Agora, nosso name2number_map tem apenas uma key:

name2number_map
Dict{String, Int64} with 1 entry:
  "one" => 1

Dicts também são usados para manipulação de dados por DataFrames.jl (Section 4) e para visualização de dados por Makie.jl (Section 5). Logo, é importante conhecer suas funcionalidades básicas.

Existe outra maneira útil de construir Dicts. Suponha que você tenha dois vetores e deseja construir um Dict com um deles como se fosse keys e outro como se fosse values. Você pode fazer isso com uma função zip que “junta” dois objetos (como um zíper):

A = ["one", "two", "three"]
B = [1, 2, 3]

name2number_map = Dict(zip(A, B))
Dict{String, Int64} with 3 entries:
  "two" => 2
  "one" => 1
  "three" => 3

Por exemplo, agora podemos obter o número 3 via:

name2number_map["three"]
3

3.2.9 Símbolo

Symbol na verdade não é uma estrutura de dados. É um tipo e se comporta de modo muito parecido com uma string. Em vez de colocar o texto entre aspas, um símbolo começa com dois pontos (:) e pode conter sublinhados:

sym = :some_text
:some_text

Podemos facilmente converter um símbolo em string e vice-versa:

s = string(sym)
some_text
sym = Symbol(s)
:some_text

Um benefício simples dos símbolos é que você digita um caractere a menos, ou seja, :some_text versus "some text". Usamos muito Symbols na manipulação de dados com o package DataFrames.jl (Section 4) e em visualização de dados com o package Makie.jl (Section 5).

3.2.10 Operador Splat

Em Julia, temos o operador “splat” ... que é usado em chamadas de função como uma sequência de argumentos. Ocasionalmente, usaremos o splat em algumas chamadas de função nos capítulos sobre manipulação de dados e visualização de dados.

A maneira mais intuitiva de aprender sobre o splat é com um exemplo. A função add_elements abaixo leva três argumentos para serem somados:

add_elements(a, b, c) = a + b + c
add_elements (generic function with 1 method)

Agora, suponha que temos uma coleção com três elementos. A maneira ingênua de fazer isso seria fornecer à função todos os três elementos como argumentos de função:

my_collection = [1, 2, 3]

add_elements(my_collection[1], my_collection[2], my_collection[3])
6

Aqui é que usamos o operador “splat” ... que pega uma coleção (geralmente uma array, vetor, tupla ou range) e a converte em uma sequência de argumentos:

add_elements(my_collection...)
6

O ... deve ser incluído após a coleção que queremos espalhar ou “splat” em uma sequência de argumentos. Ambos os exemplos apresentados acima têm o mesmo resultado:

add_elements(my_collection...) == add_elements(my_collection[1], my_collection[2], my_collection[3])
true

Sempre que Julia vê um operador splat dentro de uma chamada de função, ele será convertido em uma sequência de argumentos para todos os elementos da coleção separados por vírgulas.

Também funciona para ranges:

add_elements(1:3...)
6

  1. 13. ou, que os ponteiros de endereço de memória para os elementos na coluna são armazenados um ao lado do outro.↩︎

  2. 14. é mais fácil porque first e last também funcionam em muitas outras coleções, então você não precisa se lembrar de tanta coisa.↩︎



CC BY-NC-SA 4.0 Jose Storopoli, Rik Huijzer, Lazaro Alonso