はじめに
スライス表記を使うとこんな感じでコピーができますよね?
fruit = ['りんご', 'ぶどう', 'みかん']
fruit2 = fruit[:]
これ便利なのですが、これでコピーできるんだなーと意味も考えず使うと、よくない結果を招くので、筆者の失敗を交えて注意喚起しようと思います。
前提知識
前提知識として、ディープコピーとシャロ―コピーを知ってないと困るので、軽く話しておきます。
シャローコピー(shallow copy)
シャローコピーは、元のオブジェクトの「参照」をコピーするものです。つまり、コピーされたオブジェクトと元のオブジェクトが同じid
を参照するということです。そのため、元のオブジェクトが変更されると、コピーされたオブジェクトにも影響します。その逆も然りです。
ここではわかりやすくするために簡単な数字やアルファベットで説明します。
a = [A, B, C]
b = a[:] #aをシャロ―コピーしたもの([A, B, C]ということ)
id(a[0]), id(a[1]), id(a[2]) #1, 2, 3
id(a) #4
id(b[0]), id(b[1]), id(b[2]) #1, 2, 3
id(b) #5
こういうイメージでa[0]
もb[0]
もA
というオブジェクトを参照しています。なので、a[0]
からA
の中身を書き換えるとb[0]
からA
の中身を取ってくる時にも書き換わったものが得られる、という具合に相互に影響します。
ディープコピー(deep copy)
ディープコピーは、元のオブジェクトの「値」を完全にコピーするものです。コピーされたオブジェクトは独立したid
を持つため、元のオブジェクトが変更されても影響を受けませんし、その逆も然りです。
こちらも簡単な数字やアルファベットで説明します。
import copy
a = [A, B, C]
b = copy.deepcopy(a) #aをディープコピーしたもの([D, E, F]ということ)
id(a[0]), id(a[1]), id(a[2]) #1, 2, 3
id(a) #4
id(b[0]), id(b[1]), id(b[2]) #5, 6, 7
id(b) #8
こういうイメージで、b[0]
はA
と同じ値を持つD
という別のオブジェクトを参照しています。なので、a[0]
からA
の中身を書き換えても、D
の中身は書き変わらないのでb[0]
からD
の中身を取ってくる時には影響しません。また、その逆も然りです。
本題
筆者の失敗談を話します。筆者が失敗したのはお絵描きツールを作っているときです。redo
とundo
機能を作るため、ピクセル情報を二次元リストにして、redo_stack
とundo_stack
というリストにそれぞれ格納してました。しかし、格納する時点では正しい情報が、なぜかredo
、undo
を実行するときにおかしくなってるんです。これの原因が、スライス表記でコピーするときは、シャロ―コピーだからだったんですよね。
色を塗った時に現在のピクセル情報が書き換わる
↓
それをシャロ―コピーして作られたオブジェクトも全部同期して書き換わる。
こんな具合です。これのせいでredo
してもundo
してもなぜか絵が変わらない...保存したときのピクセルデータは正しく格納されてる...とよくわからず時間を取られてしまいました。
ちゃんと考えれば、二次元リストの情報を格納するときに、シャロ―コピーを使っているのだから、格納した後に中身が同期して書き換わるのは当然のことです。しかし、コピーはめんどいからスライスでやっとけばいいや、という思考停止状態だと、このディープコピーとシャロ―コピーというポイントに全く思い当たらず、本当に原因不明の事態でした。
結論
多次元リストをコピーするときは、中身が同期してほしくないなら、ディープコピーを使っているか指さし確認する!
最後に
ここまで読んで下さりありがとうございました。自分では理解しているつもりでも、理解できていないことって、やっぱりどっかでボロが出るんだなと思いました。こう言う文法が間違ってるとかじゃない、エラーが出ない間違いっていうのは、きっかけがないと気づきにくいですよね。今まさに、似たような原因で詰まっている人が、気づくきっかけになれば良いと思います。