この記事でPythonの関数が「値渡し」か「参照渡し」について決着をつけたいと思います。

さて、いきなり結論を書きますが、Pythonの関数は「値渡し」です。

そしてPythonではすべてがオブジェクトなので、変数(や引数)は値としてオブジェクト参照を持ちます。つまりは関数へ引数を渡すとき、オブジェクト参照が「値渡し」されるので、これを表現する言葉は「参照の値渡し」です。

結論:Pythonの関数は「参照の値渡し

この記事の残りでは、Pythonの関数の「参照の値渡し」について詳しく解説していきます。

Python公式ドキュメントの見解

関数への引数の渡し方が「値渡し」であることは、Pythonの公式ドキュメント「Python チュートリアル」にも書かれています。その部分を抜粋すると、

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

はっきりと「値渡し」と書いてあります。

そしてこの文章には脚注が付いていて、次のように記載されています。

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

ここには「参照渡しって書いちゃえばいいのかもしれないが、実際は値渡しなのでそうは書けない」と言う迷いがみて取れます。

Pythonの「参照の値渡し」とは

Pythonの関数への引数の渡し方は「参照の値渡し」と冒頭で述べました。これは実引数のオブジェクト参照の値が仮引数へ「値渡し」されることを意味します。つまりオブジェクト参照のコピーが仮引数に渡されます。

「参照の値渡し」を説明するために、まずは次のような関数を定義します。この関数は引数としてリストを受け取ります。

>>> def change_element(x):
...   print(id(x))
...   x[0] = 10
...   x = "str"
... 

この関数は最初に仮引数xが参照するオブジェクトの識別値(id()関数が返す値)を表示します。それからリストの1番目の要素を変更し、最後に仮引数xの値を文字列オブジェクト("str")へのオブジェクト参照へ変更しています。

この関数を使う次のコードを実行します。

>>> a = [1, 2, 3]
>>> id(a)
4429915968
>>> a
[1, 2, 3]
>>> change_element(a)
4429915968
>>> a
[10, 2, 3]

このコードは次のことを実行します。

  1. 「a = [1, 2, 3]」の文でリストオブジェクトが作成され、そのオブジェクトへの参照が変数aに代入されます。その後、リストオブジェクトの識別値(id()関数の返す値)とその内容を表示します。
  2. 引数に変数aを渡してchange_element()関数の呼び出します。このとき変数aの値が仮引数xにコピーされます。そして関数内部では次のことが実行されます。

    1. 仮引数xが参照するリストの識別値を表示します。表示された値は変数aが参照するオブジェクトの識別値と同じですので、aとxは同じオブジェクトを指しています。つまり、aのオブジェクト参照とyのオブジェクト参照の値は同じということです。
    2. 仮引数xが参照するリストの1番目の要素を変更します。
    3. 仮引数xへ文字列("str")への参照を代入します。これによりxの値は更新されます。
  3. 最後に変数aが参照するリストオブジェクトの内容を表示します。aが参照するリストは、関数内でxを通して変更していますので、関数呼び出し後は当然その変更が観測されます。

ここで最も重要なポイントは、関数内で仮引数xの値を変更しても、変数aの値を変更できないことです。もしPythonの関数が「参照渡し」だとすると、関数内で仮引数xを通して変数aの値を変更できるはずです。

したがって、Pythonの関数は「参照の値渡し」なのです。

関数へ渡すオブジェクトがミュータブルかイミュータブルかで何か違う?

Pythonの「値渡し」や「参照渡し」の記事で関数へ渡すオブジェクト(への参照)がミュータブルかイミュータブルかで動作の違いを説明をしているのをよく見かけます。

それに倣ってイミュータブルなオブジェクトを引数に渡す例も見てみましょう。この例ではイミュータブルなオブジェクトとして文字列を渡す例を見てみましょう。

>>> def do_nothing(y):
...   print(id(y))
...   y += " bar"
...   print(y)
...   print(id(y))
...
>>> b = "foo"
>>> do_nothing(b)
4429958896
foo bar
4429967792
>>> b
'foo'

関数内で引数に渡されたイミュータブルなオブジェクトを変更することは一切できません。イミュータブルなので当然です。出来ることはオブジェクトを参照したり、そのオブジェクトを使って新しいオブジェクトを作成することくらいです。

「y += " bar"」の文脈をそのまま読むと、yが指す文字列に" bar"を追加して、連結された文字列オブジェクトへの参照をyへ代入するとなり、一見文字列を変更しているように見えます。

しかし演算子+で文字列を連結すると、連結された新しい文字列オブジェクトが作成されその参照が返されます。そのため、連結した後のyが参照するオブジェクトの識別値は、連結前と異なります。

このように変数aが参照するオブジェクトを関数の中で一切変更できませんので、関数呼び出し後も変数aが参照するオブジェクトが何も変わらないことは当然のことです。

まとめ

この記事を書いていて「オブジェクト参照」について表現に苦心しました。説明したかったことが皆様に伝わりましたでしょうか。

なお、「オブジェクト参照」については次の記事でも少し擦れていますので、参照いただけると幸いです。