python

Pythonは「値渡し」です!「参照渡し」という誤解はなぜ生じるのか?

Pythonを学び始めた時、ネットを調べて混乱したことがありました。

それは「Pythonは参照渡し」と書かれているサイトが多かった事です。

しかし、実際に動かしてみると、どう考えても値渡しの挙動を示します。

なぜ、Pythonは参照渡しであるという誤解が生じるのか?

本記事では、値渡しと参照渡しの違いについて解説します。

値渡しと参照渡し

値渡し・参照渡しとは、関数やメソッドを読んだ時の引数の挙動についての概念です。

値渡し(call by value)の場合、変数の値をコピーして渡します。

変数の値を関数の中で変更しても、関数の呼び出し元の値が変更されることはありません。

一方、参照渡し(call by reference)は、変数の参照を渡します。

変数の値を関数の中で変更した場合、関数の呼び出し元でも値が変更されます。

値渡し・参照渡しイメージ値渡し・参照渡しイメージ

pythonでの挙動の確認

Pythonは値渡しです。

Pythonの公式ドキュメント(日本語版)にも以下のような記載があります。

関数を呼び出す際の実際の引数 (実引数) は、関数が呼び出されるときに関数のローカルなシンボルテーブル内に取り込まれます。そうすることで、実引数は 値渡し (call by value) で関数に渡されることになります (ここでの 値 (value) とは常にオブジェクトへの 参照(reference) をいい、オブジェクトの値そのものではありません)

引用元:Python 3.10.6 ドキュメント 「4.7 関数を定義する」

いくつかのコードを実行して確かめてみまよう。

まずは、以下のコードを実行します。

def test_call_by(arg):
    arg = 1

val = 2
test_call_by(val)
print(val)

 

表示される結果はです。

関数の中で値を変更しても、値渡しなので、呼び出し元では値は変わりません。

参照渡しならば、関数内で代入した値の1が、呼び出し元にも反映されるはずです。

なお、時々、以下のような説明を見かける事があります。

「Pythonは参照渡しだが、イミュータブルなオブジェクトの場合は値の変更ができないので、値渡しの挙動になる。ミュータブルなオブジェクトは参照渡しになる。」

断じて否です。

以下のコードを実行します。

def test_call_by(arg):
    arg = [1]

val = [2]
test_call_by(val)
print(val)

 

リストは、ミュータブル(変更可能)なオブジェクトですが、表示される結果は[2]です。

オブジェクトがミュータブルかイミュータブルかは本質的に関係ありません。

誤解が生じる理由

上のコードを見て、「いやいや、値を変更するってそういう事じゃないんだよ。こういう風に値を変えれば、呼び出し元でも変わるでしょ?」と思った人もいるかもしれません。

つまり、以下のようなコードです。

def test_call_by(arg):
    arg[0] = 1

val = [2]
test_call_by(val)
print(val)

 

この場合、表示される結果は[1]です。

つまり、関数の中で変更されたオブジェクトの値が、呼び出し元にも反映されています。

しかし、これもあくまでも値渡しの挙動です。

誤解が生じる理由は、Pythonの型が全てオブジェクトのため、変数の値にオブジェクトへの参照が格納されていることでしょう。

変数の値(=オブジェクトへの参照)が値渡しされるため、オブジェクトの内容が変更された場合は、呼び出し元も関数内も同じオブジェクトを参照しているため、呼び出し元でも変更した内容が反映されます。

関数の内部で変更したのは、変数の値ではなく、値が参照するオブジェクトなのです。

一方、ひとつ前の例は、関数の内部で変数の値を変えた場合(別のオブジェクトに参照を変えた場合)なので、呼び出し元には反映されません。

このような、値にオブジェクトへの参照を格納している場合の値渡しをオブジェクトへの参照渡し(call by object reference)と言う場合があります。

オブジェクトの参照渡しイメージオブジェクトへの参照渡しイメージ

 

Pythonの公式ドキュメント(日本語版)にも値渡しについての脚注として以下のような記載があります。

実際には、オブジェクトへの参照渡し (call by object reference) と書けばよいのかもしれません。というのは、変更可能なオブジェクトが渡されると、関数の呼び出し側は、呼び出された側の関数がオブジェクトに行ったどんな変更 (例えばリストに挿入された要素) にも出くわすことになるからです。

引用元:Python 3.10.6 ドキュメント 4章 脚注

重要なのは、オブジェクトへの参照渡しは、あくまでも値渡しの一種なので、参照渡しとは概念が異なるという事です。

オブジェクトへの参照渡し(call by object reference)を共有呼び(call by sharing)と言うケースもあります。(※無理やり「共有呼び」と書きましたが、日本語の方はあまり一般的ではないです。)

参照渡しはできないのか?

php、C#などは、基本的には値渡しですが、引数にキーワードを付ける事により参照渡しをすることも可能です。

phpの場合、参照渡しにするためには関数の引数に$を付与します。

<?php
//値渡しのケース(デフォルト)
function test_call_by($arg) {
    $arg = 1;
}
 
$val = 2;
test_call_by($val);
print($val) //実行結果は2
?>
<?php
//参照渡しのケース(引数に&を付与)
function test_call_by(&$arg) {
    $arg = 1;
}
 
$val = 2;
test_call_by($val);
print($val) //実行結果は1
?

 

C#の場合、参照渡しにするためには関数の引数(呼び出元、定義側両方)にrefを付与します。

//値渡しのケース(デフォルト)
using System;
 
public class Test
{
	public static void Main()
	{
		int val = 2;
		TestCallBy(val);
		 Console.WriteLine(val); //2が表示される。
	}
 
	private static void TestCallBy(int arg)
	{
		arg = 1;
	}
}
//参照渡しのケース(引数(定義、呼び出側)にrefを付与)
using System;
 
public class Test
{
	public static void Main()
	{
		int val = 2;
		TestCallBy(ref val);
		 Console.WriteLine(val); //1が表示される。
	}
 
	private static void TestCallBy(ref int arg)
	{
		arg = 1;
	}
}

 

しかしPythonには、そのような機能はなく、値渡ししかできません。

関数内での変数への変更を呼び出し元に反映させたい場合は、

  1. 参照しているオブジェクトの値を変更する。
  2. 変更した値をreturnで返す。
  3. グローバル変数を使用する。

といった方法があり、基本①か②の手法を使います。(③はよっぽどの事がなければ使いません。)

まとめ

Pythonは値渡し(オブジェクトへの参照渡し)のみをサポートしています。

「Pythonは参照渡しである」と言う言説は、オブジェクトへの参照渡しを勘違いしたものです。

また「オブジェクトがミュータブルかイミュータブルかによって挙動が変わる」と言うのは、参照しているオブジェクトの値が変えられるかどうかの違いであり、値渡しである事には変わりありません。

「参照渡し」であるという誤解によって、関数内で、値を変更した場合(”=”で値(参照先)を変えた場合)に呼び出し元が変わると思っている人が少なからずいると思います。

私は、どちらかというとJava歴の方が長いのですが、同じく値渡しのJavaでも、参照渡しであるという誤解によるバグを時々見かけました。(かなりJava歴が長い人でも誤解している人がいました。)

Javaにはプリミティブな型があり、それらはオブジェクトの参照ではなく値自体を保持するので、関数の引数では、文字通りその値を渡します。オブジェクト型の場合は、Pythonと同様にオブジェクトへの参照を渡します。

Cプログラマの方はポインタによって、このような参照の考え方は理解していると思いますが、最近のプログラミング言語ではあまり意識しなくても、コーディングができてしまいます。しかし、基本を理解していないと思わぬところで不具合が発生するので、このような最低限の振る舞いについては理解しておいた方が良いでしょう。