Pythonの引数渡しを正しく理解する|値渡し・参照渡しではなく pass-by-assignment

1. はじめに:Pythonの引数渡しはなぜ誤解されやすいのか

Pythonを学び始めた多くの人が、関数の引数について調べる過程で
「Pythonには値渡しと参照渡しがある」
「ミュータブルは参照渡し、イミュータブルは値渡し」
といった説明を目にします。

一見すると分かりやすそうですが、この説明はPythonの仕様を正確に表しているとは言えません
実際には、この言い回しが原因で「なぜこのコードは値が変わるのか」「なぜ別のケースでは変わらないのか」が分からなくなり、混乱してしまう人が少なくありません。

この誤解が生まれやすい最大の理由は、「値渡し」「参照渡し」という用語が、他のプログラミング言語の考え方を前提にしている点にあります。
C言語やC++、あるいは一部の解説書で使われる「参照渡し」という概念を、そのままPythonに当てはめてしまうと、挙動の説明が破綻してしまうのです。

Pythonには、一般的な意味での「参照渡し(pass by reference)」は存在しません。
それにもかかわらず、ミュータブルなオブジェクトを関数に渡したときに「呼び出し元の値が変わった」ように見えるため、結果だけを見て「これは参照渡しだ」と説明されてしまうケースが多くあります。

本記事では、このような表面的な分類を一度リセットし、Python公式の仕様に沿った形で、引数がどのように扱われているのかを順を追って解説します。
初心者の方でも理解できるよう、専門用語は必要最低限に抑えつつ、「なぜそうなるのか」という理由に重点を置いて説明していきます。

Ad

2. 結論先出し:Pythonに「参照渡し」は存在しない

最初に結論から述べます。
Pythonには、一般的な意味での「参照渡し(pass by reference)」は存在しません。

この一文だけを見ると、「え? でもリストを渡したら中身が変わるじゃないか」と感じるかもしれません。
その感覚は自然ですが、それは“参照渡しだから”ではありません

Pythonの引数渡しは、公式には次のように整理されます。

  • Pythonの引数渡しは 代入によって行われる

  • 英語では pass-by-assignment、あるいは call by object reference と説明される

  • 「値渡し(call by value)」だが、その値は オブジェクトへの参照

ここで重要なのは、「参照そのものを渡す(参照渡し)」のではなく、
“オブジェクトへの参照という値”が渡されているという点です。

Pythonでは、関数を呼び出すときに次のことが起きています。

  • 呼び出し元の変数が指しているオブジェクト

  • そのオブジェクトへの参照が

  • 関数側の引数名に 代入 される

つまり、関数の引数は
「呼び出し元の変数そのもの」ではなく
「同じオブジェクトを指す別の名前」として作られます。

この仕組みのため、

  • 関数内で 別のオブジェクトを代入 すると、呼び出し元には影響しない

  • 関数内で 同じオブジェクトを変更 すると、結果として呼び出し元にも影響が見える

という挙動の違いが生まれます。

多くの解説で使われがちな
「ミュータブルは参照渡し」
「イミュータブルは値渡し」
という説明は、結果だけを見たラベル付けに過ぎません。

Pythonの仕様として本当に理解すべきなのは、

  • 引数は常に代入で渡される

  • 影響が出るかどうかは、オブジェクトの変更方法によって決まる

という点です。

Ad

3. Pythonの引数渡しの正確なモデル

ここまでで、Pythonには一般的な意味での「参照渡し」が存在しないことを説明しました。
この点を本当に理解するためには、Pythonにおける「変数」と「代入」の考え方を整理する必要があります。

3.1 変数は「箱」ではなく「名前(ラベル)」

他の言語の説明では、変数を「値を入れる箱」として表現することがあります。
しかし、Pythonではこのイメージが誤解の原因になります。

Pythonの変数は、オブジェクトそのものではなく、オブジェクトに付けられた名前です。
つまり、変数は「箱」ではなく「ラベル」や「タグ」に近い存在です。

たとえば、次のようなコードを考えてみましょう。

a = 10
b = a

このとき、

  • 数値オブジェクト 10 が1つ存在する

  • そのオブジェクトに a という名前が付く

  • 同じオブジェクトに b という別の名前も付く

という状態になります。

ここで ab は別々の変数名ですが、指しているオブジェクトは同じです。
ただし、ab が「同一の変数」になるわけではありません。

この「名前がオブジェクトを指す」という考え方が、Pythonの引数渡しを理解する土台になります。

3.2 関数呼び出し時の内部動作

次に、関数を呼び出したときに何が起きているのかを見てみましょう。

def show_value(x):
    print(x)

y = 5
show_value(y)

このコードでは、関数 show_valuey を渡しています。
ここで起きているのは、次のような流れです。

  1. 数値オブジェクト 5 が存在する

  2. 変数 y は、そのオブジェクト 5 を指している

  3. 関数呼び出し時に、引数 x同じオブジェクトへの参照が代入される

重要なのは、xy そのものになるわけではない点です。
x はあくまで「関数内で新しく作られた名前」であり、
たまたま y と同じオブジェクトを指しているだけです。

そのため、関数内で x に別の値を代入しても、

def change_value(x):
    x = 100

y = 5
change_value(y)
print(y)

この場合、x は新しいオブジェクト 100 を指すようになりますが、
y が指しているオブジェクトは変わらないため、y の値は 5 のままです。

この仕組みこそが、「参照渡しではない」と言われる理由です。
関数内の代入は、呼び出し元の変数名には影響しません。

次のセクションでは、このモデルを踏まえた上で、
イミュータブルとミュータブルの違いが、なぜ挙動の差として現れるのか
を具体例とともに見ていきます。

Ad

4. イミュータブルとミュータブルの違いが結果を分ける

前のセクションで、Pythonの引数渡しは「代入によって行われる」ことを説明しました。
この仕組み自体は、すべてのオブジェクトで共通です。

それでも実際のコードでは、

  • 値が変わらないケース

  • 値が変わってしまうケース

が存在します。
この違いを生むのが、オブジェクトがイミュータブルかミュータブルかという性質です。

4.1 イミュータブルオブジェクトの場合(再代入)

イミュータブルとは、「一度作成されたら中身を変更できない」性質を持つオブジェクトです。
代表的なものには、次のような型があります。

  • 整数(int)

  • 浮動小数点数(float)

  • 文字列(str)

  • タプル(tuple)

これらを関数に渡した場合、関数内で行われる変更は、必ず再代入になります。

def double_value(x):
    x = x * 2

n = 10
double_value(n)
print(n)

このコードでは、関数内で x を2倍していますが、n の値は変わりません。
これは、次のような流れが起きているためです。

  • nx は、最初は同じ数値オブジェクトを指している

  • x * 2 によって、新しい数値オブジェクトが生成される

  • x はその新しいオブジェクトを指すように再代入される

ここで重要なのは、元の数値オブジェクトは一切変更されていないという点です。
イミュータブルなオブジェクトでは、「中身を書き換える」という操作自体が存在しないため、
結果として呼び出し元に影響が及ぶことはありません。

4.2 ミュータブルオブジェクトの場合(破壊的変更)

一方、ミュータブルオブジェクトは、生成後に中身を変更できる性質を持ちます。
代表例は次の通りです。

  • リスト(list)

  • 辞書(dict)

  • 集合(set)

これらを関数に渡すと、「参照渡しのような挙動」に見えるケースが現れます。

def add_item(items):
    items.append("new")

data = ["a", "b"]
add_item(data)
print(data)

この場合、data の中身は関数呼び出し後に変化します。
これは、次の理由によるものです。

  • dataitems は、同じリストオブジェクトを指している

  • append は、そのオブジェクト自体を変更する操作である

  • そのため、変更結果が呼び出し元からも見える

ここで起きているのは、**再代入ではなく破壊的変更(mutation)**です。
関数内で別のオブジェクトを代入しているわけではなく、
同一オブジェクトの中身を直接書き換えているため、影響が共有されます。

この挙動が、「ミュータブルは参照渡し」と誤解される最大の原因です。
しかし実際には、引数の渡し方が変わっているわけではありません

Ad

5. よくある誤解とNGな説明

ここまで理解できていれば、Pythonの引数渡しについて、かなり正確なイメージを持てているはずです。
このセクションでは、学習中によく目にする 誤解を招きやすい説明 を取り上げ、「なぜそれが正確ではないのか」を整理します。

5.1 「Pythonには値渡しと参照渡しがある」という誤解

もっともよく見かける説明がこれです。

  • イミュータブルは値渡し

  • ミュータブルは参照渡し

一見すると挙動を説明できているように見えますが、Pythonの仕様としては誤りです。
Pythonでは、すべての引数が同じルール、つまり 代入によって渡されます

挙動の違いは「渡し方」ではなく、

  • 再代入しているのか

  • 破壊的変更をしているのか

という 操作の違い によって生じています。

この説明を信じてしまうと、「参照渡しだから変わる」という曖昧な理解になり、
なぜあるコードでは影響が出て、別のコードでは出ないのかを説明できなくなります。

5.2 「参照がコピーされる」「アドレスが渡される」という表現

次によくあるのが、

  • 参照がコピーされる

  • メモリアドレスが渡される

といった説明です。

これも直感的には分かりやすそうですが、Pythonの公式な考え方とはズレがあります。
Pythonでは、参照という概念をユーザーが直接操作することはできません

重要なのは、

  • オブジェクトが存在する

  • 変数名がそのオブジェクトを指す

  • 関数呼び出し時に、その指し先が別の名前に代入される

というモデルです。

「アドレス」や「ポインタ」という言葉を使ってしまうと、
C言語的な参照渡しを連想させてしまい、かえって混乱を招きます。

5.3 「ミュータブルは危険だから使うな」という極端な理解

ミュータブルオブジェクトの挙動を学ぶと、

  • ミュータブルは危険

  • 使わない方がよい

と感じる人もいますが、これは正しい理解ではありません。

ミュータブルなオブジェクトは、

  • 状態を持つデータ構造

  • 効率的な更新処理

に欠かせない存在です。

問題になるのは、「ミュータブルであること」そのものではなく、
意図せず共有された状態を変更してしまう設計です。

正しい仕組みを理解していれば、ミュータブルは非常に便利な道具になります。

Ad

6. 実例で理解する:再代入と破壊的変更の違い

ここまでの説明で、「影響が出る/出ない」を分けているのは
引数の渡し方ではなく、関数内で行っている操作の種類だという点が見えてきたはずです。
このセクションでは、具体的なコードを使ってその違いを整理します。

6.1 再代入の例(呼び出し元に影響しない)

まずは、関数内で再代入を行うケースです。

def replace_value(x):
    x = 999
    print("関数内:", x)

value = 10
replace_value(value)
print("関数外:", value)

このコードの結果は次のようになります。

関数内: 999
関数外: 10

ここで起きていることは非常に単純です。

  • valuex は最初、同じオブジェクトを指している

  • x = 999 によって、x は別のオブジェクトを指すようになる

  • value が指しているオブジェクトは一切変更されない

つまり、再代入は変数名の付け替えに過ぎず、共有状態を変更しないということです。
この挙動は、イミュータブル・ミュータブルを問わず共通です。

6.2 破壊的変更の例(呼び出し元に影響する)

次に、**破壊的変更(mutation)**を行うケースを見てみましょう。

def update_list(items):
    items.append("C")
    print("関数内:", items)

data = ["A", "B"]
update_list(data)
print("関数外:", data)

結果は次の通りです。

関数内: ['A', 'B', 'C']
関数外: ['A', 'B', 'C']

この場合、

  • dataitems は同じリストオブジェクトを指している

  • append はオブジェクトそのものを変更する操作

  • 同じオブジェクトを共有しているため、変更結果が両方から見える

という流れになります。

ここで重要なのは、代入は一切行われていないという点です。
あくまで「同一オブジェクトに対する変更」が起きているだけです。

6.3 ミュータブルでも影響しないケース

ミュータブルなオブジェクトであっても、再代入を行えば影響は出ません

def reset_list(items):
    items = []
    items.append("X")
    print("関数内:", items)

data = ["A", "B"]
reset_list(data)
print("関数外:", data)

出力は次のようになります。

関数内: ['X']
関数外: ['A', 'B']

この例では、

  • items = [] によって、新しいリストが作られる

  • items はその新しいリストを指す

  • 元の data とは無関係になる

という挙動になります。

このコードを正しく説明できるようになれば、
「ミュータブルは参照渡し」という説明が不正確であることがはっきり分かるはずです。

Ad

7. 安全に扱うための実践テクニック

ここまでで、Pythonの引数渡しの仕組みと、再代入・破壊的変更の違いは整理できました。
このセクションでは、その理解を前提に、実務や学習の中で安全にコードを書くための具体的な方法を紹介します。

7.1 オブジェクトのコピーを使う

関数内でミュータブルオブジェクトを変更したいが、
呼び出し元には影響を与えたくないというケースは非常によくあります。

その場合は、引数として受け取ったオブジェクトを コピーしてから操作するのが基本です。

def add_item_safely(items):
    copied = items.copy()
    copied.append("new")
    return copied

original = ["A", "B"]
result = add_item_safely(original)

print("結果:", result)
print("元のリスト:", original)

このコードでは、

  • copy() によって新しいリストを作成

  • 関数内の変更はコピーにのみ反映

  • 元のリストは変更されない

という安全な挙動になります。

なお、コピーには2種類あります。

  • 浅いコピーlist.copy()copy.copy()

  • 深いコピーcopy.deepcopy()

ネストしたデータ構造(リストの中にリストや辞書がある場合)では、
深いコピーが必要になるケースもあるため、構造に応じて使い分けることが重要です。

7.2 デフォルト引数にミュータブルを使わない

Python初心者が特につまずきやすいのが、デフォルト引数にミュータブルを指定するケースです。

def add_item(item, items=[]):
    items.append(item)
    return items

この関数は、一見問題なさそうに見えますが、
複数回呼び出すと、以前の状態が残るという挙動になります。

理由は、デフォルト引数が

  • 関数定義時に一度だけ評価され

  • その同じオブジェクトが使い回される

ためです。

これを避けるための定石が、None を使う方法です。

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

この書き方にすると、

  • 呼び出しごとに新しいリストが作られる

  • 意図しない状態共有が発生しない

という安全な挙動になります。

7.3 関数の役割を明確にする

引数の扱いでトラブルを防ぐためには、
その関数が「変更する関数」なのか「変更しない関数」なのかを明確にすることも重要です。

たとえば、

  • 引数を変更する関数
    → ドキュメントや関数名で明示する

  • 変更しない関数
    → コピーを取る、または新しいオブジェクトを返す

といった設計にすると、コードを読む側も安心して使えます。

Ad

8. 他のプログラミング言語との比較

Pythonの引数渡しが分かりにくく感じられる理由のひとつは、
他の言語で学んだ概念を、そのまま当てはめてしまうことにあります。
ここでは代表的な言語と比較しながら、Pythonの立ち位置を整理します。

8.1 C / C++ の参照渡し

C言語では、基本的に関数は値渡しです。
呼び出し元の値を直接変更したい場合は、ポインタを使ってアドレスを渡します。

C++では、さらに「参照渡し」という仕組みがあり、

  • 引数が呼び出し元の変数そのものを指す

  • 関数内で代入すると、呼び出し元も変更される

という明確な仕様があります。

このように、参照渡しが言語仕様として存在するのが、C/C++の特徴です。

8.2 Java の引数渡し

Javaでは、基本型(int など)とオブジェクト型で挙動が異なるように見えますが、
仕様としては 常に値渡しです。

ただし、オブジェクト型の場合、その「値」が
オブジェクトへの参照であるため、

  • フィールドを書き換えると影響が出る

  • 再代入すると影響が出ない

という挙動になります。

この点は、Pythonとかなり近い考え方です。

8.3 Python の位置づけ

PythonもJavaと同様に、

  • 引数は常に値渡し

  • ただし値は「オブジェクトへの参照」

というモデルを採用しています。

違いがあるとすれば、Pythonでは

  • すべてがオブジェクト

  • 変数は単なる名前(ラベル)

という思想が、より徹底されている点です。

そのため、Pythonでは
「参照渡し」という言葉を使う必要がそもそもない
という結論に自然と行き着きます。

この視点を持っておくと、言語間の違いに振り回されず、
Pythonらしいコードの読み書きができるようになります。

Ad

9. まとめ:覚えるべき本質は3つだけ

最後に、この記事の要点を整理します。
Pythonの引数渡しで本当に覚えるべきことは、次の3点だけです。

  1. Pythonの引数渡しは pass-by-assignment(代入で渡される)
    参照渡しという仕組みは存在しない。

  2. 挙動の違いは 再代入か、破壊的変更か によって決まる
    渡し方ではなく、操作内容が結果を分けている。

  3. ミュータブルは「危険」ではなく、理解せずに使うことが危険
    コピーや設計で安全に扱える。

「値渡し/参照渡し」という言葉に頼らず、
オブジェクト・名前・代入という3つの視点で考えるようになると、
Pythonの関数引数に関する混乱はほぼなくなります。

この理解は、バグを減らすだけでなく、
読みやすく、意図が明確なコードを書くための土台になります。
ぜひ今後のPython学習や実務に活かしてください。

Ad
年収訴求