Vectores, tuplas y diccionarios en Julia (6ª parte – ¡Hola Julia!)

Publicado el 30 julio 2020 por Daniel Rodríguez @analyticslane

En muchos programas es necesario poder trabajar con secuencias de datos, que pueden estar ordenados o no. Para estos casos contamos con vectores, tuplas y diccionarios en Julia. Estructuras de datos que vamos a ver a continuación.

Vectores en Julia

Los vectores son posiblemente la estructura de datos más utilizada. Los cuales son una secuencia ordenada de elementos, que pueden ser números, cadenas de texto, o incluso otros vectores. A diferencia de lo que pasa en otros lenguajes, como puede se el caso de Matlab, los elementos de un vector no tienen porque ser del mismo tipo, pudiendo incluir todos los elementos que necesitemos.

Para crear un vector solamente hay que rodear entre corchetes los elementos de este. Por ejemplo,

vect = [1,2,3,4]

Este es un vector que contiene cuatro elementos que son 1, 2, 3 y 4. Además almacenados en este orden. Para acceder a cada uno de los elementos hay que escribir el nombre de la variable seguido de la posición entre corchetes. Recordando que el Julia los vectores comienzan en 1. Así, vect[3] hace referencia al tercer elemento del vector, 3.

Los vectores son mutables, por lo que en las funciones se pasan por referencia. Para modificar un elemento del vector solamente hay que asignar el nuevo valor a este. Así para asignar el valor 10 en la tercera posición solo hay que hacer vect[3] = 10.

Diferente tipos de datos y vectores anidados

Pero los vectores pueden contener más tipos de datos, como en el siguiente caso

vector = [1, 2, "hola", [3, 4]]

En este caso el prime u segundo elemento son enteros, el tercero una cadena de texto y el cuarto un vector. Los cuales se suelen denominar vectores anidados. Hay que tener en cuenta que este vector es de cuatro elementos, no cinco, ya que el vector anidado cuenta como un elemento. Para conocer el número de elementos de un vector se puede recurrir a la función length.

Cuando un vector contiene más de un tipo de dato, como es el caso, esto son de tipo Array{Any,1}, mientras que cuando son del mismo el vector es del tipo específico. Por ejemplo, en el caso de un vector de enteros sera se tipo Array{Int64,1}. Información que se puede ver con la función typeof. El número después del tipo son las dimensiones.

Recorrer los elementos de un vector

El método más sencillo para recorrer los elementos de un vector son los bucles for. Para lo que se puede utilizar el operador in o

for value ∈ datos
    println(value)
end

Ahora, en el caso de que sea necesario acceder al elemento para modificarlos se puede hacer directamente de la siguiente manera.

for i ∈ 1:length(datos)
    datos[i] *= 2
end

Aunque posiblemente sea más fácil usar la función que tiene definida Julia para este caso eachindex. Una función que nos devuelve todos los índices.

for i ∈ eachindex(datos)
    datos[i] *= 2
end

Operador dos puntos

Para acceder a una subvector de otro se puede usar el operador dos puntos. Con lo que es posible recorrer los elementos desde la posición inicial a la final en los pasos indicados. A continuación, se puede ver como se recorre los vectores de la tercera a la cuarta posición y de la tercera a la segunda (invirtiendo el orden).

datos = [1,2,3,4,5]
datos[3:4] # [3,4]
datos[3:-1:2] # [3,2]

Si se quiere llegar al último y no se conoce la longitud de los vectores siempre se puede usar la palabra clave end para indicar esto.

datos[3:end] # [3,4,5]

Finalmente, el operador dos puntos permite hacer copias de los vectores. Generando un nuevo vector igual al original. Lo que nos permite modificar los dos de manera independiente

datos = [1,2,3,4,5]
datos2 = datos[:]
datos2[2] = 0
println(datos) # [1, 2, 3, 4, 5]
println(datos2) # [1, 0, 3, 4, 5]

Si hubiésemos asignado en datos2 el valor de datos el resultado sería diferente, ambos vectores se verían modificados porque realmente apuntan al mismo objeto.

datos = [1,2,3,4,5]
datos2 = datos
datos2[2] = 0
println(datos) # [1, 0, 3, 4, 5]
println(datos2) # [1, 0, 3, 4, 5]

Cuando dos vectores son iguales

El punto anterior nos puede llevar a pensar qué pasa con los vectores al asignarlos a una variable. Al asignar un vector a una variable lo que sucede es que se asigna la dirección de memoria a esta variable, no el valor. Por lo que sí se asigna un vector a otra variable ambos contendrán el mismo objeto.

Para comprobar si dos vectores apuntan al mismo objeto se puede usar el operador igualdad === o . Un operador que no compara el contenido, sino que es el mismo objeto. Esto es, si se crean dos objetos con el mismo contenido se obtiene falso como resultado

v_1 = [1,2,3]
v_2 = [1,2,3]

v_1 ≡ v_2 # false

Pero si apunta al mismo objeto se obtendrá verdadero.

v_1 = [1,2,3]
v_2 = v_1

v_1 ≡ v_2 # true

Un comportamiento diferente al que se observa en los tipos primitivos, donde dos variables con el mismo valor son iguales.

v_1 = "Hola"
v_2 = "Hola"

v_1 ≡ v_2 # true

Agregar y eliminar elementos en un vector

Julia dispone de múltiples funciones para trabajar con los elementos de un vector y modificarlo. El método para agregar un elemento es push!, con el que se añade un elemento al final del mismo.

datos = [1,2,3]
push!(datos ,10) # [1,2,3,10]

Para concatenar vectores se puede recurrir a append!, función con la que se agrega al primer vector el contenido del segundo.

datos = [1,2,3]
append!(datos, [3, 4]) # [1,2,3,3,4]

Es importante diferencia uno de otro, ya que si se usa push! para agregar un vector este se añadirá como tal al final. Siempre y cuando el tipo de dato del primer vector lo soporte, algo que no sería válido en el caso anterior. Por ejemplo, el siguiente código no funciona

datos = [1,2,3]
push!(datos, [3, 4]) # error

Esto es porque datos es de tipo Array{Int64,1} y debería ser de tipo Array{Any,1} para poder admitir números y vectores.

Para insertar hay varias funciones:

  • pop! para eliminar el último valor y devolverlo
  • popfirst! para eliminar el primer valor y devolverlo
  • splice! para eliminar en una posición dada y devolver el valor eliminado
  • deleteat! para eliminar en una posición dada devolviendo el propio vector, una alternativa para cuando no necesitamos el valor eliminado

Lo que se puede ver en el siguiente ejemplo

data = [1, 2, 3, 4, 5, 6]

último = pop!(data)
primero = popfirst!(data)
el_2º = splice!(data, 2)

deleteat!(data, 2)

El operador .

Para aplicar sobre cada uno de los elementos de un vector es necesario usar la versión modificado con un punto de los operadores. Por ejemplo, si intentamos elevar un vector al cuadrado con ^ obtendremos un error, es necesario usar el operador .^.

[1,2,3] .^ 3 # [1, 8, 27]

Recordemos que si omitimos el punto tendremos un error. Algo que también se puede aplicar a las funciones, aunque en este caso el punto se sitúa al final del nombre. Así para poner mayúsculas a la primera letra de un vector de cadenas se puede hacer

uppercasefirst.(["hola", "julia"]) # ["Hola", "Julia"]

Recordando que no indicar el punto provocaría que la función fallase, ya que no soporta entradas de tipo vector.

Tuplas

Las tuplas son básicamente vectores inmutables. Esto es, una vez definida una dupla no es posible modificar esta, ni ninguno de sus valores. A diferencia de los vectores las tuplas se crean escribiendo los vectores entre paréntesis. Así una tupla podría ser:

tupla = (1,2,3)

Aunque hay un caso especial, las tuplas de un elemento, en las que es necesario escribir una coma después del valor, esto es (1,). Ya que algo como (1) no seria una tupla.

El acceso a un elemento es exactamente igual que en los vectores, pero no se pueden modificar.

En este punto se puede notar que las funciones que devuelven mas de un valor lo que realmente devuelven es una tupla.

Diccionarios

Los diccionarios son una generalización de los vectores donde en lugar de usar un entero para hacer referencia a los valores se unan claves. Esto es, una clave que se asocia con un valor. Por lo que las claves no se pueden repetir. Las claves pueden ser de cualquier tipo de dato que tenga una representación única.

La creación de un diccionario se hace con el constructor Dict. Siendo la forma más sencilla crear un objeto vacío y añadir los valores. Ya que los diccionarios son mutables.

dic = Dict()
dic["uno"] = 1
dic["dos"] = 2

En este ejemplo se ha creado un diccionario con dos elementos. Las claves son "uno" y "dos", las cuales están asociadas respectivamente con los valores 1 y 2. Este diccionario también se puede crear de la siguiente manera.

dic = Dict("uno" => 1, "dos" => 2)

Aunque el resultado no es lo mismo. Ahora se tiene un diccionario de tipo Dict{String,Int64}, mientras que antes el tipo era Dict{Any,Any}. Algo que hay que tener en cuenta ya que en el segundo caso no se puede añadir una clave que no sea una cadena de texto ni un valor que no sea entero. Julia dará un error. Algo que no es un problema en el primer caso ya que tanto las claves como el valor pueden ser de cualquier tipo.

Una de las funciones más interesante para los diccionarios es keys, con las que se puede obtener un vector con los nombres de las claves de este. Algo que se puede usar para iterar sobre el diccionario o comprobar que una clave existe.

Estructurar de datos en Julia

Al estudiar los vectores en Julia, junto a las tuplas y diccionarios, nos podemos dar cuenta que el lenguaje cuenta con herramientas avanzadas. Algo facilitar mucho el trabajo.