python PR

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

記事内に商品プロモーションを含む場合があります

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

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

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

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

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

なお本記事について、お問合せから様々なご意見を頂きました。
「オブジェクトへの参照渡し」は結局参照渡しなのではないか、Javaとは内部のロジックが違うから一概に同じとは言えないのではないか、共有渡しという言葉の方が妥当なのか(これはさらに混乱するから使わない方が良いとか)などなど・・
最後は宗教論争みたいになってしまうような事もありましたが、最も強調したかったのは、言葉というより挙動でありVBのByRefを使用した場合と同じようになると勘違いしているのを正したいという意図だとご理解ください。

なお以下の記事には、C++,C#,Pythonの「値渡し」「参照渡し」の挙動の違いについて詳しく説明しております。

言語が変われば挙動も変わる!参照渡し・値渡しに潜む思わぬ落とし穴以前、以下の趣旨の記事を投稿したところ、何人かの方々から問い合わせを受けました。 https://dodotechno.com/p...

値渡しと参照渡し

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

値渡し(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]です。

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

pythonの挙動を「値渡し」というか「参照渡し」については多少なり議論があるにしてもオブジェクトがミュータブルかイミュータブルかは本当に関係ないです。
以下のコードを実行した場合に代入されたオブジェクトの参照につけ変わる点はミュータブルだろうとイミュータブルだろうと同じです。

print("イミュータブルの場合") 

n1 = 10
print("n1のID:", id(n1))  # n1のメモリアドレスを出力
n2 = 20
print("n2のID:", id(n2))  # n2のメモリアドレスを出力
n1 = n2
print("n1がn2に代入された後のn1のID:", id(n1))  # n1がn2に代入された後のメモリアドレスを出力

print("ミュータブルの場合") 

obj1 = [10, 20]
print("ojjのID:", id(obj1))  # ojjのメモリアドレスを出力
obj2 = [20, 30, 40]
print("obj2のID:", id(obj2))  # obj2のメモリアドレスを出力
obj1 = obj2
print("obj1がobj2に代入された後のobj1のID:", id(obj1))

結果

イミュータブルの場合
n1のID: 4304634384
n2のID: 4304634704
n1がn2に代入された後のn1のID: 4304634704
ミュータブルの場合
ojjのID: 4305384000
obj2のID: 4305991232
obj1がobj2に代入された後のobj1のID: 4305991232

ただCやC++だと上記のn1,obj1のID(アドレス)は変更されずn2,obj2の実態がコピーされる(これこそ本当の値渡しだ!)とすれば「pythonは参照渡しだろ!」と言われる方の気持ちもわかります…。

誤解が生じる理由

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

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

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