Introduction to the Julia programming language
4 Arrays¶
Arrays¶
Julia supports Arrays as first-class values - they're represented in the type hierarchy as a set of parameterised types (by geometry and the type of their elements).
For 1-d Arrays, things look much like Python lists - in fact, an array literal containing a mix of types will work much like a Python list in all respects. We can also write "comprehensions" as for Python lists.
Arrays holding just one type are specialised - for efficiency - to just hold that type.
#you can make "Python like" arrays with mixed types in them
@show my_list = [1, 2, 'a']
#but Julia will specialise an Array literal with only 1 type in it to be uniform
@show my_integer_array = [1,2,7, 66]
@show typeof(my_integer_array)
@show my_float_array = Float64[1, 5.5, 5//6] #or we can explicitly impose a type
@show squared_integers = [i^2 for i in 1:10];
my_list = [1, 2, 'a'] = Any[1, 2, 'a'] my_integer_array = [1, 2, 7, 66] = [1, 2, 7, 66] typeof(my_integer_array) = Vector{Int64} my_float_array = Float64[1, 5.5, 5 // 6] = [1.0, 5.5, 0.8333333333333334] squared_integers = [i ^ 2 for i = 1:10] = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Indexing of Julia arrays starts at 1
by default, not 0, although arrays can be customised to have different index ranges. The keywords begin
and end
always refer to the first and last elements in a particular index.
As well as specifying a specific index, or range of indices, Julia arrays also support other selection methods - for example, selection masks where only the true
elements are returned.
@show my_list[1] == my_list[begin] #basic indexing
@show my_integer_array[3:end] #last 2 elements
mask = [ iseven(i) for i in 1:10 ] #only even values are true
@show squared_integers[mask]; #selects only elements in the mask
my_list[1] == my_list[begin] = true my_integer_array[3:end] = [7, 66] squared_integers[mask] = [4, 16, 36, 64, 100]
Appending elements to an array:
a = [1, 2, 3]
push!(a, 4)
4-element Vector{Int64}: 1 2 3 4
Multi-dimensional Arrays¶
Julia also supports multi-dimensional Arrays internally as first-class types, (that is, they're not simply "an array of arrays" or whatever), allowing for efficient representations of specialised data (like SparseArrays or diagonal matrices).
As a result, indexing is done with a single index expression for all axes - in addition, Julia arrays are column-major, unlike C, Python etc.
A = [ [1, 2] [3, 4] ]
2×2 Matrix{Int64}: 1 3 2 4
A[1,1] # same as A[begin,begin]
1
Julia shows 2D arrays as <rows>x<cols> Array{<type>}
:
B = [ 1 4
2 5
3 6 ]
3×2 Matrix{Int64}: 1 4 2 5 3 6
Another way of specifying elements row-wise:
C = [1 2 3; 4 5 6 ]
2×3 Matrix{Int64}: 1 2 3 4 5 6
Be aware of these subtile differences:
D = [1, 2, 3]
3-element Vector{Int64}: 1 2 3
F = [1 2 3]
1×3 Matrix{Int64}: 1 2 3
E = [1; 2; 3]
3-element Vector{Int64}: 1 2 3
A = [ [1, 2], [3, 4]]
2-element Vector{Vector{Int64}}: [1, 2] [3, 4]
A = [ [1, 2] [3, 4]]
2×2 Matrix{Int64}: 1 3 2 4
Multi-dimensional array literals can be created with a terse concatenation syntax: sequences of multiple semicolons represent concatenation of sub-arrays in increasingly higher dimensions (space separation is equivalent to "row" and comma separation to "column" ordering).
threed_array = [ 1 ; 2 ;; 3 ; 4 ;;; 5 ; 6 ;; 7 ; 8 ]
2×2×2 Array{Int64, 3}: [:, :, 1] = 1 3 2 4 [:, :, 2] = 5 7 6 8
We can also use the fact that for
takes the outer product of its ranges to write a compact comprehension, if our array is expressible in such a way.
odd_powers_and_offset = [ i^j + k for i in 0:3, j in 1:2:5, k in 0:1 ]
4×3×2 Array{Int64, 3}: [:, :, 1] = 0 0 0 1 1 1 2 8 32 3 27 243 [:, :, 2] = 1 1 1 2 2 2 3 9 33 4 28 244
Array constructors¶
As with NumPy, there are a large number of utility methods to efficiently create Arrays of a specific type and geometry, and to reshape existing arrays.
@show many_zeros = zeros(Float64, 2, 4, 3) #2x4x3 array, zero'd
m = [0,1,2,3]
reshape(m, (2,2)) #2x2 matrix, column-major!
many_zeros = zeros(Float64, 2, 4, 3) = [0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0;;; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0;;; 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0]
2×2 Matrix{Int64}: 0 2 1 3
M1 = [1 2 ; 3 4]
M2 = [5 6 ; 7 8]
2×2 Matrix{Int64}: 5 6 7 8
Concatenating vertically:
M = [M1; M2] # same as vcat(M1, M2)
4×2 Matrix{Int64}: 1 2 3 4 5 6 7 8
Concatenating horizontally:
M = [M1 M2] # same as hcat(M1, M2)
2×4 Matrix{Int64}: 1 2 5 6 3 4 7 8
Views¶
We can "slice" into an array in all dimensions with a syntax familiar from Python, but by default this creates a copy of the original array data.
slice1 = odd_powers_and_offset[:,:,1]
slice2 = odd_powers_and_offset[:,:,1]
#false, because these are both different copies of the underlying data
@show slice1 === slice2
slice1[end,end,end] = 88
slice2[end,end,end] #hasn't changed because these are different memory
slice1 === slice2 = false
243
Julia provides view
to instead give us an equivalent slice which references the original array - there's no copy incurred, and as a result modifying the view modifies the corresponding elements of the source array. Views are explicitly lazy, so they also work well with tabular implementations that may page rows into memory or otherwise work lazily themselves.
In order to make this more ergonomic, the macros @view
(for a single slice operation) and @views
(for an expression containing multiple slices) will convert slices into views automatically.
A number of methods exist for views to inspect their parent arrays.
view1 = view(odd_powers_and_offset, :, : , 1)
view2 = @view odd_powers_and_offset[:,:,begin]
@views view3 = odd_powers_and_offset[:,:,begin]
@show view1
#true, because these reference the same memory
@show view1 === view2 === view3
view1[end,end,end] = 88
#has changed, as these are the same memory
odd_powers_and_offset[end,end,1]
view1 = [0 0 0; 1 1 1; 2 8 32; 3 27 88] view1 === view2 === view3 = true
88
Transposing an array¶
M = [1 2 3 4
5 6 7 8
9 10 11 12]
M'
4×3 adjoint(::Matrix{Int64}) with eltype Int64: 1 5 9 2 6 10 3 7 11 4 8 12