0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go, Python, C# などで配列の一部を取り出す

Last updated at Posted at 2023-04-08

これは何?

を読んで、いろいろ調べた結果を記す。

いろいろな処理系でどうなるか調べた

Go

Go の スライスの気持ちは正直良くわからない。

スライスはどこかのメモリを参照している。そのメモリは make で作ったかもしれないし、配列かもしれない。

go
a := make([]int, 0, 10)
for i := 0; i < 10; i++ {
	a[0:10][i] = i // [A]
}
b := a[1:5] // [B]
fmt.Println(b, len(b)) //=> [1 2 3 4] 4
c := b[2:6] // [C]
fmt.Println(c, len(c)) //=> [3 4 5 6] 4
func() {
	defer func() {
		fmt.Println("panic! ", recover())
	}()
	ix := -1
	fmt.Println(c[ix:4]) //[D] => [略] slice bounds out of range [-1:]
}()
a[0:10][3] = 333 // [E]
fmt.Println(b, c) //=> [1 2 333 4] [333 4 5 6]
defer func() {
	fmt.Println("panic! ", recover())
}()
d := c[5:10] // [F] => [略] slice bounds out of range [:10] with capacity 7
fmt.Println(d, len(c))

初期化のために仕方なく [A] のような汚いことをした。これが合法であるのが Go のスライスの特徴のひとつだと思う。

[A] が可能なので [B] [C] が可能なのは当たり前と思う。
[B] [C] 直後の Println の結果、[A] で埋めた値が確認できるので、[B] や [C] のタイミングでゼロ値で埋めているわけではないことがわかる。

len(スライス) を超えてスライスを切り出せるんだから負のインデックスも行けるだろうと思って [F] を試すと panic。一貫性がない印象。

a への書き込みが b c に影響を与えているので、同じメモリを参照していることがわかる([E])。

[F] で、capacity を超えるとパニックになることがわかる。

Python

Python には、list と tuple がある。さらに numpy.array もある。

それぞれどうなるか見てみる。

以下、登場する p という関数は、print(repr(x)) のような内容になっている。

list

Python3
a = [0,1,2,3,4,5,6,7,8,9]
b = a[1:5]
p(b) #=> [1, 2, 3, 4]
c = b[2:6]
p(c) #=> [3, 4]
a[3] = 333
p(b,c) #=> ([1, 2, 3, 4], [3, 4])

a[1:5] のような式で取り出されたリストは、元のリストとメモリを共有していない。
キャパシティのような値はユーザーから全く見ることができない(と思ってるんだけど、どう?>識者)。

len(リスト) を超えて b[2:6] のようなことをすると、要求より短いリストが手に入る。

[][100] が例外なのに [][100:101] が例外にならないのは一貫性が少なめな印象だけどどうなんだろう。

tuple

Python3
a = (0,1,2,3,4,5,6,7,8,9)
b = a[1:5]
p(b) #=> (1, 2, 3, 4)
c = b[2:6]
p(c) #=> (3, 4)

タプルはリストと違って要素を変更できない。

a[1:5] のような式で取り出されたタプルが、元のタプルとメモリを共有しているかどうかは知る方法がないし、実装の詳細なのでどうでもいい。

リストと同様、len(タプル) を超えて b[2:6] のようなことをすると要求より短いリストが手に入る。

numpy.array

Python3
a = np.array([0,1,2,3,4,5,6,7,8,9])
b = a[1:5]
p(b) #=> array([1, 2, 3, 4])
c = b[2:6]
p(c) #=> array([3, 4])
a[3] = 333
p(b,c) #=> (array([1, 2, 333, 4]), array([333, 4]))
b = np.append(b,555)
p(a) #=> array([0, 1, 2, 333, 4, 5, 6, 7, 8, 9])
p(b,c) #=> (array([1, 2, 333, 4, 555]), array([333, 4]))

numpy.array は、list と異なり、 a[1:5] のような方法で切り出した部分は、もとの numpy.array と同じメモリを参照する。

そこは Go の Slice と似ているが、len(numpy の array) を超えて b[2:6] のようなことをした場合の動作は Python のリストやタプルと同じように、要求より短いリストを返すということになっている。

Go との違いはもうひとつ。
Go はappend すると、もとのメモリが使えればそこを使うけど、numpy.array はコピーを行うので元のメモリに影響を与えない。

C++20

C++20 にはコンテナの一部を別のコンテナとみなす、という趣旨の機能が入っているので、初めて使ってみた。あんまりわかっていないのだけれど。

python と同じく、p という関数が別途あって、中身を適当に出力する。

c++20
using namespace std::ranges;
std::vector<int> a{0,1,2,3,4,5,6,7,8,9};

// a | views::subrange(1,4) みたいに書けないの?
auto b = a | views::drop(1) | views::take(4);
p(b); //=> 1 2 3 4
auto c = b | views::drop(2) | views::take(4);
p(c); //=> 3 4
a[3]=333;
p(b,c); //=> 1 2 333 4, 333 4
return;

C++ も Python と同じく、take(4) としても要素が 4 に満たなければある文だけ返すということになっている。

上記の b ca のメモリを使っているので a を書き換えると b c が書き換わる。

Zig

例によって、 p は中身を適当に出力する関数。

zig0.10
var a = [_]i32{ 0,1,2,3,4,5,6,7,8,9 };
var b = a[1..5];
try p(b);
a[3]=333;
try p(b);
var c = b[2..6]; // error: end index 6 out of bounds for array of length 4

zig の場合、 b[2..6] が範囲外を参照していることがコンパイラにバレて、コンパイルエラーになる。

ならばとこうすると

zig0.10
fn sliceTest(n0 :usize, n1:usize) !void {
    var a = [_]i32{ 0,1,2,3,4,5,6,7,8,9 };
    var b = a[1..5];
    try p(b); // 1 2 3 4
    a[3]=333;
    try p(b); // 1 2 333 4
    var c = b[n0..n1]; // panic: index out of bounds: index 6, len 4
    try p(c);
}

pub fn main() !void {
    try sliceTest(2,6);
}

コンパイルは通り、実行時エラーになる。

この panic というものを catch というか rescue というか recover というかできるかどうかを知らない。

追加はできないよなと思っていたんだけど、コンパイル時ならできる。

zig0.10
var a = [_]i32{ 0,1,2,3,4,5,6,7,8,9 };
var  b = a[1..5];
var  c = b ++ [_]i32{11,22,33};
a[3]=333;
try p(b); // 1 2 333 4
try p(c); // 1 2 3 4 11 22 33

Go とちがって、 ++ はもとの配列に影響を与えない。

C#

C# もあんまり書かないからよく知らないんだけど、こんな感じかな。

str は、空白で Join した結果を返すメソッド。

c#
int[] arr = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var a = new ArraySegment<int>(arr, 1, 5); 
Console.WriteLine(str(a)); //=> 1 2 3 4 5
var b = a.Slice(2,2);
Console.WriteLine(str(b)); //=> 3 4
arr[3]=333;
Console.WriteLine(str(a)+ ", "+ str(b)); //=> 1 2 333 4 5, 333 4
var c = a.Slice(2,6); //=> (略) System.ArgumentOutOfRangeException: Index was out of range. (略)

ArraySegment というものの存在を初めて知ったんだけど、配列の一部の view を作る、みたいなやつ。もとの配列とメモリを共有する。

a.Slice(2,6) は、Go のスライスと同じロジックなら困らずに作れるはずだけど a の範囲からは逸脱しているということで例外。

まとめ

スライス(のようなもの) b からスライス(のようなもの) c を切り出す際。

「メモリ共有」は、b の要素と c の要素が同じメモリを使うかどうか。
「範囲外スライス」は、例えば b の長さが 4 なのに「2番目から始まる 4個くれ」とお願いしたらどうなるか。
「追加」は Go の append に相当する処理。

道具 メモリ
共有
範囲外スライス 追加
Go のスライス 共有 非負のインデックス なら スライス長を超えて capacity までは行ける。
負のインデックスはパニック
上書き
Python の list 非共有 要求より短いものを返す ふつう
Python の tuple どうでもいい 要求より短いものを返す 不可能
Python の numpy.array 共有 要求より短いものを返す コピー
C++ の std::ranges::views 共有 要求より短いものを返す 未調査
Zig のスライス 共有 コンパイルエラー または panic (リカバー不可能?) コピー
C# の ArraySegment 共有 例外 不可能

こうしてみると、capacity を見に行っているのは Go だけ。
その点も含めて Go は 異端だなと思う。

「追加」の欄をちょっと説明しておくと。

Go の append は、キャパシティに余裕があればもとのスライスと同じメモリを使うので、b から切り出したスライス cappend すると、b の内容が書き換わる。
Python の list は、別の list と共有していないのでこの欄で書くべきことがない。
Python の tuple は「自分に追加」がそもそも不可能。追加操作をすると別の tuple になる。
C++ の std::ranges::views は、追加できないことを確認できていない。少なくとも push_back は生えていない。
Zig の追加操作はコンパイル時のみ可能。 ++ で連結できる。連結の結果はもとのスライスとはメモリを共有しない。
C# の ArraySegmentAdd メソッドが生えているものの、呼ぶと必ず例外。

おもうところ

Python の list は、list の何番目に何があるかを覚えるためのメモリの管理も list が担っている。なので、list のメモリを他の list と共有するなんてありえない。C++ の std::vector や ruby の Array なんかもそう。

C++ の std::ranges::views は、メモリの持ち主は他人。なので push_back できない。C# の ArraySegmentAdd できないのもそういうこと。

ちょっときもちわるい(個人の感想です)のは numpy.arrayb=a[1:5] のようにすると、 何番目に何があるかを覚えるためのメモリは ab で共有する。そうすると便利ということなのか、速度のための選択なのかわからないけど、同じ Python の list と異なる挙動なので罠として機能すると思う。
メモリ共有している状態で numpy.append numpy.delete などのサイズが変わる演算をするとコピーが発生する。
サイズが変わらない演算(そういう reshape とか)だとコピーが発生せず、メモリ画は共有されたままになる。むずかしい。

かなりきもちわるい(個人の感想です)のは Go。
numpy.array と同様、メモリの持ち主が誰なのかが曖昧。
numpy.array はそれでも「僕は行列です」という気分を表現していると思うけど、Go のスライスは「僕は値の列だよ」という振る舞いと「僕は値の列のビューだよ」という振る舞いが混ざっているように思う。

値の列なら末尾を超えて切り出せるべきではないし、値の列のビューなら append の挙動は受け入れがたい。

0
1
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?