やるだけPython競プロ日誌

競プロの解説をPythonでやっていきます。できるだけ初心者に分かりやすいように『やるだけ』とかは言わないようにします。コメントについては必ず読んでいます。どんなに細かいことでもいいのでコメントくださればうれしいです。

Python における list の本質と 二次元配列 ( 多次元配列 ) のお話。

こんなページを見てくださっているような方々は分かり切っていることかもしれませんが、わたくしなりに考えてみたことです。

ネットで、私の疑問に直接回答しているサイトは見当たりませんでしたので、ここに記します。

茶番開始です()

配列の宣言

Pythonでリストを宣言するときは、

list = [1, 1, 1, 1, 1]  # あるいは
list = [1] * 5

のようにするのが一般的かと思います。
そして中身を変更する際は

list[0] = 9
print(list)  # [9, 1, 1, 1, 1]

とするものだと思います。

基本中の基本ですよね。


二次元配列

知らない方のために一応説明しておきますが、二次元配列とは、"要素として配列を使用した配列"です。

つまり、

[[1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3]]

こういったものです。今のリストは、

list = [[i] * 5 for i in range(1, 4)]

で作成できます。

range(1, 4) つまり i = 1, 2, 3の時の [i] * 5 を要素とするリスト。という意味になります。



ふむふむ。分かりやすい。


では、すべて0で初期化した二次元目の要素数5、一次元目の要素数3のリストを作ってみましょう。

list = [[0] * 5] * 3
print(list)  # [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

できました。では、要素の変換をしてみましょう。

それぞれの要素ごとに対応したインデックスを付けたします。

list[一次元目のインデックス][二次元目のインデックス]

といった感じです。

list[1][2] では、一次元目が2番目の、二次元目が3番目の要素を指定できます。

list = [[0] * 5] * 3
list[1][2] = 1
print(list)  # [[0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]]

あれ!? 丘people!? なんでこうなるの!?





茶番でした()


はい。よく聞くお話ですよね。


上の宣言では、[0] * 5 というリストを3つ並べているだけなので、1つの要素を変えると、ほかの要素まで変わってしまう。という話です。


もう少し細かくいうと、


[0] * 5 という1つしか無いリストをそれぞれlist[0]からlist[2]で参照しているだけなので、list[1][2]というたった1つの要素を変えたつもりでも、参照元[0] * 5 というリストそのものを書き換えてしまっているので、list[0], list[2]の要素も変わってしまう(様に見える) ということです。
(様に見える)、というのは、もともとlist[0]list[2]も固有の要素を持っているわけではないという意味です※ここ重要です。

なので

list = [[0] * 5] * 3
if id(list[0]) == id(list[1]):
    print("same id")  # same id

となります(id は固有に与えられたものなので、違うものであれば同じidであることはありません。)


ちなみに、先ほどの二次元配列を正しく初期化しようとすると、

list = [[0] * 5 for i in range(3)]
if id(list[0]) != id(list[1]):
    print("different id")  # different id

[0] * 5 というリストを3回作っているので、別のものとしてカウントされます。

となります。

これは私も理解できます。ところで

何故二次元の要素は同じだとカウントされないのだろう

ということを疑問に思いました。

先ほどの物は例として悪いので、新しいものを挙げます。

number = 0
list = [[number] * 5] * 3
print(list)  # [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
list[0][0] = 1
print(list)  # [[1, 0, 0, 0, 0], [1, 0, 0, 0, 0], [1, 0, 0, 0, 0]]
print(number)  # 0

こういった場合に関する疑問です。

上の宣言から、listの各要素はすべてが number であることがわかります。

そして代入時に

list[0][0] = 1

つまり

number = 1

よって、すべてのnumberを参照している値(list[0][0] から list[2][4])すべてが1に代わるのではないかと思ったのです。


私のどの部分が間違っているのでしょうか?


先ほど使ったid()関数を用いて考えます。

要素すべてがnumber を参照しているわけではない説

確かめてみましょう

number = 0
list = [[number] * 5] * 3
print(id(number))  # 1952997504
print(id(list[0][0]))  # 1952997504
print(id(list[0][1]))  # 1952997504
print(id(list[2][4]))  # 1952997504

全て同じですね。

この説は野獣先輩たまご説ていどの信ぴょう性しかなさそうです。


ということは

代入時に参照が解かれている説

number = 0
list = [[number] * 5] * 3
print(id(number))  # 1952997504
print(id(list[0][0]))  # 1952997504
list[0][0] = 1
print(id(number))  # 1952997504
print(id(list[0][0]))  # 1952997536
print(id(list[0][1]))  # 1952997504

らしいですね。

なぜこんなことが起こるのでしょうか?


実は、Pythonという言語の変数( リスト )の仕様に答えがあります。

答え

勘違いしがちなのですが、実際に変数やリストに値が代入されているわけではないのです。

実のところ、"値をメモリに記憶し、その参照先( アドレス )を保管している"のです。

つまり、list[0][0]が初めに参照してたのは、numberの参照先データです。

そのうえからlist[0][0] には、1 というデータを保管したアドレスを上書きされたので、numberには直接影響は出ません

しかし、list[1][0]list[2][0]list[0][0]のアドレスを参照しているので、こういった事態になるのです。


これは野獣先輩女の子説と同じくらいはっきりわかんだね…

あー、すっきりした。

というわけで、今回は私のバカな疑問でした。

それではPythonistaの皆さん、よいPython Life を!!

宣伝…