Python の引数は値渡しか、それとも参照渡しか。

値渡し (call by value)

 

f:id:domodomodomo:20170528223804j:plain
 

引数は 値渡し (call by value) で関数に渡されることになります

4. その他の制御フローツール — Python 3.6.1 ドキュメント

 

ここで渡されている値は、具体的には id という値だと思っています

思っていますというのはマニュアルにそう書かれている記載がないからです。 id か、それに近い値だと思っています。
 

id はオブジェクトに与えられる識別用の番号です。id 関数を使うことで実際にオブジェクトに与えられている id を確認することができます。
 

>>> id(1)
4424748000
>>> 
>>> id(['a', 'b', 'c'])
4427860936
>>> 
>>> id('Hello, world!')
4427920112

 

id(object)
オブジェクトの “識別値” を返します。この値は整数で、このオブジェクトの有効期間中は一意かつ定数であることが保証されています。有効期間が重ならない 2 つのオブジェクトは同じ id() 値を持つかもしれません。
CPython 実装の詳細: This is the address of the object in memory.

2. 組み込み関数 — Python 3.6.1 ドキュメント

でも、なんかもう一度よく読んでみると...

○ 疑問1

値は参照でもあるんだよね。じゃあ、値を渡しているということは参照を渡してるとも言えるんじゃないの?参照渡しでもあるんじゃないの?

引数は 値渡し (call by value) で関数に渡されることになります(ここでの 値 (value) とは常にオブジェクトへの参照(reference) をいい、オブジェクトの値そのものではありません) [1]。

4. その他の制御フローツール — Python 3.6.1 ドキュメント

 

上記の文章で、値が参照だと言われているのは 値つまり id はオブジェクトの識別子なので、参照と考えることもできます。それと思しき記述もマニュアルの中に見当たります。

名前 (name) は、オブジェクトを参照します。

4. 実行モデル — Python 3.6.1 ドキュメント

○ 疑問2

後ろに "オブジェクトへの参照渡し(call by object reference)" って書いてある... 参照渡しって書いてあるから参照渡しなんじゃないの?

[1] 実際には、オブジェクトへの参照渡し(call by object reference) と書けばよいのかもしれません。

というのは、変更可能なオブジェクトが渡されると、関数の呼び出し側は、呼び出された側の関数がオブジェクトに (リストに値が挿入されるといった) 何らかの変更に出くわすことになるからです。

4. その他の制御フローツール — Python 3.6.1 ドキュメント

○ 答え

それでも 参照渡し (call by reference) ではない。id は値です。新たに id という値のコピーを作って渡しているだから、値渡しになります。

オブジェクトへの参照渡し(call by object reference)

じゃあ、この値渡し (call by value) に分類される オブジェクトへの参照渡し(call by object reference) というものが、どういう動作をしていているのかを見ていきたいと思います。

○ 理解するためのポイント

「変数への代入」と「オブジェクトの属性への代入」の動作の違いを区別することで理解しやすくなります。

動作 結果
変数への代入 オブジェクトの属性は変化しない。
オブジェクトの属性への代入 オブジェクトの属性が変化する。

○ 実際にはどういうことだってばよ?

次のような Person クラスについて考えてみましょう。

>>> class Person():
...   def __init__(self, name):
...     self.name = name
... 
>>> person = Person('やる夫')
>>> 
>>> print(person.name)
やる夫
>>>
>>> print(id(person.name))
4326054528

1. 変数への代入

○ 問題

実行結果 1, 2 には何が出力されるでしょうか?

>>> # 変数への代入
... def change_name_1(target_name):
...   target_name = '岩倉玲音'
...   print(target_name)
...   print(id(target_name))
... 
>>> change_name_1(person.name)
岩倉玲音
4325452152
>>> 
>>> print(person.name)
実行結果1
>>> print(id(person.name))
実行結果2

○ 答え

# 実行結果1
やる夫

# 実行結果2
4326054528

 

○ 解説

f:id:domodomodomo:20170527103250j:plain
change_name_1 関数 は、change_name_1 関数の中だけで有効な局所変数 target_name の中身を取り替えただけです。

オレンジの箱(変数)と緑の箱(仮引数)は、別の箱です。関数の中で緑の箱(仮引数)に何を代入しようと、オレンジの箱(変数)は変化しません。

そのため、実行しても関数の外側にある person オブジェクトの属性は変化しませんでした。


2. オブジェクトの属性への代入

○ 問題

実行結果 3, 4 には何が出力されるでしょうか?

>>> # 属性への代入
... def change_name_2(target_person):
...   target_person.name = 'サーバルちゃん'
...   print(target_person.name)
...   print(id(target_person.name))
... 
>>> change_name_2(person)
サーバルちゃん
4325452240
>>> 
>>> print(person.name)
実行結果3
>>> print(id(person.name))
実行結果4

○ 答え

# 実行結果3
サーバルちゃん

# 実行結果4
4325452240

 

○ 解説

f:id:domodomodomo:20170527103309j:plain
関数の外側にあるにある属性(箱)に格納されていた値 id を別の id に書き換えています。

change_name_2 関数 は id が 367214915 であるオブジェクトの name 属性(箱)に、新しい id 4325452240 を代入しています。

 


参照渡しと値渡しの違い

○ ポイント


参照渡し 箱を渡す
値渡し 箱の中身を渡す


値渡し (call by value) :
メソッドの引数に、新たに値のコピーを作って渡す方法のこと。

参照渡し (call by reference) :
メソッドの引数に、変数そのものの参照を渡す方法のこと。

対象を「どうやって渡すか」を表しているのが「値渡し」や「参照渡し」です。何が渡されるかではないのです。つまり「値」を渡すから「値渡し」で「参照」を渡すから「参照渡し」というわけでは断じてないのです。

もう参照渡しとは言わせない - Qiita

○ もし Python が参照渡し(call by reference) なら?

上の図で説明すれば、箱ごと関数に引数として渡される動作をします。

そして、もし Python が参照渡しだったとして、箱ごと渡していたら change_name_1 関数を使うと引数 target_name には、 person.name の箱そのものが渡されます。すると、関数から抜けても person.name は値が変更されているというわけです。


動作 結果
オブジェクトの属性への代入 オブジェクトの属性が変化する。
変数への代入 オブジェクトの属性も変化する。



○ しかし、何故このような誤解が起こるのでしょうか?

おそらくその目線の違いが、参照渡しなのか、値渡しなのかの誤解を生んでいる原因ではないかと思っています。

これはおそらくプログラマの目線で見ると参照を渡しているように見えますが、Python 言語そのものを実装している人の目線からすると id という値を渡しています。