Pythonの関数の引数はどう渡される?|変数と参照から理解する

ソフトウェア

はじめに

Pythonで関数に変数を渡したとき、関数の中で値を変更すると、呼び出し元にも影響するのでしょうか。

まずは、整数とリストを使った次のコードを見てください。

def change_number(x):
    x = 20

def change_list(items):
    items.append(30)

a = 10
numbers = [10, 20]

change_number(a)
change_list(numbers)

print(a)
print(numbers)

実行結果は次のようになります。

10
[10, 20, 30]

整数の a は変わらないのに、リストの numbers には 30 が追加されました。同じように関数へ引数を渡したのに、なぜ結果が違うのでしょうか。

この違いを理解する鍵は、Pythonの変数がオブジェクトを参照する名前であること、そして「名前への再代入」と「オブジェクト自体の変更」が異なることです。

Pythonでは、引数として渡したオブジェクトへの参照が、関数内の引数名に束縛されます。関数内で何を操作するかによって、呼び出し元への影響が変わります。

この記事では、変数と参照の考え方を使って、関数の引数がどのように渡されるのかを順番に確認します。

この記事でわかること

この記事では、次の内容の理解を目指します。

  • Pythonの引数渡しが「値渡し」「参照渡し」とどう違うのか
  • 関数を呼び出すと、引数名とオブジェクトの間に何が起こるのか
  • 整数を関数内で再代入しても、呼び出し元が変わらない理由
  • リストを関数内で変更すると、呼び出し元にも影響する理由
  • リストの変更と再代入の違い、元のデータを守る方法

1.Pythonの引数は値渡し?参照渡し?

関数の引数の渡し方は、プログラミング言語によって異なります。一般に「値渡し」は値をコピーして渡す方式、「参照渡し」は呼び出し元の変数に対して関数側から代入できる方式として説明されます。

Pythonでは、関数を呼び出すと、渡されたオブジェクトが関数内の引数名に束縛されます。この方式は「オブジェクト共有渡し(call by sharing)」と呼ばれることもあります。

ここでコピーされるのは、リストや整数などのオブジェクトそのものではありません。関数内には新しい引数名が用意され、その名前が呼び出し元から渡されたオブジェクトを参照します。

そのため、関数内で引数名を別のオブジェクトに再代入しても、呼び出し元の名前の参照先は変わりません。一方、共有しているミュータブルなオブジェクトを変更すれば、その変更は呼び出し元からも見えます。

引数名は関数内で使われる別の名前ですが、最初は呼び出し元と同じオブジェクトを参照します。

2.関数に引数を渡すと何が起こるのか?

まず、最も単純な関数で確認しましょう。

def show_value(x):
    print(x)

a = 10
show_value(a)

実行結果は次のようになります。

10

a = 10 によって、名前 a が整数オブジェクト 10 を参照します。次に show_value(a) を呼び出すと、関数内の引数名 x も、その同じ整数オブジェクトを参照します。

この関係を図で確認してみましょう。

関数に引数を渡したときの参照関係

上の図では、a と x という2つの名前から、同じオブジェクト 10 へ矢印が伸びています。これは参照関係を説明する模式図であり、実際のメモリ配置を表すものではありません。

重要なのは、a と x が同じ名前になるわけではないことです。それぞれ独立した名前でありながら、同じオブジェクトを参照しています。

2-1.is で同じオブジェクトか確認する

オブジェクトの同一性は、is を使って確認できます。

def check_reference(x):
    print(x is a)

a = [10, 20]
check_reference(a)

実行結果は次のようになります。

True

x is a が True なのは、関数内の x と呼び出し元の a が同じリストオブジェクトを参照しているからです。この例では同一性の確認だけを目的として、関数内から外側の a を参照しています。

is と == の違いについては、前の記事で詳しく解説しています。

▶ 「値の等価性」と「オブジェクトの同一性」の違いを復習したい
Pythonの「==」と「is」の違いとは?|値とオブジェクトの同一性を理解する
Pythonの「==」と「is」は何が違うのでしょうか。値の等価性とオブジェクトの同一性を、変数の参照や代入・コピーの仕組みから図解。id()による確認やNoneとの比較など、具体例を使って初心者向けにわかりやすく解説します。

3.整数を関数内で再代入しても呼び出し元が変わらない理由

次は、関数内で引数名に別の値を代入してみましょう。

def change_number(x):
    x = 20
    print("関数内:", x)

a = 10
change_number(a)
print("呼び出し元:", a)

実行結果は次のようになります。

関数内: 20
呼び出し元: 10

関数内では x が 20 になりましたが、呼び出し元の a は 10 のままです。

3-1.再代入は名前の参照先を変更する

x = 20 は、整数オブジェクト 10 の中身を 20 に書き換える操作ではありません。関数内の名前 x を、整数オブジェクト 20 に新しく束縛する操作です。

このとき、呼び出し元の a は引き続き 10 を参照しています。図で見ると、2つの名前の参照先が分かれます。

関数内で再代入したときの参照先の変化

上の図では、再代入後の x だけが 20 を指しています。呼び出し元の a の矢印は変わりません。

整数はイミュータブル(変更不可能)なオブジェクトです。そのため、整数の値そのものを書き換えることはできません。ただし、ここで呼び出し元が変わらない直接の理由は、関数内の名前を再代入しても、呼び出し元の名前は再代入されないことです。

4.リストを変更すると呼び出し元にも影響する理由

次に、リストを関数に渡してみましょう。

def add_item(items):
    items.append(30)

numbers = [10, 20]
add_item(numbers)
print(numbers)

実行結果は次のようになります。

[10, 20, 30]

今度は、呼び出し元の numbers にも 30 が追加されました。これは、append() が引数名を別のリストに再代入するのではなく、参照先のリストオブジェクトそのものを変更するためです。

リストはミュータブル(変更可能)なオブジェクトです。関数内の items と呼び出し元の numbers は同じリストを参照しているので、どちらから見ても変更後の内容になります。

この関係を図で確認してみましょう。

リストの内容を変更したときの参照関係

上の図では、2つの名前の矢印は変わらず、参照先のリストの内容だけが変化しています。これが、呼び出し元からも変更が見える理由です。

整数のようなイミュータブルなオブジェクトと、リストのようなミュータブルなオブジェクトの違いについては、次の記事で詳しく解説しています。

▶ Pythonのミュータブル・イミュータブルの違いを復習したい
Pythonのミュータブル・イミュータブルとは?|型ごとの違いを整理
Pythonのミュータブル・イミュータブルとは?list、dict、set、int、str、tupleなどの型ごとの違いを図解で解説します。再代入とオブジェクトの変更、タプル内のリストを変更できる理由、参照の共有との関係まで、初心者向けにわかりやすく説明します。

4-1.要素への代入でも同じことが起こる

append() だけでなく、リストの要素を変更した場合も同様です。

def change_first(items):
    items[0] = 99

numbers = [10, 20]
change_first(numbers)
print(numbers)

実行結果は次のようになります。

[99, 20]

items[0] = 99 は、items という名前の参照先を変更するのではなく、参照先のリストの先頭要素を変更します。そのため、呼び出し元のリストにも結果が反映されます。

5.リストでも再代入すれば呼び出し元は変わらない

ここまでの結果だけを見ると、整数は呼び出し元に影響せず、リストは影響するように思えるかもしれません。しかし、違いを決めているのは型だけではありません。

リストでも、関数内で引数名を再代入するだけなら、呼び出し元の参照先は変わりません。

def change_list(items):
    items = [100, 200]
    print("関数内:", items)

numbers = [10, 20]
change_list(numbers)
print("呼び出し元:", numbers)

実行結果は次のようになります。

関数内: [100, 200]
呼び出し元: [10, 20]

items = [100, 200] では、新しいリストオブジェクトが作られ、関数内の items がそのリストを参照するようになります。一方、numbers は元の [10, 20] を参照したままです。

つまり、整数でもリストでも、引数名への再代入は呼び出し元の名前を変更しません。リストの場合に呼び出し元へ影響するのは、共有しているリスト自体を変更したときです。

6.引数の再代入とオブジェクトの変更を比較する

ここまでの違いを整理してみましょう。

関数内の操作 起こること 呼び出し元への影響
x = 20 引数名を別の整数に再束縛 なし
items.append(30) 共有しているリストを変更 あり
items[0] = 99 共有しているリストの要素を変更 あり
items = [100, 200] 引数名を別のリストに再束縛 なし

ここでの「影響なし」は、例に示した操作によって、呼び出し元の名前や元のオブジェクトが変更されないことを意味します。

この表からわかるように、items がリストを参照していても、操作が再代入なのかオブジェクトの変更なのかによって結果は異なります。

「ミュータブルだから必ず呼び出し元が変わる」のではありません。共有しているオブジェクトを変更したときに、その変更が呼び出し元からも見えます。

7.呼び出し元のリストを変更したくない場合は?

関数を作るときは、渡されたリストを直接変更してよいかどうかを意識することが大切です。意図しない変更を避けたい場合は、元のリストを変更しない処理を選べます。

例えば、sort() は元のリストを並べ替えます。

def sort_numbers(items):
    items.sort()

numbers = [30, 10, 20]
sort_numbers(numbers)
print(numbers)

実行結果は次のようになります。

[10, 20, 30]

一方、sorted() は新しいリストを返すため、元のリストの並び順を変えずに結果を得られます。

def sort_numbers(items):
    return sorted(items)

numbers = [30, 10, 20]
result = sort_numbers(numbers)

print("元のリスト:", numbers)
print("並べ替え結果:", result)

実行結果は次のようになります。

元のリスト: [30, 10, 20]
並べ替え結果: [10, 20, 30]

このように、関数が元のオブジェクトを変更するのか、新しいオブジェクトを返すのかを区別すると、予期しない動作を防ぎやすくなります。

また、items.copy() を使ってリストをコピーする方法もあります。ただし、これは浅いコピーであり、入れ子になったリストなどの内部オブジェクトは共有されることがあります。コピーの詳細は次の記事で解説しています。

▶ Pythonの浅いコピーと深いコピーの違いを復習したい
Pythonの代入とコピーの違いとは?|copyとdeepcopyを理解する
Pythonの代入とコピーは何が違うのでしょうか。代入、浅いコピー、深いコピーの違いを、オブジェクトと参照の仕組みから図解。copyやdeepcopyの使い分けまで、具体例を使ってわかりやすく解説します。

まとめ

Pythonの関数に引数を渡したときの仕組みを、整数とリストの例で確認しました。今回のポイントを整理します。

  • 関数を呼び出すと、引数名は渡されたオブジェクトを参照する
  • 関数内の引数名と呼び出し元の変数名は別の名前である
  • 引数名に再代入しても、呼び出し元の名前の参照先は変わらない
  • 共有しているミュータブルなオブジェクトを変更すると、呼び出し元からも変更が見える
  • 呼び出し元のデータを変更したくない場合は、新しいオブジェクトを返す処理などを選ぶ

Pythonの引数渡しを理解する鍵は、「名前の再束縛」と「オブジェクト自体の変更」を区別することです。

これまで学んだ変数・参照・ミュータブル・イミュータブルの考え方は、関数の中でも同じように成り立ちます。

次回の記事では、関数内の名前と関数外の名前が、どの範囲で使えるのかを考えます。ローカル変数とグローバル変数の違いを理解することで、今回の「関数内の引数名は別の名前」という考え方をさらに深めていきます。

関連記事

▶ 前の学習テーマ:Pythonの「==」と「is」の違いとは?|値とオブジェクトの同一性を理解する
Pythonの「==」と「is」の違いとは?|値とオブジェクトの同一性を理解する
Pythonの「==」と「is」は何が違うのでしょうか。値の等価性とオブジェクトの同一性を、変数の参照や代入・コピーの仕組みから図解。id()による確認やNoneとの比較など、具体例を使って初心者向けにわかりやすく解説します。
▶ 次回:Pythonのスコープとは?|ローカル変数とグローバル変数の違い
Pythonのスコープとは?|ローカル変数とグローバル変数の違い
Pythonのスコープとは何か?ローカル変数とグローバル変数の違いを、コードと図でわかりやすく解説します。関数内外で変数を参照できる範囲や、同じ名前の変数がある場合の動作、UnboundLocalErrorが発生する理由を、名前とオブジェクトの参照関係から理解します。

Part 2:Pythonの関数とスコープをまとめて理解する

▶ Pythonの関数とスコープを理解する|引数・LEGB・クロージャ

(準備中)

関連書籍

Pythonの関数やオブジェクトの仕組みをさらに学びたい人には『入門 Python 3』もおすすめです。基本的な文法から関数やデータ構造まで確認したいときに役立ちます。

Amazon.co.jp
ソフトウェア
スポンサーリンク
toshi

京都市在住のエンジニア。読書好きが高じてブログを始め、現在は数学やAI・ソフトウェアを中心に、おすすめの小説についても記事を書いています。

toshiをフォローする
シェアする
toshiをフォローする

コメント

タイトルとURLをコピーしました