🧙‍♀️

【Python】Aを変えると、Bが変わる不思議な変数とIDの話

2022/12/28に公開

不思議な変数のトリック

世の中には不思議なことがたくさんあるもので、プログラムも例外ではありません。
次のコードを実行してみてください。

a = [79, 58, 45, 4, 38]
b = a
b.append(32)
print(a)
# [79, 58, 45, 4, 38, 32]

これ、最初に見た時は「えっ?バグ?」って思いました。
でも実際はバグでもなんでもありません。

aのリストをbにコピーして、bに要素を追加したらaにも要素が追加されているんです。
パッと見、ホラーですよね。

学習時のイメージが逆

実はこれ、一般的にプログラミングを教えている時に使う表現と実際の動きが逆だから起こる現象であり、違和感のもととなっています。

a = 10

この代入の流れを最初にを学ぶときは、「箱に10という値を入れ、aと名付ける」といった表現をされたと思います。
でも実際には違います。

aが数値の10を見る

実際はどこかに10という値が生成され、aという変数にはそれを見ています。
aに入れるのではなく、aが見に行っているのです。

では、a = bはどうでしょう。

aもbも10を見る

この場合は、aと同じ対象を見ることになります。
決して、aの見ているオブジェクトと同じものが、bにコピーされる訳ではありません
よって、今abは同じものを見ていることになります。

例に挙げたものは、数値の10でした。
ではそれがリストとなると、どうでしょう。
次のコードを想定してみます。

a = [79, 58, 45, 4, 38]
b = a

aとbがリストを見る

このようになりますね。
リストのオブジェクトが生成されて、代入によりaがそれを見ます。
a = bにより、bも同じオブジェクトを見ます。
つまり、今abは同じものを見ています。

ここで、.append()が起こるとどうなるでしょう。

b.append(32)

aとbの見ているリストに32が追加される

bの見ている対象のオブジェクトに、32が追加されました。
でもそれは、aが見ているオブジェクトでもあります
よって、print(a)をしても、32が追加されているという結果になります。

あ~、スッキリ!

直接定義したものは、新規生成

あなたがコードのなかで直接定義したものは、すべて新規生成されるものになります。
10も、'Unico'も、['Your', 'list']も、すべてそうです。
なので、同じ値を入れていたとしても、直接定義されていればそれは別のものを見ています。

a = 10
b = 10

aとbがお互いに別の同値のオブジェクトを見ている

気を付けるべき点は、変数間で中身を移動したい場合です。
クラスを作ると、self.value = valueなどよくやりますね。
こういったときには注意して設計しましょう。

同 じ も の を 見 て い ま す 。

# テンプレートとして利用したい
params = {
    'name': None,
    'level': 1,
    'skill': []
}

# コピーしたつもり
me = params

# パラメータを設定
me['name'] = 'Unico'
me['level'] = 20
me['skill'].append('水の魔法')

# テンプレート(params)が変わってしまう
print(params)
# {
#   'name': 'Unico',
#   'level': 20,
#   'skill': ['水の魔法']
# }

コピーしたつもりで=を使ってしまうと、とんでもないバグになってしまいます。

リストをコピーして別々で持たせたいんだけど!?

上記のように、テンプレートとして利用したいものがあって、どうしてもコピーしたい場合には、いくつか手段があります。
その中の一つは、deepcopyを利用することです。
deepcopyは同じ中身の値を持った、新しいオブジェクトにして生成してくれます。

使い方は、超かんたん。
importして、関数にコピーしたい変数を渡すだけ。
同値の新しい生まれたてのオブジェクトにして、戻してくれます。

from copy import deepcopy

# テンプレート
params = {
    'name': None,
    'level': 1,
    'skill': []
}

# コピーしたつもり
me = deepcopy(params)

# パラメータを設定
me['name'] = 'Unico'
me['level'] = 20
me['skill'].append('水の魔法')

# テンプレート(params)は変わっていない
print(params)
# {
#   'name': None,
#   'level': 1,
#   'skill': []
# }

# deepcopyした方(me)は変わっている
print(me)
# {
#   'name': 'Unico',
#   'level': 20,
#   'skill': ['水の魔法']
# }

もっと知りたい方は・・・

変数のID、ミュータブル、イミュータブルな変数について調べてみよう!

それではよいPythonライフを。

参考

Discussion