多次元配列
他の多くの技術計算言語と同様に、Julia はファーストクラスの配列実装を提供します。たいていの技術計算言語では配列の実装に多くの手間が割かれ、他のコンテナが割を食っているのが実情です。しかし Julia は配列を特別扱いしません。配列ライブラリはほぼ全てが Julia 自身で書かれており、その性能は他の Julia コードと同様コンパイラに由来します。このため AbstractArray
を継承する独自の配列型を定義することもできます。独自の配列型を実装する方法について詳しくは AbstractArray
インターフェースのマニュアルを参照してください。
配列は多次元の格子に保存されたオブジェクトの集合です。最も一般的な場合には、配列の要素は Any
型の任意のオブジェクトとなります。ただ計算用途では多くの場合、配列は Float64
や Int32
といったもっと特定的な型のオブジェクトを持つはずです。
他の多くの技術計算言語とは異なり、一般に Julia では性能を向上させるためにプログラムをベクトル化された形で書くことはありません。Julia のコンパイラは型推論を使ってスカラー配列の添え字アクセスに対して最適化されたコードを生成するので、性能を犠牲にせずに分かりやすく読みやすいプログラムを書くことができます。さらにプログラムのメモリ使用量を節約できることもあります。
Julia で関数の引数は全て共有による受け渡し (pass by sharing) でやり取りされます (つまりポインタが渡されます)。配列を値渡しする技術計算言語もあり、そうすると呼び出された関数が呼び出し側の値を誤って書き換えることを防げますが、配列の不必要なコピーを避けるのが難しくなります。Julia では、引数を改変・破壊する関数には名前の後ろに !
を付けることが慣習となっています (例えば sort
と sort!
を比べてみてください)。入力を変更せずにこういった関数を使いたいときは、呼び出し側による明示的なコピーが必要です。改変を行わないバージョンの関数の多くは、入力を明示的にコピーして !
が付いたバージョンの関数を呼び、そのコピーを返すという形で実装されています。
基本的な関数
関数 | 説明 |
---|---|
eltype(A)
|
A に含まれる要素の型 |
length(A)
|
A に含まれる要素の個数 |
ndims(A)
|
A の次元数 |
size(A)
|
A の各次元のサイズからなるタプル |
size(A,n)
|
A の n 番目の次元のサイズ |
axes(A)
|
A の正当な添え字からなるタプル |
axes(A,n)
|
A の n 番目の次元の正当な添え字を表す範囲 |
eachindex(A)
|
A の各要素を訪れる高性能な反復子 |
stride(A,k)
|
A の k 番目の次元の歩長 (隣接する要素間の距離を添え字の個数で表した値) |
strides(A)
|
A の各次元の歩長からなるタプル |
構築と初期化
配列を構築・初期化するための関数が数多く提供されます。次の表に示す関数で dims...
は次元のサイズを表す一つのタプル、もしくは次元のサイズを表す可変個の引数を表します。多くの関数は第一引数に配列の要素型 T
を受け取りますが、T
は省略しても構いません。T
を省略したときのデフォルト値は Float64
です。
関数 | 説明 |
---|---|
Array{T}(undef, dims...)
|
未初期化の密配列 (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 の要素型と A の次元) |
reinterpret(T, A)
|
A と同一のバイナリデータを持つ要素型 T の配列 |
rand(T, dims...)
|
各要素が独立同一に半開区間 [0,1) 上の一様分布に従う Array |
randn(T, dims...)
|
各要素が独立同一に標準正規分布に従う Array |
Matrix{T}(I, m, n)
|
m × n の単位行列 (I を使うには using LinearAlgebra が必要)
|
range(start, stop, length)
|
start から stop まで線形に配置された n 個の要素からなる区間 |
fill!(A, x)
|
配列 A を値 x で埋める |
fill(x, dims...)
|
値 x で埋まった Array |
これらの関数で異なる方法を使って次元を指定する例を示します:
julia> zeros(Int8, 2, 3)
2×3 Array{Int8,2}:
0 0 0
0 0 0
julia> zeros(Int8, (2, 3))
2×3 Array{Int8,2}:
0 0 0
0 0 0
julia> zeros((2, 3))
2×3 Array{Float64,2}:
0.0 0.0 0.0
0.0 0.0 0.0
ここで (2, 3)
は Tuple
型の値です。要素型を表す第一引数は省略可能で、省略すると Float64
になります。
配列リテラル
配列は角括弧を使って直接構築できます。[A, B, C, ...]
という配列リテラル構文は、コンマで区切られた引数を要素とする一次元の配列 (ベクトル) を作成します。
配列リテラルが作成する配列の要素型 (eltype
) は括弧に囲まれる要素の型から次の手順で自動的に決定されます。まず、引数が全て同じ型ならそれが eltype
となります。それ以外のとき、共通の昇格先があればそれが要素型になり、各要素は convert
で変換されます。このいずれでもないとき、どんな型の値でも保持できる不均一な配列 Vector{Any}
が構築されます ──引数の無い配列リテラル []
も Vector{Any}
型となります。
julia> [1,2,3] # Int の配列
3-element Array{Int64,1}:
1
2
3
julia> # Int, Float64, Rational の組み合わせは Float64 に昇格される...
promote(1, 2.3, 4//5)
(1.0, 2.3, 0.8)
julia> [1, 2.3, 4//5] # ...そのため、この配列の要素型は Float64 となる
3-element Array{Float64,1}:
1.0
2.3
0.8
julia> []
Any[]
連結
角括弧に含まれる引数が (コンマではなく) セミコロン (;
) あるいは改行で区切られる場合には、各要素は (配列の要素となるのではなく) 垂直に連結されます。
julia> [1:2, 4:5] # コンマがあるので連結は起きない。区間がそのまま要素となる。
2-element Array{UnitRange{Int64},1}:
1:2
4:5
julia> [1:2; 4:5]
4-element Array{Int64,1}:
1
2
4
5
julia> [1:2
4:5
6]
5-element Array{Int64,1}:
1
2
4
5
6
同様に配列リテラルの引数をタブまたはスペースで区切ると、各要素は水平に連結されます:
julia> [1:2 4:5 7:8]
2×3 Array{Int64,2}:
1 4 7
2 5 8
julia> [[1,2] [4,5] [7,8]]
2×3 Array{Int64,2}:
1 4 7
2 5 8
julia> [1 2 3] # 数値を水平に連結することもできる。
1×3 Array{Int64,2}:
1 2 3
セミコロン・改行とスペース・タブを組み合わせれば、水平および垂直にまとめて連結できます:
julia> [1 2
3 4]
2×2 Array{Int64,2}:
1 2
3 4
julia> [zeros(Int, 2, 2) [1; 2]
[3 4] 5]
3×3 Array{Int64,2}:
0 0 1
0 0 2
3 4 5
より一般的に言うと、連結は cat
関数が行います。配列リテラルの連結構文は関数呼び出しの省略形に過ぎません。構文と関数の対応を示します:
構文 | 関数 | 説明 |
---|---|---|
(存在しない) |
cat
|
k 番目の次元に関する連結 |
[A; B; C; ...] |
vcat
|
cat(A...; dims=1) の省略形。 |
[A B C ...] |
hcat
|
cat(A...; dims=2) の省略形。 |
[A B; C D; ...] |
hvcat
|
垂直方向と水平方向の同時連結 |
型付き配列リテラル
特定の要素型を持つ配列は型付き配列リテラル T[A, B, C, ...]
という構文で構築できます。この構文は A
, B
, C
を要素に持つ要素型が T
の一次元配列を構築し、例えば Any[x, y, z]
とすれば任意の値を持てる不均一配列が構築されます。
同様に連結構文の先頭に型を付ければ、要素型を指定できます:
julia> [[1 2] [3 4]]
1×4 Array{Int64,2}:
1 2 3 4
julia> Int8[[1 2] [3 4]]
1×4 Array{Int8,2}:
1 2 3 4
内包表記
内包表記 (comprehension) は配列を構築する一般的で強力な方法を提供します。内包表記の構文は数学における集合の表記に似ています:
A = [ F(x,y,...) for x=rx, y=ry, ... ]
これは rx
, ry
, ... に含まれる値 x
, y
, ... の組み合わせそれぞれに対する F(x,y,...)
の評価結果からなる配列を意味します。値の指定には任意の反復可能オブジェクトが利用できますが、通常は 1:n
, 2:(n-1)
のような区間や、[1.2, 3.4, 5.7]
のような値を明示的に示した配列を使います。内包表記が返すのは N 次元の密配列です。ここで N は rx
, ry
, ... の個数に等しく、各次元の大きさは rx
, ry
, ... が返す値の個数に等しく、各要素が F(x,y,...)
となります。
一次元格子の各点に対して、その点と両隣の点の重み付き平均を計算する例を示します:
julia> x = rand(8)
8-element Array{Float64,1}:
0.843025
0.869052
0.365105
0.699456
0.977653
0.994953
0.41084
0.809411
julia> [ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
6-element Array{Float64,1}:
0.736559
0.57468
0.685417
0.912429
0.8446
0.656511
内包表記が返す配列の型は計算された要素から配列リテラルと同様に定まります。内包表記の前に型を付ければ、評価結果の配列の型を明示的に指定できます。例えば、一つ前の例で結果を単精度とするには次のようにします:
Float32[ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
ジェネレータ式
内包表記の角括弧を書かないと、ジェネレータ (generator) と呼ばれるオブジェクトが生成されます。このオブジェクトを反復すると必要になったときに必要な分だけ値が生成され、事前に配列をアロケートして全ての値を計算することはありません (参照: 反復)。例えば、次の式はメモリをアロケートせずに数列の和を計算します:
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: syntax: invalid iteration specification
for
の後のコンマで区切られた式が全てジェネレータ式の一部と解釈されるためです。括弧を加えれば map
の第三引数となります:
julia> map(tuple, (1/(i+j) for i=1:2, j=1:2), [1 3; 2 4])
2×2 Array{Tuple{Float64,Int64},2}:
(0.5, 1) (0.333333, 3)
(0.333333, 2) (0.25, 4)
ジェネレータは内部関数を使って実装されます。Julia の他の部分で使われる内部関数と同様に、ジェネレータで使われる内部関数は現在のスコープに含まれる変数を "キャプチャ" できます。例えば sum(p[i] - q[i] for i=1:n)
は p
, q
, n
を周りのスコープからキャプチャします。キャプチャされた変数が原因で性能が落ちる場合があります: パフォーマンス Tips を参照してください。
ジェネレータと内包表記で区間が前に宣言された変数に依存するときは、for
を重ねて書きます:
julia> [(i,j) for i=1:3 for j=1:i]
6-element Array{Tuple{Int64,Int64},1}:
(1, 1)
(2, 1)
(2, 2)
(3, 1)
(3, 2)
(3, 3)
このとき結果は必ず一次元になります。
if
キーワードを使うと生成された値をフィルタリングできます:
julia> [(i,j) for i=1:3 for j=1:i if i+j == 4]
2-element Array{Tuple{Int64,Int64},1}:
(2, 2)
(3, 1)
添え字アクセスの構文
n
次元配列 A
に対する添え字アクセスの一般的な構文は次の通りです:
X = A[I_1, I_2, ..., I_n]
I_k
には整数スカラーおよび整数の配列、そしてサポートされる添え字が利用できます。サポートされる添え字には例えば次元全体を選択するコロン :
, 連続する区間および有歩長区間を表す a:c
と a:b:c
, そして true
となる添え字を選択する真偽値配列があります。
添え字が全てスカラーなら、添え字アクセスの返り値 X
は配列 A
の単一要素となります。それ以外の場合 X
の次元は添え字の次元を組み合わせたタプルと一致します。
例えば添え字 I_k
が全てベクトルであれば、添え字アクセスの返り値 X
の形状は (length(I_1), length(I_2), ..., length(I_n))
となり、X
の位置 i_1, i_2, ..., i_n
には A[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] # 添え字が全てスカラー
3
julia> A[[1, 2], [1], [1, 2], [1]] # 添え字が全てベクトル
2×1×2×1 Array{Int64,4}:
[:, :, 1, 1] =
1
2
[:, :, 2, 1] =
5
6
julia> A[[1, 2], [1], [1, 2], 1] # スカラーとベクトルが混ざった添え字
2×1×2 Array{Int64,3}:
[:, :, 1] =
1
2
[:, :, 2] =
5
6
最後の二つの実行例で返り値のサイズが異なることに注目してください。
I_1
を二次元の行列に変えると、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 Array{Int64,2}:
1 2
1 2
julia> A[[1 2; 1 2], 1, 2, 1]
2×2 Array{Int64,2}:
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
の値は最も内側の配列のサイズから決定されます。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 Array{Int64,2}:
6 10
7 11
julia> x[1, [2 3; 4 1]]
2×2 Array{Int64,2}:
5 9
13 1
添え字代入
n
次元配列 A
に値を代入する一般的な構文は次の通りです:
A[I_1, I_2, ..., I_n] = X
ここで I_k
には整数スカラーおよび整数配列、そしてサポートされる添え字が利用できます。サポートされる添え字には例えば次元全体を選択するコロン :
, 連続する区間および有歩長区間を表す a:c
と a:b:c
, そして true
となる添え字を選択する真偽値配列があります。
添え字 I_k
が全て整数なら、A
の位置 I_1, I_2, ..., I_n
に X
が代入されます。そのとき必要であれば A
の eltype
に対する convert
が発生します。
添え字 I_k
のいずれが一つ以上の位置を選択するなら、右辺 X
は A[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
添え字アクセスと同様に、角括弧の中では end
キーワードで次元の最後の添え字を表せます。end
の値は代入を受ける配列 A
のサイズによって決まります。end
キーワードを持たない添え字代入の構文は次の setindex!
の呼び出しと等価です:
setindex!(A, X, I_1, I_2, ..., I_n)
例を示します:
julia> x = collect(reshape(1:9, 3, 3))
3×3 Array{Int64,2}:
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 Array{Int64,2}:
-1 -4 7
-2 -5 8
3 6 -9
サポートされる添え字の型
式 A[I_1, I_2, ..., I_n]
において、各 I_k
に使えるのはスカラーの添え字とスカラーの添え字の配列、そしてスカラーの添え字の配列に to_indices
で変換できるオブジェクトです:
- スカラーの添え字:
- 真偽値を除く整数
-
CartesianIndex(N)
型の値: 複数の次元にまたがる整数N
-タプルのように振る舞う (後述)。
- スカラーの添え字の配列:
- 整数のベクトルおよび多次元配列
- 空の配列
[]
: 要素を一つも選択しない。 a:c
やa:b:c
のような区間:a
からb
まで (両端含む) の連続する区間あるいは有歩長区間を表す。- スカラーの添え字の独自配列 (
AbstractArray
の部分型) CartesianIndex{N}
の配列 (後述)
-
to_indices
でスカラーの添え字の配列に変換できるオブジェクト-
コロン (
:
): 次元または配列全体の添え字を表す。 - 真偽値配列:
true
である添え字を選択する (後述)。
-
コロン (
いくつか例を示します:
julia> A = reshape(collect(1:2:18), (3, 3))
3×3 Array{Int64,2}:
1 7 13
3 9 15
5 11 17
julia> A[4]
7
julia> A[[2, 5, 8]]
3-element Array{Int64,1}:
3
9
15
julia> A[[1 4; 3 8]]
2×2 Array{Int64,2}:
1 7
5 15
julia> A[[]]
Int64[]
julia> A[1:2:5]
3-element Array{Int64,1}:
1
5
9
julia> A[2, :]
3-element Array{Int64,1}:
3
9
15
julia> A[:, 3]
3-element Array{Int64,1}:
13
15
17
格子添え字
特殊な 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
を使って非常に綺麗で性能にも優れるコードを書けます。以下の反復の節や、多次元のアルゴリズムと反復に関するブログ記事 Multidimensional algorithms and iteration を参照してください。
CartesianIndex{N}
の配列もサポートされます。この配列は要素に N
次元にまたがるスカラーの添え字を持ち、点ごとの添え字アクセス (pointwise indexing) などと呼ばれるアクセスを可能にします。例えば CartesianIndex{N}
の配列を使うと、上記の配列 A
の最初の "ページ" が持つ対角要素にアクセスできます:
julia> page = A[:,:,1]
4×4 Array{Int64,2}:
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 Array{Int64,1}:
1
6
11
16
さらにドットによるブロードキャストを使い、(A
から page
を切り出すことなく) 通常の整数添え字を組み合わせると、この処理はずっと簡単に書けます。さらに :
を使えば二つのページの対角要素を取り出すこともできます:
julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), 1]
4-element Array{Int64,1}:
1
6
11
16
julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), :]
4×2 Array{Int64,2}:
1 17
6 22
11 27
16 32
CartesianIndex
および CartesianIndex
の配列では、end
キーワードを使って次元の最後の添え字を表すことはできません。この二つのオブジェクトのいずれかを含む添え字で end
を使わないでください。
論理添え字アクセス
論理値配列の true
になっている添え字に対してアクセスを行う手法は「論理 (logical) 添え字アクセス」あるいは「論理マスク (logical mask) による添え字アクセス」などと呼ばれます。
真偽値ベクトル B
による添え字アクセスは findall(B)
が返す整数ベクトルによる添え字アクセスと事実上同じです。同様に N
次元の真偽値配列による添え字アクセスは、値が true
となる部分の CartesianIndex{N}
からなる配列による添え字アクセスと事実上同じです。論理添え字アクセスの添え字はアクセスする次元と同じ長さのベクトル、もしくはアクセスする配列とサイズと次元数が同じ配列である必要があります。後者の場合は添え字は一つだけでなければなりません。一般に真偽値配列を使ってアクセスする方が最初に findall
を呼ぶよりも効率が良くなります。真偽値配列を使った添え字アクセスの例を示します:
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[[false, true, true, false], :]
2×4 Array{Int64,2}:
2 6 10 14
3 7 11 15
julia> mask = map(ispow2, x)
4×4 Array{Bool,2}:
1 0 0 0
1 0 0 0
0 0 0 0
1 1 0 1
julia> x[mask]
5-element Array{Int64,1}:
1
2
4
8
16
格子添え字と線形添え字
格子添え字
N
次元配列にアクセスする通常の方法は正確に N
個の添え字を使うものです。このときそれぞれの添え字が特定の次元における要素を選択します。例えば三次元配列 A = rand(4, 3, 2)
に対する A[2, 3, 1]
というアクセスは、一つ目の "ページ"・三番目の列・二番目の行にある数値を選択します。このアクセス方法を格子添え字アクセス (Cartesian indexing) と呼びます。
線形添え字
ちょうど一つの整数を添え字として A[i]
と配列にアクセスするとき、添え字 i
は配列の特定の次元に沿った位置を表しません。そうではなく、A[i]
は列優先の反復順序を使って線形に配列全体を数えたときに i
番目に来る要素を選択します。このアクセス方法を線形添え字アクセス (linear indexing) と呼びます。これは配列を vec
で一次元のベクトルに変形し、それにアクセスしたかのような振る舞いです:
julia> A = [2 6; 4 7; 3 1]
3×2 Array{Int64,2}:
2 6
4 7
3 1
julia> A[5]
7
julia> vec(A)[5]
7
配列 A
に対する線形添え字 i
は CartesianIndices(A)[i]
で格子添え字に変換できます (参照: 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)
を使うことが強く推奨されます。こうすると A
が IndexCartesian
のとき非常に高速になるだけではなく、OffsetArrays
もサポートされます。
添え字の省略と追加
線形添え字の他にも、N
次元配列に N
個ではない添え字でアクセスする方法が存在します。
最後の次元の長さが 1 なら、その次元に対する添え字は省略できます。言い換えると、境界内の正当な添え字として使える値が一つしかないなら、その添え字は省略できます。例えば四次元配列のサイズが (3, 4, 2, 1)
なら、四つ目の次元の長さが 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] # 長さ 1 の四つ目の次元を省略する
19
julia> A[1, 3] # 三つ目の次元 (長さ 2) と四つ目の次元 (長さ 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] # 線形添え字アクセス
19
全ての添え字を省略した A[]
は、配列に要素が一つしか存在しないことを確認しつつその唯一の要素を取り出すイディオムとして利用できます。
同様に、配列の次元を超える添え字が全て 1 なら、配列の次元数を超える個数の添え字を使ったアクセスが可能です (より一般的に言えば、その次元に対する axes(A, d)
が含む唯一の値を添え字にできます)。この機能を使うとベクトルへ一列の行列のようにアクセスできます:
julia> A = [8,6,7]
3-element Array{Int64,1}:
8
6
7
julia> A[2,1]
6
反復
配列全体を反復する方法として推奨されるのは次の構文です:
for a in A
# 要素 a を使って処理を行う
end
for i in eachindex(A)
# 添え字 i と要素 A[i] を使って処理を行う
end
最初の構文は各要素の値だけが必要で添え字が必要ないときに使用します。二つ目の構文では、A
が高速な線形添え字アクセスに対応する配列型なら i
は Int
となり、対応しなければ i
は 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)
1:length(A)
ではなく eachindex(A)
を使って反復を行うと、任意の配列型に対する効率的な反復が可能になります。
配列トレイト
独自の AbstractArray
型が高速な線形添え字アクセスを持つことを宣言するには、次の定義を加えます:
Base.IndexStyle(::Type{<:MyArray}) = IndexLinear()
この定義により MyArray
に対する eachindex
を使った反復が整数の添え字を使うようになります。このトレイトを指定しないとデフォルトのIndexCartesian()
が使われます。
ベクトル化された演算/関数と配列
配列に対しては次の演算子がサポートされます:
- 単項算術演算子:
-
,+
- 二項算術演算子:
-
,+
,*
,/
,\
,^
- 比較演算子:
==
,!=
,≈
(isapprox
),≉
数学演算やその他の演算のベクトル化を簡単にするために、Julia はドット構文 f.(args...)
を提供します。例えば sin.(x)
や min.(x,y)
とすると、配列の組もしくは配列とスカラーの組に対する演算を配列の要素ごとに行えます (参照: ブロードキャスト)。ドット構文を使った呼び出しには、他のドット構文を使った呼び出しと組み合わさったときに「融合」が起きて単一のループになるという利点もあります。例えば sin.(cos.(x))
とすると融合が起きます。
関数と同様に、全ての二項演算子はドットバージョンをサポートします。ドットバージョンの二項演算子は配列の組もしくは配列とスカラーの組に対して融合されたブロードキャスト演算を行います。例えば z .== sin.(x .* y)
と使います。
==
などの比較は配列全体に対して作用し単一の真偽値を返すことに注意してください。要素ごとの比較には .==
といったドットの付いた演算子が利用できます (<
のような比較演算は、.<
として要素ごとの演算にした場合にだけ配列へ適用可能になります)。
また max.(a,b)
と maximum(a)
の違いにも注意してください。max.(a,b)
は max
を要素ごとにブロードキャストしますが、maximum(a)
は a
に含まれる最大の要素を見つけます。min.(a,b)
と minimum(a)
でも同じ関係が成り立ちます。
ブロードキャスト
サイズが異なる配列に対して要素ごとの二項演算ができると便利な場合があります。例えば行列の各列にベクトルを足すといった演算です。効率の悪いやり方は、ベクトルをコピーして行列のサイズに合わせてから足すというものです:
julia> a = rand(2,1); A = rand(2,3);
julia> repeat(a,1,3)+A
2×3 Array{Float64,2}:
1.20813 1.82068 1.25387
1.56851 1.86401 1.67846
次元が大きくなるとこれでは無駄が大きいので、 Julia はブロードキャストを使った効率の良い方法を提供します。引数の配列のサイズが 1 の次元をもう一方の配列の対応する次元の大きさにまでメモリを使わずに引き延ばし、その配列に対して与えられた関数を要素ごとに適用するという方法です:
julia> broadcast(+, a, A)
2×3 Array{Float64,2}:
1.20813 1.82068 1.25387
1.56851 1.86401 1.67846
julia> b = rand(1,2)
1×2 Array{Float64,2}:
0.867535 0.00457906
julia> broadcast(+, a, b)
2×2 Array{Float64,2}:
1.71056 0.847604
1.73659 0.873631
.+
や .*
といったドット演算子は broadcast
の呼び出しと等価です (ただし以前に説明したように、融合はドット呼び出しでだけ起きます)。結果を書き出す場所を指定する broadcast!
関数もあり、これは代入で .=
を使えば融合と共に利用できます。実を言えば f.(args...)
は broadcast(f, args...)
と等価であり、任意の関数をブロードキャストするための簡単な記法として存在しているだけです (参照: ドット構文)。ネストされたドット呼び出し f.(...)
は単一の broadcast
呼び出しへ自動的に融合されます (.+
なども同様です)。
また broadcast
が処理できるのは配列だけではなく、スカラー・タプル・その他のコレクションも扱えます (詳しくは関数のドキュメントを見てください)。スカラーとみなされる引数の型はデフォルトで決まっており、例えば Number
, String
, Symbol
, Type
, Function
といった型、そして missing
や nothing
のようなシングルトン型はスカラーとみなされます (他にもスカラーとみなされる型は存在します)。その他の引数には反復または添え字アクセスによって要素ごとの処理が行われます:
julia> convert.(Float32, [1, 2])
2-element Array{Float32,1}:
1.0
2.0
julia> ceil.(UInt8, [1.2 3.4; 5.6 6.7])
2×2 Array{UInt8,2}:
0x02 0x04
0x06 0x07
julia> string.(1:3, ". ", ["First", "Second", "Third"])
3-element Array{String,1}:
"1. First"
"2. Second"
"3. Third"
通常はブロードキャストで要素ごとの反復が行われるコンテナ (例えば配列) を "保護" したい場合もあるはずです。別のコンテナ (一要素の Tuple
など) に入れれば、内部のコンテナを単一の値としてブロードキャストできます:
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])
実装
Julia の基礎的な配列型は抽象型 AbstractArray{T,N}
です。AbstractArray{T,N}
は次元数 N
と要素型 T
をパラメータに持ちます。AbstractVector
と AbstractMatrix
はそれぞれ一次元と二次元の AbstractArray
に対する別名です。AbstractArray
オブジェクトに対する演算は高水準な演算子と関数を使って定義されており、内部データの格納方法とは独立しています。一般にこれらの演算は特定の配列実装が存在しないときにフォールバックとして正しく動作するようになっています。
AbstractArray
型は「配列のように扱えるもの」なら何でも表せるので、その実装は伝統的な配列とは大きく異なることがあります。例えば、要素は格納されるのではなく要請されるたびに計算されるかもしれません。ただし、任意の AbstractArray{T,N}
の具象型は一般に size(A)
, getindex(A,i)
, getindex(A,i1,...,iN)
の三つを実装すべきとされます (size(A)
は Int
のタプルを返します)。可変配列では加えて setindex!
も実装するべきです。これらの操作はほぼ定数時間で行えること、正確に言えば Õ(1) の時間複雑性を持つことが推奨されます。もしこの条件が満たされないと、予期せぬ配列関数が遅くなる可能性があります。AbstractArray{T,N}
の具象型は通常 similar(A,T=eltype(A),dims=size(A))
メソッドも加えて提供します。このメソッドは A
と同じ型の配列を作成するものであり、配列を別の場所へ移す copy
のような処理で使われます。
AbstractArray{T,N}
の内部表現に関わらず、T
は空でない配列 A
に対する整数添え字アクセス A[1, ..., 1]
が返す値の型を表します。独自の AbstractArray
を実装する方法について詳しくは、インターフェースの章にある AbstractArray
インターフェースの解説を参照してください。
DenseArray
は AbstractArray
の抽象部分型であり、要素が列優先の順序で連続的に格納される任意の配列を表します (詳しい情報がパフォーマンス Tips にあります)。Array
型は DenseArray
のインスタンスであり、Vector
と Matrix
はそれぞれ一次元と二次元の DenseArray
の別名です。全ての AbstractArray
に必要とされる演算を除くと、Array
に対して特別に実装されている操作はほとんどありません: 配列ライブラリの大部分は一般的な方法で実装されているので、全ての独自配列型で Array
と同様の振る舞いが可能です。
SubArray
は AbstractArray
を特殊化したパラメトリック型であり、別の配列をコピーすることなくメモリを共有しながらその配列に対する添え字アクセスを行います。SubArray
は getindex
と同様の引数 (配列または添え字の列) を使って呼び出される view
関数で作成されます。view
の返り値は getindex
の返り値と同じ配列の一部分を表しますが、データのコピーは行われません。view
は入力の添え字ベクトルを SubArray
オブジェクトに保存し、それを使って元の配列に間接的にアクセスします。コードブロックあるいは式の前に @views
マクロを置くと、そこに含まれる array[...]
によるスライスを SubArray
ビューを作成する処理に変換できます。
BitArray
は空間効率に優れる "パックされた" 真偽値配列であり、一つの真偽値を一ビットに格納します。BitArray
は一つの真偽値を一バイトに格納する Array{Bool}
と同様に利用でき、Array(bitarray)
と BitArray(array)
で相互変換も可能です。
配列が「歩長 (stride) を持つ」あるいは「有歩長である (strided)」とは、要素がメモリ上で一定の間隔を空けて並んでいることを言います。サポートされた要素型を持つ有歩長配列は BLAS や LAPACK のような (Julia とは関係のない) 外部ライブラリに渡すことができ、そのときは pointer
と各次元の歩長を渡せば済みます。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 個の要素を飛ばす必要があるので、三つ目の次元の歩長は 35 となります。strides(A)
は三つの歩長を一つにしたタプルを返します:
julia> strides(A)
(1, 5, 35)
この例では、飛ばされる要素のメモリ上での間隔が線形添え字の間隔に一致します。これは Array
(および他の DenseArray
の部分型) のような連続配列では成り立ちますが、一般には成り立ちません。添え字を区間としたビューは連続でない有歩長配列の良い例です: 例として V = @view A[1:3:4, 2:2:6, 2:-1:1]
を考えます。V
は A
と同じメモリ領域を指しますが、要素のスキップや方向の逆転を行います。V
は A
の要素を三行おきに取るので、V
の一つ目の次元の歩長は 3 です:
julia> V = @view A[1:3:4, 2:2:6, 2:-1:1];
julia> stride(V, 1)
3
同様にビュー V
は配列 A
の列を一つおきに取ります ──よって二つ目の次元の添え字を一つ増やすと、五要素の列二つ分がスキップされます:
julia> stride(V, 2)
10
三つ目の次元は順序が逆なので興味深いことが起こります! V
の最初のページから次のページに移るにはメモリ上を逆方向に移動する必要があるので、この次元の歩長は負となります:
julia> stride(V, 3)
-35
これは V
のポインタが最初 A
のメモリ領域の途中を指しており、添え字に応じてそこから後ろにある要素も前にある要素も指せることを意味します。独自の有歩長配列を定義する方法について有歩長配列の節を参照してください。StridedVector
と StridedMatrix
はそれぞれ一次元と二次元の有歩長配列を表す組み込みの配列型の別名であり、これらの型でディスパッチすれば高度に最適化された BLAS や LAPACK の関数をポインタと歩長を渡して呼び出す特殊実装を選択できます。
歩長はメモリにおけるオフセットであり、添え字とは関係がないことは強調しておいた方がよいでしょう。線形 (一つの整数を使った) 添え字と直行 (複数の整数を使った) 添え字の間で変換を行う方法については LinearIndices
型や CartesianIndices
型のドキュメントを参照してください。