Python では純粋な「二次元配列」はないはずで,「リストのリスト」で「二次元配列」を実現しなければならない。Pythonでも,Numpy には(当然であるが)二次元配列はある。
Juliaには二次元配列(行列)はあり,「リストのリスト」もある。
それぞれにはメリットとデメリットがある。
Pythonの「リストのリスト」
- メリット
-
シンプルさ: Pythonのリストのリストは非常にシンプルで柔軟な方法である。リスト内包表記や標準的なリストの操作を使用して、多くの行列操作を簡単に行うことができる。
-
ダイナミック: リストのリストは異なるデータ型や異なる長さの行を含むことができ、行ごとに異なる長さの列を持つことも可能である。
-
- デメリット
-
パフォーマンス: PythonのリストのリストはC言語などの低レベルの言語で実装された二次元配列よりも遅い場合がある。これは、Pythonのリストが動的型付けやリストの拡張性を提供するためにオーバーヘッドが発生するためである。
-
メモリ効率: Pythonのリストは追加のメモリオーバーヘッドを持ち、多次元の大きな行列を扱う場合にメモリ効率が低いことがある。
-
Juliaの「二次元配列」
- メリット
-
パフォーマンス: Juliaの二次元配列は高速なアクセスと操作が可能である。JuliaはCやFortranのような高性能な言語に匹敵する速度を提供し、特に数値計算や科学技術計算のために優れている。
-
メモリ効率: Juliaは高度なメモリ効率を提供し、大規模なデータセットや行列を効率的に処理できる。
-
- デメリット
-
複雑性: Juliaの二次元配列は、Pythonのような動的型付けと柔軟性が制限されており、一般的なリストの操作が制約されている。
-
学習曲線: JuliaはPythonよりも新しい言語であり、Pythonよりも学習コミュニティやライブラリが小さなため、Pythonに比べて学習曲線が急であることが指摘されている。
-
要するに、Pythonの「リストのリスト」アプローチは柔軟性とシンプルさを提供しますが、パフォーマンスとメモリ効率が低い可能性がある。
一方、Juliaの「二次元配列」アプローチは高性能とメモリ効率を提供しますが、複雑さと学習曲線の高さがある。選択肢は、特定のプロジェクトや要件に依存し、どちらが最適かは状況に応じて変わる。
例
いくつかの例で,「リストのリスト」と「二次元配列」を比較してみる。
なお,JIT コンパイルの影響を避けるために比較プログラムは関数で定義し,@time により関数を引用する実行時間速度を測定する。
メモリ確保
二次元目のリストの要素数が異なる場合や,型の異なる要素からなる場合には,「リストのリスト」を使うしかない。
a = [["foo", 1, 3.14], ["hello", "world"], [32, "years old", "male"]]
a
3-element Vector{Vector{Any}}:
["foo", 1, 3.14]
["hello", "world"]
[32, "years old", "male"]
n×m行列で,要素が単一の型を持つ場合。
using BenchmarkTools
(n, m) = (10000000, 10);
GC.gc()
@btime a = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0] for _ in 1:n];
1.082 s (10000004 allocations: 1.42 GiB)
GC.gc()
@btime b = zeros(Int, n, m);
61.080 ms (2 allocations: 762.94 MiB)
所見: 単にメモリを確保する場合,「リストのリスト」の場合は「二次元配列」の場合に比べて 7〜40 倍ほどの実行時間を要する(実行ごとに変わるが「リストのリスト」が遅いことに変わりはない)。
length(), typeof() による違い
a = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0] for _ in 1:n];
println("length(a) = $(length(a)); length(a[1]) = $(length(a[1])); typeof(a) = $(typeof(a))")
length(a) = 10000000; length(a[1]) = 10; typeof(a) = Vector{Vector{Int64}}
所見: length(a) により得られるのは一次元目のリストの個数 n, length(a[1]) は一次元目のリストの要素数 m
b = zeros(Int, n, m);
println("length(b) = $(length(b)); length(b[1]) = $(length(b[1])); typeof(b) = $(typeof(b))")
length(b) = 100000000; length(b[1]) = 1; typeof(b) = Matrix{Int64}
要素へのアクセス法による違い
a = [[1,2,3], [4,5,6], [7,8,9]]
a
3-element Vector{Vector{Int64}}:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
println(a)
println(a[1][2])
# a[1, 2] |> println # これは 'BoundsError' になる
println(a[1])
println(a[1][:])
println(a[:][1])
println(a[:, :])
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
2
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[[1, 2, 3]; [4, 5, 6]; [7, 8, 9];;]
b = [1 2 3
4 5 6
7 8 9]
b
3×3 Matrix{Int64}:
1 2 3
4 5 6
7 8 9
println(b)
# println(b[1][2]) # これは 'BoundsError' になる
println(b[1, 2])
println(b[1])
# println(b[1][:]) # これは 'MethodError' になる
println(b[1, :])
println(b[:, :])
println(b[:, 1])
[1 2 3; 4 5 6; 7 8 9]
2
1
[1, 2, 3]
[1 2 3; 4 5 6; 7 8 9]
[1, 4, 7]
結論
特殊な場合を除いては二次元配列を使うほうが良さそう。
Python は(Numpy を使わない場合は)真の二次元配列をリストで作れないから「リストのリスト」を使っているだけかな?