はじめに
以下公式リファレンスのQAに元ネタ及び結論は書かれています。
ただ、知らない人も多い気がしたので自分もプラスアルファで調べてみた、という記事です。
リファレンスのQAには他にもみんなが詰まるであろう質問がまとまっているので、一読をお勧めします!
https://docs.python.org/ja/3/faq/programming.html#why-did-changing-list-y-also-change-list-x
実行環境、バージョン
Python3系
ソースコードは以下に配置しています。本記事のソースコードそのままではありません。
https://github.com/Yumihiki/python-lesson-pass-call-value
https://paiza.io/ja
でPythonを選択することで、すぐ動作を確認できます。
挙動について
まずはintの変数について。以下の例、a, bをprintするとどうなるでしょうか?
a = 1
b = a
b = 5
# 出力結果
# a: 1
# b: 5
なんとなくイメージがつきますよね。
では、次のlistの場合はどうでしょうか?
c = [1, 2, 3]
d = c
d[0] = 100
# 出力結果
# c: [100, 2, 3]
# d: [100, 2, 3]
なんということでしょう。dの方はlistの0番目の値が100になっているのは意図的だとしても、cの方も変わってしまいました。
これはPythonの仕様が原因です。
整数はイミュータブル(immutable)で、値を変更できないようになっているそうです。
ただ、値が変わっていますよね?最初の例でbが5になったのですから。
これは私たちから見ると値が変わったように見えていますが、実態は新しいオブジェクトを生成して代入されています。
つまり、元のオブジェクト自体は変わっていないのです。あくまでも、新しいオブジェクトの生成が行われている、ということです。
(Pythonでは全てがオブジェクトという概念がありますが、それだけで記事が書けるので今回は割愛します)
Pythonではid()という組み込み関数を用いることで生成したオブジェクトの識別子を確認できます。
実際に確認してみましょう。
a = 1
b = a
b = 5
print(id(a))
print(id(b))
# 出力結果(値は各環境によって異なります)
id(a): 9788992
id(b): 9789120
c = [1, 2, 3]
d = c
d[0] = 100
print(id(c))
print(id(d))
# 出力結果(値は各環境によって異なります)
id(c): 23352490774208
id(d): 23352490774208
idが同じなので、同じオブジェクトを参照していることがわかりました。
この挙動について、以下のように公式リファレンスのQAには書かれています。
- 変数とは、単にオブジェクトを参照するための名前に過ぎません。 y = x とすることは、リストのコピーを作りません -- それは x が参照するのと同じオブジェクトを参照する新しい変数 y を作ります。つまり、あるのは一つのオブジェクト(この場合リスト)だけであって、 x と y の両方がそれを参照しているのです。
- リストは mutable です。内容を変更出来る、ということです。
漠然と「代入することでコピーしている」と思っていると全くの意味が異なることがわかります。(私はコピーしているとばかり思っていたので・・・)
こういう「実際には何をしているのか?」という点を理解していないと、意図せず元の値を変更する可能性があるので注意が必要です。
そして、実際には上のような例は起こりにくいと思いますので別の例も用意してみました。
base_data_list = [
[1, 'PHP', 1],
[2, 'Python', 1],
[3, 'Perl', 0],
[4, 'Ruby', 1]
]
# P始まりの言語を残す関数(3つ目の要素が頭文字P始まりの言語を表すかどうか、という想定です)
def make_prefix_p_langeage_list(data_list):
for i in data_list:
IS_PREFIX_P_LANGUAGE = i[2]
if not IS_PREFIX_P_LANGUAGE:
data_list.remove(i)
return data_list
print(make_prefix_p_langeage_list(base_data_list))
print(base_data_list)
# 出力結果
# make_prefix_p_langeage_list(base_data_list): [[1, 'PHP', 1], [2, 'Python', 1], [4, 'Ruby', 1]]
# base_data_list: [[1, 'PHP', 1], [2, 'Python', 1], [4, 'Ruby', 1]]
このような例だとどうでしょうか。引数として関数に渡しただけなのに、元の値に影響を及ぼしてしまっています。
このような挙動は「オブジェクトの参照を値渡し」していることが原因です。Python自体はあくまでも値渡しを行う言語で、参照渡しできない言語になります。
値渡しと参照渡しはどういうものか @shiracamus さんがコメントしてくれているので、そのまま引用致します。
変数が保持している値(オブジェクトも値)を渡すが値渡し、変数領域への参照を渡して変数を共有(変数エイリアス)するのが参照渡し、ですね。
そして、元の値への影響を防ぐにはcopyした値を渡してやればOKです。(繰り返しになりますが、代入時には同じオブジェクトへの参照を行っているだけでコピーをしていないので)
import copy
base_data_list = [
[1, 'PHP', 1],
[2, 'Python', 1],
[3, 'Perl', 0],
[4, 'Ruby', 1]
]
def make_prefix_p_langeage_list(data_list):
for i in data_list:
IS_PREFIX_P_LANGUAGE = i[2]
if not IS_PREFIX_P_LANGUAGE:
data_list.remove(i)
return data_list
print(make_prefix_p_langeage_list(base_data_list.copy()))
print(base_data_list)
# 出力結果
# make_prefix_p_langeage_list(copy_data_list): [[1, 'PHP', 1], [2, 'Python', 1], [4, 'Ruby', 1]]
# base_data_list: [[1, 'PHP', 1], [2, 'Python', 1], [3, 'Perl', 0], [4, 'Ruby', 1]]
最後に
元々は、「値渡し」「参照渡し」の違いについて説明しようと思って記事を書き始めましたがPythonは値渡しを行なっている、ということが学べて勉強になりました。(いわゆる参照渡しだとばかりずっと思っていました・・・)
人に説明するために間違っていないかどうかを調べる必要があるのでアウトプットすることは大事だな、と改めて学べました。
(ただ、調べた上で間違っている可能性は否定しきれませんが・・・ 100%の自信はないので、間違っていたら教えていただきたいです!)
参考文献
https://docs.python.org/ja/3/tutorial/controlflow.html?highlight=if#id1
https://pythonmaniac.com/call-by-value-or-reference/
https://teratail.com/questions/273295?link=qa_related_sp