基本的なことなんだろうけど、理解してないせいで詰まったのでメモ。
まえがき
pythonで配列を作るのは簡単でいいですね。
a = [0] * 2
print(a) # [0, 0]
二次元配列も作ってみましょう。
b = [[0] * 2] * 2
print(b) # [[0, 0], [0, 0]]
あら便利。じゃあ操作してみましょう。
b[0][1] = 1
printしてみましょう。
print(b) #[[0, 1], [0, 1]]
んん??????
結論
こう書きましょう。
c = [[0] * 2 for i in range(2)]
print(c) # [[0, 0], [0, 0]]
c[0][1] = 1
print(c) # [[0, 1], [0, 0]]
なんでダメなの?
id()関数を使ってみましょう。
b = [[0] * 2] * 2
print(id(b[0])) # 1835216839944
print(id(b[1])) # 1835216839944 アドレスが同じ
c = [[0] * 2 for i in range(2)]
print(id(c[0])) # 1835216058120
print(id(c[1])) # 1835216840392 アドレスが違う
*2でコピーすると同じアドレスを参照することになります。
b[0][1] = 1、という形でアドレスの参照している「中身」を変更すると、アドレスを共有する全ての配列に反映されるんですね。
なんで1次元配列だとこうならないの?
1次元配列だとこの現象は発生しませんね?
a = [0] * 2
a[0] = 1
print(a) # [1, 0]
アドレスを見てみましょう。
a = [0] * 2
print(a[0] == a[1]) # True
a[0] = 1
print(a[0] == a[1]) # False
print(a) # [1, 0]
最初にコピーした時点では同じアドレスを持っていますが、a[0]=1を代入するとアドレスが変化しました。
これは参照先がイミュータブル(変更不能体)かミュータブル(変更可能体)かの違いです。
1次元配列を操作する時は「整数を持つ参照先の中身を変更することはできない」(イミュータブル)ので、別のアドレスを与えることで実質的に値を変更しています。
2次元配列を操作する時は「配列を持つ参照先の中身は変更できる」のでアドレスは固定したまま値を変更することができます。
ちなみに
こんな代入だとどうでしょう?
# さっきまでのパターン
b = [[0] * 2] * 2
b[0][1] = 1
print(b) # [[0, 1], [0, 1]]
print(id(b[0]) == id(b[1])) #True
# 変化させたバージョン
b = [[0] * 2] * 2
b[0] = [0, 1]
print(b) # [[0, 1], [0, 0]]
print(id(b[0]) == id(b[1])) #False
新たにアドレスが生成されます。
この場合は「中身の操作」ではなく「再定義」という扱いになり、参照先が変更されます。
b = [[0] * 2] * 2
b[0] = [0, 0]
print(b) # [[0, 0], [0, 0]]
print(id(b[0]) == id(b[1])) #False
参考など
参考: http://delta114514.hatenablog.jp/entry/2018/01/02/233002
追記:
コメントの指摘を元に内容を修正しました。「Pythonは全て参照渡し」「イミュータブル(変更不能体)とミュータブル(変更可能体)」あたりの概念を理解してませんでした。
参考: https://snowtree-injune.com/2019/09/16/shallow-copy/