단일·다차원 배열

단일·다차원 배열 (Single- and multi-dimensional Arrays)

줄리아는 대부분의 기술 컴퓨팅 언어처럼 일급(first-class) 배열 구현을 제공해요. 대부분의 기술 컴퓨팅 언어는 다른 컨테이너를 희생하면서 배열 구현에 많은 주의를 기울여요. 하지만 줄리아는 배열을 특별히 대우하지 않아요. 배열 라이브러리는 거의 전적으로 줄리아 자체로 구현되어 있고, 다른 어떤 줄리아 코드와 마찬가지로 컴파일러로부터 성능을 얻어요. 따라서 AbstractArray를 상속해 커스텀 배열 타입을 정의하는 것도 가능해요. 커스텀 배열 타입 구현에 대한 자세한 내용은 AbstractArray 인터페이스에 관한 매뉴얼 절을 보세요.

배열은 다차원 격자(grid)에 저장된 객체들의 컬렉션이에요. 0차원 배열도 허용되는데, 이 FAQ 항목을 보세요. 가장 일반적인 경우 배열은 Any 타입의 객체를 담을 수 있어요. 대부분의 계산 목적에서는 배열이 Float64Int32 같은 더 구체적인 타입의 객체를 담아야 해요.

일반적으로, 많은 다른 기술 컴퓨팅 언어와 달리 줄리아는 성능을 위해 프로그램을 벡터화 스타일로 작성하기를 기대하지 않아요. 줄리아의 컴파일러는 타입 추론을 사용하고 스칼라 배열 인덱싱에 대해 최적화된 코드를 생성해서, 성능을 희생하지 않고 더 편리하고 읽기 좋은 스타일로 프로그램을 작성할 수 있게 해 주며, 때로는 메모리를 덜 쓰게 해 줘요.

줄리아에서 함수의 모든 인자는 공유(sharing, 즉 포인터)로 전달돼요. 어떤 기술 컴퓨팅 언어는 배열을 값으로 전달하는데, 이는 피호출자가 호출자의 값 변경을 우연히 하지 못하게 막지만, 원치 않는 배열 복사를 피하기 어렵게 해요. 관례상 !로 끝나는 함수 이름은 그 인자 중 하나 이상의 값을 변형(mutate)하거나 파괴한다는 뜻이에요 (예를 들어 sortsort!를 비교해 보세요). 피호출자는 변경 의도가 없는 입력을 수정하지 않도록 명시적 복사를 해야 해요. 변형하지 않는 많은 함수는 입력의 명시적 복사본에 대해 끝에 !가 붙은 같은 이름의 함수를 호출하고 그 복사본을 반환하는 방식으로 구현돼요.

출처: julia 공식 메뉴얼 — Arrays

본문

기본 함수 (Basic Functions)

Function Description
eltype(A) A에 담긴 요소들의 타입
length(A) A의 요소 개수
ndims(A) A의 차원 수
size(A) A의 차원들을 담은 튜플
size(A,n) 차원 n을 따른 A의 크기
axes(A) A의 유효한 인덱스들을 담은 튜플
axes(A,n) 차원 n을 따른 유효한 인덱스들을 표현하는 범위
eachindex(A) A의 각 위치를 방문하는 효율적인 이터레이터
stride(A,k) 차원 k를 따른 스트라이드(인접 요소 사이의 선형 인덱스 거리)
strides(A) 각 차원의 스트라이드들의 튜플

생성과 초기화 (Construction and Initialization)

배열을 생성하고 초기화하는 많은 함수가 제공돼요. 다음의 그러한 함수 목록에서 dims... 인자를 가진 호출은 단일 차원 크기 튜플을 받거나, 가변 인자 수로 전달된 일련의 차원 크기를 받을 수 있어요. 이 함수들 대부분은 배열의 요소 타입인 첫 입력 T도 받아요. 타입 T를 생략하면 기본값 Float64가 돼요.

Function Description
Array{T}(undef, dims...) 초기화되지 않은 조밀 배열(dense Array)
zeros(T, dims...) 모두 0인 Array
ones(T, dims...) 모두 1인 Array
trues(dims...) 모든 값이 true인 BitArray
falses(dims...) 모든 값이 false인 BitArray
reshape(A, dims...) A와 같은 데이터를 담지만 다른 차원을 가진 배열
copy(A) A 복사
deepcopy(A) A 복사, 요소도 재귀적으로 복사
similar(A, T, dims...) A와 같은 타입(조밀, 희소 등)이지만 지정된 요소 타입과 차원을 가진 초기화되지 않은 배열. 두 번째와 세 번째 인자는 모두 선택적이며, 생략하면 각각 A의 요소 타입과 차원이 기본값.
reinterpret(T, A) A와 같은 이진 데이터를 담지만 요소 타입이 T인 배열
rand(T, dims...) 무작위, iid[1]이고 균등 분포된 값을 가진 Array. 부동소수점 타입 T의 경우 값은 반개방 구간 $[0, 1)$에 있어요.
randn(T, dims...) 무작위, iid이고 표준 정규 분포된 값을 가진 Array
Matrix{T}(I, m, n) m×n 항등 행렬. I에 대해 using LinearAlgebra가 필요.
range(start, stop, n) start에서 stop까지 선형 간격의 n개 요소로 이루어진 범위
fill!(A, x) 배열 A를 값 x로 채움
fill(x, dims...) x로 채워진 Array. 특히 fill(x)x를 담은 0차원 배열을 만듦

이 함수들에 차원을 전달하는 다양한 방법을 보려면 다음 예시를 생각해 보세요.

julia> zeros(Int8, 2, 3)
2×3 Matrix{Int8}:
 0  0  0
 0  0  0

julia> zeros(Int8, (2, 3))
2×3 Matrix{Int8}:
 0  0  0
 0  0  0

julia> zeros((2, 3))
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

여기서 (2, 3)은 튜플이고 첫 번째 인자 — 요소 타입 — 는 선택적이며 기본값은 Float64예요.

배열 리터럴 (Array literals)

배열은 대괄호로 직접 만들 수도 있어요. [A, B, C, ...] 문법은 쉼표로 구분된 인자들을 요소로 담는 1차원 배열(즉 벡터)을 만들어요. 결과 배열의 요소 타입(eltype)은 중괄호 안 인자들의 타입으로 자동 결정돼요. 모든 인자가 같은 타입이면 그것이 eltype이 돼요. 모두 공통 승격 타입이 있으면 convert를 써서 그 타입으로 변환되고 그 타입이 배열의 eltype이 돼요. 그게 아니면 아무거나 담을 수 있는 이질적 배열 — Vector{Any} — 이 만들어져요. 여기에는 인자가 없는 리터럴 []도 포함돼요. 배열 리터럴은 T[A, B, C, ...] 문법으로 타입을 지정할 수 있는데, T는 타입이에요.

julia> [1, 2, 3] # An array of `Int`s
3-element Vector{Int64}:
 1
 2
 3

julia> promote(1, 2.3, 4//5) # This combination of Int, Float64 and Rational promotes to Float64
(1.0, 2.3, 0.8)

julia> [1, 2.3, 4//5] # Thus that's the element type of this Array
3-element Vector{Float64}:
 1.0
 2.3
 0.8

julia> Float32[1, 2.3, 4//5] # Specify element type manually
3-element Vector{Float32}:
 1.0
 2.3
 0.8

julia> []
Any[]

연결 (Concatenation)

대괄호 안의 인자들이 쉼표 대신 단일 세미콜론(;)이나 개행으로 구분되면, 그 내용이 인자 자체를 요소로 쓰는 대신 서로 **수직 연결(vertically concatenated)**돼요.

julia> [1:2, 4:5] # Has a comma, so no concatenation occurs. The ranges are themselves the elements
2-element Vector{UnitRange{Int64}}:
 1:2
 4:5

julia> [1:2; 4:5]
4-element Vector{Int64}:
 1
 2
 4
 5

julia> [1:2
        4:5
        6]
5-element Vector{Int64}:
 1
 2
 4
 5
 6

마찬가지로 인자들이 탭이나 공백 또는 이중 세미콜론으로 구분되면 그 내용이 서로 **수평 연결(horizontally concatenated)**돼요.

julia> [1:2  4:5  7:8]
2×3 Matrix{Int64}:
 1  4  7
 2  5  8

julia> [[1,2]  [4,5]  [7,8]]
2×3 Matrix{Int64}:
 1  4  7
 2  5  8

julia> [1 2 3] # Numbers can also be horizontally concatenated
1×3 Matrix{Int64}:
 1  2  3

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

단일 세미콜론(또는 개행)과 공백(또는 탭)을 결합해 수평·수직 연결을 동시에 할 수 있어요.

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

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

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

공백(과 탭)은 세미콜론보다 우선순위가 높아서, 먼저 수평 연결을 수행한 다음 결과를 연결해요. 반면 이중 세미콜론을 수평 연결에 쓰면 결과를 수평 연결하기 전에 수직 연결을 먼저 수행해요.

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

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

;;;이 각각 첫 번째와 두 번째 차원으로 연결하듯, 더 많은 세미콜론을 쓰면 이 일반적인 체계가 확장돼요. 구분자의 세미콜론 개수가 특정 차원을 지정해요. 그래서 ;;;는 세 번째 차원으로, ;;;;는 4번째 차원으로 연결하고, 이런 식이에요. 세미콜론이 적을수록 우선순위가 높아서, 보통 낮은 차원들이 먼저 연결돼요.

julia> [1; 2;; 3; 4;; 5; 6;;;
        7; 8;; 9; 10;; 11; 12]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

이전과 마찬가지로 수평 연결용 공백(과 탭)은 어떤 수의 세미콜론보다도 우선순위가 높아요. 따라서 더 높은 차원의 배열도 요소들을 레이아웃과 유사하게 텍스트로 배치해서 행을 먼저 지정해 쓸 수 있어요.

julia> [1 3 5
        2 4 6;;;
        7 9 11
        8 10 12]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

julia> [1 2;;; 3 4;;;; 5 6;;; 7 8]
1×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
 1  2

[:, :, 2, 1] =
 3  4

[:, :, 1, 2] =
 5  6

[:, :, 2, 2] =
 7  8

julia> [[1 2;;; 3 4];;;; [5 6];;; [7 8]]
1×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
 1  2

[:, :, 2, 1] =
 3  4

[:, :, 1, 2] =
 5  6

[:, :, 2, 2] =
 7  8

둘 다 두 번째 차원으로의 연결을 뜻하지만, 이중 세미콜론이 단순히 "줄 연속(line continuation)" 문자 역할을 하지 않는 한 공백(또는 탭)과 ;;은 같은 배열 표현식에 나타날 수 없어요. 이는 단일 수평 연결이 여러 줄에 걸칠 수 있게 해 줘요 (줄바꿈이 수직 연결로 해석되지 않고).

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

종결 세미콜론을 써서 길이 1인 차원을 뒤에 추가할 수도 있어요.

julia> [1;;]
1×1 Matrix{Int64}:
 1

julia> [2; 3;;;]
2×1×1 Array{Int64, 3}:
[:, :, 1] =
 2
 3

더 일반적으로 연결은 cat 함수로 수행할 수 있어요. 이런 문법들은 그 자체가 편의 함수인 함수 호출의 축약형이에요.

Syntax Function Description
cat(A...; dims...) cat 차원(들) k를 따라 입력 배열 연결
[A; B; C; ...] vcat cat(A...; dims=1)의 축약
[A B C ...] hcat cat(A...; dims=2)의 축약
[A B; C D; ...] hvcat 동시 수직·수평 연결
[A; C;; B; D;;; ...] hvncat 동시 n차원 연결. 세미콜론의 개수가 연결할 차원을 나타냄

타입이 지정된 배열 리터럴 (Typed array literals)

특정 요소 타입을 가진 배열은 T[A, B, C, ...] 문법으로 만들 수 있어요. 요소 타입 T의 1차원 배열을 만들고 A, B, C 등의 요소를 담도록 초기화해요. 예를 들어 Any[x, y, z]는 어떤 값이든 담을 수 있는 이질적 배열을 만들어요.

연결 문법도 앞에 타입을 붙여 결과의 요소 타입을 지정할 수 있어요.

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

julia> Int8[[1 2] [3 4]]
1×4 Matrix{Int8}:
 1  2  3  4

컴프리헨션 (Comprehensions)

컴프리헨션은 배열을 만드는 일반적이고 강력한 방법을 제공해요. 컴프리헨션 문법은 수학의 집합 구성 표기법과 비슷해요.

A = [ F(x, y, ...) for x=rx, y=ry, ... ]

이 형태의 의미는 변수 x, y 등이 주어진 값 목록의 각 값을 취하면서 F(x,y,...)가 평가된다는 거예요. 값은 어떤 반복 가능한 객체로도 지정할 수 있지만, 보통 1:n이나 2:(n-1) 같은 범위 또는 [1.2, 3.4, 5.7] 같은 명시적 값 배열일 거예요. 결과는 각 F(x,y,...) 평가가 스칼라를 반환하는, 변수 범위 rx, ry 등의 차원들의 연결인 차원을 가진 N차원 조밀 배열이에요.

다음 예시는 1차원 격자를 따라 현재 요소와 그 좌우 이웃의 가중 평균을 계산해요.

julia> x = [4, 8, 2, 6, 10, 10, 2, 8]
8-element Vector{Int64}:
  4
  8
  2
  6
 10
 10
  2
  8

julia> [ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
6-element Vector{Float64}:
 5.5
 4.5
 6.0
 9.0
 8.0
 5.5

결과 배열 타입은 배열 리터럴과 마찬가지로 계산된 요소들의 타입에 따라 달라져요. 타입을 명시적으로 제어하려면 컴프리헨션 앞에 타입을 붙일 수 있어요. 예를 들어 결과를 단정밀도(single precision)로 요청할 수도 있었어요.

Float32[ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]

제너레이터 표현식 (Generator Expressions)

컴프리헨션은 감싸는 대괄호 없이도 쓸 수 있는데, 제너레이터(generator)로 알려진 객체를 만들어요. 이 객체는 배열을 할당하고 값을 미리 저장하는 대신(반복(Iteration) 참고) 요구에 따라 값을 생성해 순회할 수 있어요. 예를 들어 다음 표현식은 메모리를 할당하지 않고 급수를 합해요.

julia> sum(1/n^2 for n=1:1000)
1.6439345666815615

인자 목록 안에서 여러 차원을 가진 제너레이터 표현식을 쓸 때는 제너레이터를 이후 인자들과 구분하기 위해 괄호가 필요해요.

julia> map(tuple, 1/(i+j) for i=1:2, j=1:2, [1:4;])
ERROR: ParseError:
# Error @ none:1:44
map(tuple, 1/(i+j) for i=1:2, j=1:2, [1:4;])
#                                          └ ── invalid iteration spec: expected one of `=` `in` or `∈`

for 뒤의 쉼표로 구분된 모든 표현식은 범위로 해석돼요. 괄호를 추가하면 map에 세 번째 인자를 더할 수 있어요.

julia> map(tuple, (1/(i+j) for i=1:2, j=1:2), [1 3; 2 4])
2×2 Matrix{Tuple{Float64, Int64}}:
 (0.5, 1)       (0.333333, 3)
 (0.333333, 2)  (0.25, 4)

제너레이터는 내부 함수로 구현돼요. 언어의 다른 곳에서 쓰이는 내부 함수처럼, 감싸는 스코프의 변수를 내부 함수에 "포착(capture)"할 수 있어요. 예를 들어 sum(p[i] - q[i] for i=1:n)은 감싸는 스코프에서 p, q, n 세 변수를 포착해요. 포착된 변수는 성능 문제를 제시할 수 있어요. 성능 팁(performance tips)을 보세요.

제너레이터와 컴프리헨션의 범위는 for 키워드를 여러 개 써서 이전 범위에 의존할 수 있어요.

julia> [(i, j) for i=1:3 for j=1:i]
6-element Vector{Tuple{Int64, Int64}}:
 (1, 1)
 (2, 1)
 (2, 2)
 (3, 1)
 (3, 2)
 (3, 3)

이런 경우 결과는 항상 1차원이에요.

생성된 값은 if 키워드로 필터링할 수 있어요.

julia> [(i, j) for i=1:3 for j=1:i if i+j == 4]
2-element Vector{Tuple{Int64, Int64}}:
 (2, 2)
 (3, 1)

인덱싱 (Indexing)

n차원 배열 A에 인덱싱하는 일반 문법은 다음과 같아요.

X = A[I_1, I_2, ..., I_n]

여기서 각 I_k는 스칼라 정수, 정수 배열, 또는 다른 지원되는 인덱스일 수 있어요. 여기에는 전체 차원 내의 모든 인덱스를 선택하는 콜론(:), 인접하거나 스트라이드된 부분을 선택하는 a:ca:b:c 형태의 범위, 그리고 true인 인덱스에서 요소를 선택하는 불리언 배열이 포함돼요.

모든 인덱스가 스칼라이면 결과 X는 배열 A의 단일 요소예요. 그렇지 않으면 X는 모든 인덱스의 차원성의 합과 같은 차원 수를 가진 배열이에요.

예를 들어 모든 인덱스 I_k가 벡터라면 X의 형태는 (length(I_1), length(I_2), ..., length(I_n))이 되고, X의 위치 i_1, i_2, ..., i_nA[I_1[i_1], I_2[i_2], ..., I_n[i_n]] 값을 담아요.

예시:

julia> A = reshape(collect(1:16), (2, 2, 2, 2))
2×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
 1  3
 2  4

[:, :, 2, 1] =
 5  7
 6  8

[:, :, 1, 2] =
  9  11
 10  12

[:, :, 2, 2] =
 13  15
 14  16

julia> A[1, 2, 1, 1] # all scalar indices
3

julia> A[[1, 2], [1], [1, 2], [1]] # all vector indices
2×1×2×1 Array{Int64, 4}:
[:, :, 1, 1] =
 1
 2

[:, :, 2, 1] =
 5
 6

julia> A[[1, 2], [1], [1, 2], 1] # a mix of index types
2×1×2 Array{Int64, 3}:
[:, :, 1] =
 1
 2

[:, :, 2] =
 5
 6

마지막 두 경우에서 결과 배열의 크기가 어떻게 다른지 주목하세요.

I_1을 2차원 행렬로 바꾸면 X는 형태 (size(I_1, 1), size(I_1, 2), length(I_2), ..., length(I_n))의 n+1차원 배열이 돼요. 행렬이 차원을 하나 추가하죠.

예시:

julia> A = reshape(collect(1:16), (2, 2, 2, 2));

julia> A[[1 2; 1 2]]
2×2 Matrix{Int64}:
 1  2
 1  2

julia> A[[1 2; 1 2], 1, 2, 1]
2×2 Matrix{Int64}:
 5  6
 5  6

위치 i_1, i_2, i_3, ..., i_{n+1}A[I_1[i_1, i_2], I_2[i_3], ..., I_n[i_{n+1}]]의 값을 담아요. 스칼라로 인덱싱된 모든 차원은 빠져요. 예를 들어 J가 인덱스 배열이면 A[2, J, 3]의 결과는 크기 size(J)의 배열이고, 그 j번째 요소는 A[2, J[j], 3]으로 채워져요.

이 문법의 특별한 부분으로, end 키워드는 인덱싱되는 가장 안쪽 배열의 크기로 결정되는 인덱싱 괄호 안 각 차원의 마지막 인덱스를 나타내는 데 쓰일 수 있어요. end 키워드 없는 인덱싱 문법은 getindex 호출과 동등해요.

X = getindex(A, I_1, I_2, ..., I_n)

예시:

julia> x = reshape(1:16, 4, 4)
4×4 reshape(::UnitRange{Int64}, 4, 4) with eltype Int64:
 1  5   9  13
 2  6  10  14
 3  7  11  15
 4  8  12  16

julia> x[2:3, 2:end-1]
2×2 Matrix{Int64}:
 6  10
 7  11

julia> x[1, [2 3; 4 1]]
2×2 Matrix{Int64}:
  5  9
 13  1

인덱스 할당 (Indexed Assignment)

n차원 배열 A에 값을 할당하는 일반 문법은 다음과 같아요.

A[I_1, I_2, ..., I_n] = X

여기서 각 I_k는 스칼라 정수, 정수 배열, 또는 다른 지원되는 인덱스일 수 있어요. 여기에는 전체 차원 내의 모든 인덱스를 선택하는 콜론(:), 인접하거나 스트라이드된 부분을 선택하는 a:ca:b:c 형태의 범위, 그리고 true인 인덱스에서 요소를 선택하는 불리언 배열이 포함돼요.

모든 인덱스 I_k가 정수면 A의 위치 I_1, I_2, ..., I_n의 값이 X의 값으로 덮어써지며, 필요하면 A의 eltype으로 변환돼요.

어떤 인덱스 I_k가 그 자체로 배열이면, 오른쪽 XA[I_1, I_2, ..., I_n] 인덱싱의 결과와 같은 형태의 배열이거나 같은 수의 요소를 가진 벡터여야 해요. A의 위치 I_1[i_1], I_2[i_2], ..., I_n[i_n]의 값은 X[i_1, i_2, ..., i_n]의 값으로 덮어써지며, 필요하면 변환돼요. 요소별 할당 연산자 .=를 써서 X를 선택된 위치들에 브로드캐스트할 수 있어요.

A[I_1, I_2, ..., I_n] .= X

인덱싱(Indexing)에서와 마찬가지로, end 키워드는 할당되는 배열의 크기로 결정되는 인덱싱 괄호 안 각 차원의 마지막 인덱스를 나타내는 데 쓰일 수 있어요. end 키워드 없는 인덱스 할당 문법은 setindex! 호출과 동등해요.

setindex!(A, X, I_1, I_2, ..., I_n)

예시:

julia> x = collect(reshape(1:9, 3, 3))
3×3 Matrix{Int64}:
 1  4  7
 2  5  8
 3  6  9

julia> x[3, 3] = -9;

julia> x[1:2, 1:2] = [-1 -4; -2 -5];

julia> x
3×3 Matrix{Int64}:
 -1  -4   7
 -2  -5   8
  3   6  -9

지원되는 인덱스 타입 (Supported index types)

표현식 A[I_1, I_2, ..., I_n]에서 각 I_k는 스칼라 인덱스, 스칼라 인덱스의 배열, 또는 스칼라 인덱스 배열을 나타내고 to_indices로 그렇게 변환될 수 있는 객체일 수 있어요.

  • 스칼라 인덱스. 기본적으로 다음을 포함해요.
    • 비불리언 정수
    • 여러 차원에 걸친 정수 N-튜플처럼 동작하는 CartesianIndex{N} (자세한 내용은 아래 참고)
  • 스칼라 인덱스의 배열. 다음을 포함해요.
    • 정수의 벡터와 다차원 배열
    • 요소를 선택하지 않는 [] 같은 빈 배열. 예: A[[]] (A[]와 혼동하지 마세요)
    • a:c 또는 a:b:c 같은 범위. a에서 c(포함)까지 인접하거나 스트라이드된 부분을 선택
    • AbstractArray의 서브타입인 스칼라 인덱스의 임의의 커스텀 배열
    • CartesianIndex{N}의 배열 (자세한 내용은 아래 참고)
  • 스칼라 인덱스 배열을 나타내고 to_indices로 그렇게 변환될 수 있는 객체. 기본적으로 다음을 포함해요.
    • Colon() (:). 전체 차원 또는 전체 배열에 걸친 모든 인덱스를 나타냄
    • 불리언 배열. true인 인덱스에서 요소를 선택 (자세한 내용은 아래 참고)

몇 가지 예시:

julia> A = reshape(collect(1:2:18), (3, 3))
3×3 Matrix{Int64}:
 1   7  13
 3   9  15
 5  11  17

julia> A[4]
7

julia> A[[2, 5, 8]]
3-element Vector{Int64}:
  3
  9
 15

julia> A[[1 4; 3 8]]
2×2 Matrix{Int64}:
 1   7
 5  15

julia> A[[]]
Int64[]

julia> A[1:2:5]
3-element Vector{Int64}:
 1
 5
 9

julia> A[2, :]
3-element Vector{Int64}:
  3
  9
 15

julia> A[:, 3]
3-element Vector{Int64}:
 13
 15
 17

julia> A[:, 3:3]
3×1 Matrix{Int64}:
 13
 15
 17

데카르트 인덱스 (Cartesian indices)

특별한 CartesianIndex{N} 객체는 여러 차원에 걸친 정수 N-튜플처럼 동작하는 스칼라 인덱스를 나타내요. 예를 들어:

julia> A = reshape(1:32, 4, 4, 2);

julia> A[3, 2, 1]
7

julia> A[CartesianIndex(3, 2, 1)] == A[3, 2, 1] == 7
true

혼자서는 다소 사소해 보일 수 있어요. CartesianIndex는 단순히 여러 정수를 하나의 다차원 인덱스를 나타내는 단일 객체로 모을 뿐이니까요. 하지만 다른 인덱싱 형태 및 CartesianIndex를 산출하는 이터레이터와 결합하면, 매우 우아하고 효율적인 코드를 만들 수 있어요. 아래의 반복(Iteration)을 보고, 더 고급 예시는 다차원 알고리즘과 반복에 관한 이 블로그 포스트를 보세요.

CartesianIndex{N}의 배열도 지원돼요. 각각 N차원에 걸치는 스칼라 인덱스들의 컬렉션을 나타내서, 때로 **점별 인덱싱(pointwise indexing)**이라고 불리는 인덱싱 형태를 가능하게 해 줘요. 예를 들어 위 A의 첫 번째 "페이지"에서 대각선 요소들에 접근할 수 있게 해 줘요.

julia> page = A[:, :, 1]
4×4 Matrix{Int64}:
 1  5   9  13
 2  6  10  14
 3  7  11  15
 4  8  12  16

julia> page[[CartesianIndex(1, 1),
             CartesianIndex(2, 2),
             CartesianIndex(3, 3),
             CartesianIndex(4, 4)]]
4-element Vector{Int64}:
  1
  6
 11
 16

이것은 점 브로드캐스트(dot broadcasting)와 결합하고 일반 정수 인덱스와 결합하면 훨씬 더 간단하게 표현할 수 있어요 (A에서 첫 페이지를 별도 단계로 추출하는 대신). :와 결합해 두 페이지에서 두 대각선을 동시에 추출할 수도 있어요.

julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), 1]
4-element Vector{Int64}:
  1
  6
 11
 16

julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), :]
4×2 Matrix{Int64}:
  1  17
  6  22
 11  27
 16  32

경고: CartesianIndexCartesianIndex의 배열은 차원의 마지막 인덱스를 나타내는 end 키워드와 호환되지 않아요. CartesianIndex 또는 그 배열을 포함할 수 있는 인덱싱 표현식에서 end를 쓰지 마세요.

논리 인덱싱 (Logical indexing)

흔히 논리 인덱싱 또는 논리 마스크를 사용한 인덱싱이라고 불리는, 불리언 배열로 인덱싱하는 것은 값이 true인 인덱스에서 요소를 선택해요. 불리언 벡터 B로 인덱싱하는 것은 findall(B)가 반환하는 정수 벡터로 인덱싱하는 것과 실질적으로 같아요. 마찬가지로 N차원 불리언 배열로 인덱싱하는 것은 값이 trueCartesianIndex{N}의 벡터로 인덱싱하는 것과 실질적으로 같아요. 논리 인덱스는 인덱싱하는 차원(들)과 같은 형태의 배열이어야 하거나, 제공된 유일한 인덱스여야 하며 인덱싱하는 배열의 1차원으로 reshape된 뷰의 형태와 일치해야 해요. 불리언 배열을 인덱스로 직접 쓰는 것이 먼저 findall을 호출하는 것보다 일반적으로 더 효율적이에요.

julia> x = reshape(1:12, 2, 3, 2)
2×3×2 reshape(::UnitRange{Int64}, 2, 3, 2) with eltype Int64:
[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

julia> x[:, [true false; false true; true false]]
2×3 Matrix{Int64}:
 1  5   9
 2  6  10

julia> mask = map(ispow2, x)
2×3×2 Array{Bool, 3}:
[:, :, 1] =
 1  0  0
 1  1  0

[:, :, 2] =
 0  0  0
 1  0  0

julia> x[mask]
4-element Vector{Int64}:
 1
 2
 4
 8

julia> x[vec(mask)] == x[mask] # we can also index with a single Boolean vector
true

인덱스의 개수 (Number of indices)

데카르트 인덱싱 (Cartesian indexing)

N차원 배열에 인덱싱하는 보통의 방법은 정확히 N개의 인덱스를 쓰는 것이에요. 각 인덱스는 그 특정 차원에서 위치(들)를 선택해요. 예를 들어 3차원 배열 A = rand(4, 3, 2)에서 A[2, 3, 1]은 배열의 첫 번째 "페이지"에서 세 번째 열의 두 번째 행에 있는 수를 선택해요. 이는 흔히 **데카르트 인덱싱(cartesian indexing)**이라고 불려요.

선형 인덱싱 (Linear indexing)

정확히 하나의 인덱스 i가 주어지면, 그 인덱스는 더 이상 배열의 특정 차원에서 위치를 나타내지 않아요. 대신, 전체 배열을 선형으로 가로지르는 열 우선(column-major) 순회 순서를 사용해 i번째 요소를 선택해요. 이것을 **선형 인덱싱(linear indexing)**이라고 해요. 본질적으로 배열이 vec로 1차원 벡터로 reshape된 것처럼 취급해요.

julia> A = [2 6; 4 7; 3 1]
3×2 Matrix{Int64}:
 2  6
 4  7
 3  1

julia> A[5]
7

julia> vec(A)[5]
7

배열 A에 대한 선형 인덱스는 CartesianIndices(A)[i]로 데카르트 인덱싱용 CartesianIndex로 변환할 수 있고 (CartesianIndices 참고), N개의 데카르트 인덱스 집합은 LinearIndices(A)[i_1, i_2, ..., i_N]로 선형 인덱스로 변환할 수 있어요 (LinearIndices 참고).

julia> CartesianIndices(A)[5]
CartesianIndex(2, 2)

julia> LinearIndices(A)[2, 2]
5

이 변환들의 성능에는 매우 큰 비대칭성이 있다는 점을 주목하는 것이 중요해요. 선형 인덱스를 데카르트 인덱스 집합으로 변환하려면 나눗셈과 나머지가 필요하고, 반대 방향은 곱셈과 덧셈뿐이에요. 현대 프로세서에서 정수 나눗셈은 곱셈보다 10~50배 느릴 수 있어요. Array 자체처럼 일부 배열은 선형 메모리 청크로 구현되고 구현에서 직접 선형 인덱스를 쓰지만, Diagonal 같은 다른 배열은 조회에 전체 데카르트 인덱스 집합이 필요해요 (어느 쪽인지 살펴보려면 IndexStyle 참고).

경고: 배열의 모든 인덱스를 순회할 때는 1:length(A)보다 eachindex(A)로 순회하는 것이 더 좋아요. AIndexCartesian인 경우 더 빠를 뿐만 아니라, OffsetArrays 같은 커스텀 인덱스를 가진 배열도 지원해요. 값만 필요하다면 배열을 직접, 즉 for a in A로 순회하는 것이 더 좋아요.

생략된 인덱스와 추가 인덱스 (Omitted and extra indices)

선형 인덱싱 외에도 N차원 배열은 특정 상황에서 N개보다 적거나 많은 인덱스로 인덱싱될 수 있어요.

인덱싱되지 않는 꼬리(trailing) 차원들이 모두 길이 1이면 인덱스가 생략될 수 있어요. 다시 말해, 생략된 인덱스들이 in-bounds 인덱싱 표현식에 가질 수 있는 가능한 값이 하나뿐일 때만 꼬리 인덱스를 생략할 수 있어요. 예를 들어 크기 (3, 4, 2, 1)인 4차원 배열은, 건너뛰는 차원(네 번째 차원)의 길이가 1이므로 세 개의 인덱스로만 인덱싱될 수 있어요. 선형 인덱싱은 이 규칙보다 우선한다는 점에 주의하세요.

julia> A = reshape(1:24, 3, 4, 2, 1)
3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64:
[:, :, 1, 1] =
 1  4  7  10
 2  5  8  11
 3  6  9  12

[:, :, 2, 1] =
 13  16  19  22
 14  17  20  23
 15  18  21  24

julia> A[1, 3, 2] # Omits the fourth dimension (length 1)
19

julia> A[1, 3] # Attempts to omit dimensions 3 & 4 (lengths 2 and 1)
ERROR: BoundsError: attempt to access 3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64 at index [1, 3]

julia> A[19] # Linear indexing
19

A[]로 모든 인덱스를 생략하면, 이 의미는 배열에서 유일한 요소를 검색하면서 동시에 요소가 하나뿐임을 보장하는 간단한 관용구를 제공해요.

마찬가지로, 배열의 차원성을 넘어선 모든 인덱스가 1이면 (또는 더 일반적으로 axes(A, d)의 첫 번째이자 유일한 요소이면, 여기서 d는 그 특정 차원 번호) N개보다 많은 인덱스가 제공될 수 있어요. 이는 예를 들어 벡터를 1열 행렬처럼 인덱싱할 수 있게 해 줘요.

julia> A = [8, 6, 7]
3-element Vector{Int64}:
 8
 6
 7

julia> A[2, 1]
6

반복 (Iteration)

전체 배열을 순회하는 권장 방법은 다음과 같아요.

for a in A
    # Do something with the element a
end

for i in eachindex(A)
    # Do something with i and/or A[i]
end

첫 번째 구조는 각 요소의 값을 필요로 하지만 인덱스는 필요 없을 때 써요. 두 번째 구조에서 iA가 빠른 선형 인덱싱을 가진 배열 타입이면 Int가 되고, 그렇지 않으면 CartesianIndex가 돼요.

julia> A = rand(4, 3);

julia> B = view(A, 1:3, 2:3);

julia> for i in eachindex(B)
           @show i
       end
i = CartesianIndex(1, 1)
i = CartesianIndex(2, 1)
i = CartesianIndex(3, 1)
i = CartesianIndex(1, 2)
i = CartesianIndex(2, 2)
i = CartesianIndex(3, 2)

참고: for i = 1:length(A)와 대조적으로, eachindex로 순회하는 것은 어떤 배열 타입이든 효율적으로 순회하는 방법을 제공해요. 게다가 OffsetArrays 같은 커스텀 인덱스를 가진 제너릭 배열도 지원해요.

배열 특성 (Array traits)

커스텀 AbstractArray 타입을 작성한다면, 다음으로 빠른 선형 인덱싱을 가진다고 지정할 수 있어요.

Base.IndexStyle(::Type{<:MyArray}) = IndexLinear()

이 설정은 MyArray에 대한 eachindex 순회가 정수를 사용하게 해요. 이 특성을 지정하지 않으면 기본값 IndexCartesian()이 사용돼요.

배열·벡터화 연산자와 함수 (Array and Vectorized Operators and Functions)

배열에 대해 다음 연산자가 지원돼요.

  • 단항 산술 – -, +
  • 이항 산술 – -, +, *, /, \, ^
  • 비교 – ==, !=, (isapprox),

수학 및 기타 연산의 편리한 벡터화를 가능하게 하기 위해, 줄리아는 점 문법 f.(args...)를 제공해요. 예: sin.(x) 또는 min.(x, y). 이것은 배열 또는 배열과 스칼라의 혼합에 대한 요소별 연산(브로드캐스팅(Broadcasting) 연산)이며, 다른 점 호출과 결합하면 "융합(fusing)"되어 단일 루프가 된다는 추가 이점이 있어요. 예: sin.(cos.(x)).

또한 모든 이항 연산자는 그런 융합 브로드캐스팅 연산에서 배열(과 배열·스칼라 조합)에 적용할 수 있는 점 버전을 지원해요. 예: z .== sin.(x .* y).

== 같은 비교는 전체 배열에 대해 동작해 단일 불리언 답을 준다는 점에 주의하세요. 요소별 비교에는 .== 같은 점 연산자를 쓰세요. (< 같은 비교 연산의 경우 배열에는 요소별 . < 버전만 적용 가능해요.)

max.(a,b)(a와 b에 대해 max를 요소별 브로드캐스트)와 maximum(a)(a 안에서 가장 큰 값을 찾음)의 차이도 주목하세요. 같은 관계가 min.(a, b)minimum(a)에도 성립해요.

브로드캐스팅 (Broadcasting)

때로는 서로 다른 크기의 배열에 대해 요소별 이항 연산을 수행하는 것이 유용해요. 행렬의 각 열에 벡터를 더하는 것 같은 경우죠. 이를 하는 비효율적인 방법은 벡터를 행렬 크기로 복제하는 것이에요.

julia> a = [0.2, 0.5]; A = [1.0 1.6 1.05; 1.07 1.36 1.18];

julia> repeat(a, 1, 3) + A
2×3 Matrix{Float64}:
 1.2   1.8   1.25
 1.57  1.86  1.68

차원이 커지면 이는 낭비적이라, 줄리아는 broadcast를 제공해요. 이것은 배열 인자의 단일 차원을 다른 배열의 대응하는 차원과 일치시키도록 확장하되 추가 메모리를 쓰지 않고, 주어진 함수를 요소별로 적용해요.

julia> broadcast(+, a, A)
2×3 Matrix{Float64}:
 1.2   1.8   1.25
 1.57  1.86  1.68

julia> b = [0.9 0.1]
1×2 Matrix{Float64}:
 0.9  0.1

julia> broadcast(+, a, b)
2×2 Matrix{Float64}:
 1.1  0.3
 1.4  0.6

.+.* 같은 점 연산자는 broadcast 호출과 동등해요 (단, 위에서 설명한 대로 융합된다는 점 빼고). 명시적 목적지를 지정하는 broadcast! 함수도 있어요. (또한 .= 할당으로 융합 방식으로 접근할 수 있어요.) 실제로 f.(args...)broadcast(f, args...)와 동등해서, 어떤 함수든 브로드캐스트하는 편리한 문법(점 문법)을 제공해요. 중첩된 "점 호출" f.(...) (.+ 등에 대한 호출 포함)은 자동으로 단일 broadcast 호출로 융합돼요.

추가로 broadcast는 배열에 국한되지 않아요 (함수 문서 참고). 스칼라, 튜플, 다른 컬렉션도 처리해요. 기본적으로 일부 인자 타입만 스칼라로 간주되는데, 여기에는 (이에 국한되지 않지만) Numbers, Strings, Symbols, Types, Functions와 missing, nothing 같은 몇몇 흔한 싱글턴이 포함돼요. 다른 모든 인자는 요소별로 순회하거나 인덱싱돼요.

julia> convert.(Float32, [1, 2])
2-element Vector{Float32}:
 1.0
 2.0

julia> ceil.(UInt8, [1.2 3.4; 5.6 6.7])
2×2 Matrix{UInt8}:
 0x02  0x04
 0x06  0x07

julia> string.(1:3, ". ", ["First", "Second", "Third"])
3-element Vector{String}:
 "1. First"
 "2. Second"
 "3. Third"

때로는 브로드캐스트에 평소 참여할 컨테이너(배열 같은)를 브로드캐스트의 모든 요소를 순회하는 동작으로부터 "보호"하고 싶을 수 있어요. 그것을 다른 컨테이너(단일 요소 튜플 같은) 안에 두면 브로드캐스트가 그것을 단일 값으로 취급해요.

julia> ([1, 2, 3], [4, 5, 6]) .+ ([1, 2, 3],)
([2, 4, 6], [5, 7, 9])

julia> ([1, 2, 3], [4, 5, 6]) .+ tuple([1, 2, 3])
([2, 4, 6], [5, 7, 9])

구현 (Implementation)

줄리아의 기본 배열 타입은 추상 타입 AbstractArray{T,N}이에요. 그것은 차원 수 N과 요소 타입 T로 매개변수화돼요. AbstractVectorAbstractMatrix는 각각 1차원과 2차원 경우의 별칭이에요. AbstractArray 객체에 대한 연산은 더 높은 수준의 연산자와 함수를 사용해 정의되는데, 밑에 있는 저장소와 무관한 방식이에요. 이 연산들은 일반적으로 특정 배열 구현에 대한 폴백으로 올바르게 동작해요.

AbstractArray 타입은 대략 배열 같은 모든 것을 포함하며, 그 구현은 전통적인 배열과 아주 다를 수 있어요. 예를 들어 요소가 저장되는 대신 요구에 따라 계산될 수도 있어요. 하지만 구체적인 AbstractArray{T,N} 타입은 일반적으로 최소한 size(A) (Int 튜플 반환), getindex(A, i), getindex(A, i1, ..., iN)을 구현해야 하고, 가변 배열은 setindex!도 구현해야 해요. 이 연산들이 거의 상수 시간 복잡도를 가지는 것이 권장되는데, 그렇지 않으면 일부 배열 함수가 예상외로 느릴 수 있어서요. 구체 타입은 보통 similar(A, T=eltype(A), dims=size(A)) 메서드도 제공해야 하는데, 그것은 copy 및 그 밖의 out-of-place 연산을 위한 유사한 배열을 할당하는 데 쓰여요. AbstractArray{T,N}이 내부적으로 어떻게 표현되든, T는 정수 인덱싱(A가 비어 있지 않을 때 A[1, ..., 1])이 반환하는 객체의 타입이고, Nsize가 반환하는 튜플의 길이여야 해요. 커스텀 AbstractArray 구현 정의에 대한 자세한 내용은 인터페이스 장의 배열 인터페이스 가이드를 보세요.

DenseArray는 요소가 열 우선 순서로 연속 저장되는 모든 배열을 포함하려는 AbstractArray의 추상 서브타입이에요 (성능 팁(Performance Tips)의 추가 주석 참고). Array 타입은 DenseArray의 특정 인스턴스이에요. VectorMatrix는 각각 1차원과 2차원 경우의 별칭이에요. 모든 AbstractArray에 필요한 것 너머로 Array에 특화해 구현된 연산은 극히 드물어요. 배열 라이브러리의 상당 부분은 모든 커스텀 배열이 유사하게 동작하게 하는 일반적인 방식으로 구현돼요.

SubArray는 복사하지 않고 원래 배열과 메모리를 공유해 인덱싱을 수행하는 AbstractArray의 특수화예요. SubArraygetindex와 같은 방식으로(배열과 일련의 인덱스 인자로) 호출되는 view 함수로 만들어져요. view의 결과는 데이터가 제자리에 남아 있다는 점을 빼면 getindex의 결과와 같아 보여요. view는 입력 인덱스 벡터를 SubArray 객체에 저장하고, 그것은 나중에 원래 배열을 간접적으로 인덱싱하는 데 쓰일 수 있어요. @views 매크로를 표현식이나 코드 블록 앞에 두면, 그 표현식에서 array[...] 슬라이스가 SubArray 뷰를 만들도록 변환돼요.

BitArray는 공간 효율적인 "압축된(packed)" 불리언 배열로, 불리언 값당 1비트를 저장해요. 그것은 Array{Bool} 배열(불리언 값당 1바이트 저장)과 유사하게 쓸 수 있고, 각각 Array(bitarray)BitArray(array)로 후자로/부터 변환할 수 있어요.

배열은 요소 사이에 잘 정의된 간격(스트라이드)으로 메모리에 저장되면 "스트라이드(strided)"돼요. 지원되는 요소 타입을 가진 스트라이드 배열은 그 포인터와 각 차원의 스트라이드를 넘기기만 하면 BLAS나 LAPACK 같은 외부(비줄리아) 라이브러리에 전달할 수 있어요. stride(A, d)는 차원 d를 따른 요소 사이의 거리예요. 예를 들어 rand(5,7,2)가 반환하는 내장 Array는 요소가 열 우선 순서로 연속 배열되어 있어요. 이는 첫 번째 차원의 스트라이드 — 같은 열 안 요소 사이의 간격 — 가 1이라는 뜻이에요.

julia> A = rand(5, 7, 2);

julia> stride(A, 1)
1

두 번째 차원의 스트라이드는 같은 행 안 요소 사이의 간격으로, 단일 열에 있는 요소 수(5)만큼 건너뛰어요. 마찬가지로 두 "페이지"(세 번째 차원) 사이를 뛰려면 5*7 == 35개 요소를 건너뛰어야 해요. 이 배열의 스트라이드는 이 세 수를 함께 담은 튜플이에요.

julia> strides(A)
(1, 5, 35)

이 특정 경우에는 메모리에서 건너뛰는 요소 수가 건너뛰는 선형 인덱스 수와 일치해요. 이것은 Array(및 다른 DenseArray 서브타입) 같은 연속 배열에만 해당하며, 일반적으로 사실이 아니에요. 범위 인덱스를 가진 뷰가 비연속 스트라이드 배열의 좋은 예시예요. V = @view A[1:3:4, 2:2:6, 2:-1:1]을 생각해 보세요. 이 뷰 VA와 같은 메모리를 가리키지만 그 요소 중 일부를 건너뛰고 재배열해요. V의 첫 번째 차원 스트라이드는 3인데, 원래 배열에서 매 3번째 행만 선택하니까요.

julia> V = @view A[1:3:4, 2:2:6, 2:-1:1];

julia> stride(V, 1)
3

이 뷰는 마찬가지로 원래 A에서 매 두 번째 열을 선택해요. 따라서 두 번째 차원에서 인덱스 사이를 이동할 때 5개 요소 열 두 개에 해당하는 만큼 건너뛰어야 해요.

julia> stride(V, 2)
10

세 번째 차원은 순서가 뒤집혀서 흥미로워요! 첫 번째 "페이지"에서 두 번째로 가려면 메모리에서 뒤로 가야 하므로, 이 차원의 스트라이드는 음수예요!

julia> stride(V, 3)
-35

이는 V의 포인터가 실제로 A의 메모리 블록 중간을 가리키고, 메모리에서 앞뒤 양방향의 요소를 가리킨다는 뜻이에요. 나만의 스트라이드 배열 정의에 대한 자세한 내용은 스트라이드 배열에 대한 인터페이스 가이드를 보세요. StridedVectorStridedMatrix는 스트라이드 배열로 간주되는 많은 내장 배열 타입의 편리한 별칭이어서, 포인터와 스트라이드만 사용해 고도로 튜닝되고 최적화된 BLAS와 LAPACK 함수를 호출하는 선택된 특수 구현으로 디스패치할 수 있게 해 줘요.

스트라이드는 인덱싱이 아니라 메모리에서의 오프셋에 관한 것임을 강조할 가치가 있어요. 선형(단일 인덱스) 인덱싱과 데카르트(다중 인덱스) 인덱싱 사이를 변환하고 싶다면 LinearIndicesCartesianIndices를 보세요.

  • [1] iid, 독립적이고 동일하게 분포됨(identically and independently distributed).

더 알아보기 (Learn more)

배열을 이해하는 핵심은 줄리아가 벡터화에 기대지 않고 스칼라 인덱싱으로도 빠른 성능을 낸다는 점, 그리고 ! 접미사가 변형을 뜻한다는 관례를 파악하는 거예요. 리터럴·컴프리헨션·cat을 통한 생성, 콜론과 범위·불리언·CartesianIndex를 아우르는 인덱싱, broadcast의 융합 동작까지 코드로 직접 확인해 보면 자연스럽게 정리돼요.