記事の目的
pythonで代入とコピーの違いとか、ディープコピーって何?とかの質問がよくあって、その度に、うまく説明された資料がないなぁと思っていたので作ってみた。
うまく説明されたとは思わないけど、伝えたいことは書けた。参考になれば。
pythonの値(オブジェクト)と変数
pythonではいくつかの方法で値を作ることができます。作った値を置いておく場所がありますが、ここでは作業領域と呼ぶことにします。
いくつか値を作ってみます。
# 数値
3.14
# 文字列
'円周率'
# 最初の3つの素数のリスト
[2, 3, 5]
# 辞書
{'円周率': 3.14, '究極の疑問の答え': 42}
このように書くと、下図のように作業領域に対応するデータが作られます。(このときのデータはクラスのインスタンスで、本当は値だけでなくいろいろな属性も持っているというのは別の話で。)
作られはしましたが、この状態ではこれらの値を使う方法がありません。
これらの値を使えるようにするためには値を変数に代入します。代入と言っても、値を変数の中に入れるのではなく、変数と値を結び付けます。このことを変数を値で束縛するとも言います。
# 数値
pi = 3.14
# 文字列
pai = '円周率'
図にするとこんな感じで、変数が値に束縛されていることを矢印で示しています。
リストと辞書の図を書いていませんでしたね。
リストはこんな感じです。リストを理解する場合、中に値が入っているわけではないところが注意ポイントです。中の値は、作業領域の別の場所につくられたものを指すことで組入れています。
辞書はこんな感じです。リストと同様、中に値が入っているわけではないところが注意ポイントです。 また、キーの方は特殊な扱いなのでとりあえずは中に入っていると考えておいてください。
これでひととおり説明できました。複雑なデータもこれの組み合わせでできています。辞書の値にリストを入れた場合。
他の変数に値を代入したときに起きること
以下のコードで起きていることを図示するとこんな感じです。
aに、辞書のリストを代入します。
a = [{'一': 1, '二': 2, '三': 3},
{'十': 10, '百': 100, '千': 1000}]
ここで、aの値をbに代入すると、aとbが同じものを指すようになります。
b = a
この状態で、bの値を変更するとaの値も変ります。 図からすればあたりまえですね。
b[0] = "なし"
print(a)
さて、このような動きを想定していなくて、aの値が変わらないようにしたいのであれば、作業領域のデータをコピーすればいいんじゃないかということになりますよね。
実際、pythonにはコピーする方法があります。 ただし、そのコピーには浅いコピーと深いコピーの2種類があります。
浅いコピー(shallow copy)
まずは浅いコピーです。英語だとshallow copy(シャローコピー)です。
pythonのオブジェクトには、copyメソッドが付いていくものがありますが、それらは大抵浅いコピーをします。
ここでは、copyモジュールの関数を使います。
a = [{'一': 1, '二': 2, '三': 3},
{'十': 10, '百': 100, '千': 1000}]
import copy
b = copy.copy(a)
たぶん、想像と違っていると思います。1層めだけコピーされていて、中身は元のデータと同じものを指しています。 1層めが辞書でも同じことが起きます。
この状態で、前節のようにb[0]の値を変更してみます。
b[0] = "なし"
print(a)
このとき、aの値が変わらないことがわかります。
図はこのとおり。
bに代入されているリストは、aに代入されているリストと異なるので、0番目を変更してもaに影響はありません。
次に、リストの2番目にある辞書の内容を変更するとどうなるでしょうか。
b[1]["百"] = "百"
print(a)
これは、aの値も変ってしまいました。
図で見ると理由は明らかですね。
このように、浅いコピーでは一段目の構造のみ複製が作られ、内容は同じものを使うようになっています。
リストそのものを操作するような使い方であればこれで充分ですし、そのような使いかたは多いでしょう。
深いコピー(deep copy)
次は深いコピーです。英語だとdeep copy(ディープコピー)です。
前節の2番目のような動作が問題になるのであれば、深いコピーを使います。
深いコピーは浅いコピーと違って、全てのオブジェクトをたどって複製を作ってくれます。
import copy
b = copy.deepcopy(a)
内容まで完全にコピーされているので、bの内容をどのように変更しても、もうaに影響することはなくなります。
いつもdeepcopyでいいのでは?
pythonに限らず、他の言語でも同様のことはよくあることで、初心者のうちはよくこの動作に悩まされたりします。すべての動作がdeepcopyであったらこんなことは起きないと思うかもしれませんが、いくつか理由があります。
- 効率がよい
作業領域は有限なので、有効に使いたい、全てをコピーするとその分消費するし、時間もかかる。
値を参照するだけであれば、同じものを見ていても問題ない。 - 積極的に使うこともできる
同じものに別のところからアクセスできるというのは、うまく使えば便利。