1. はじめに:Pythonの引数渡しはなぜ誤解されやすいのか
Pythonを学び始めた多くの人が、関数の引数について調べる過程で
「Pythonには値渡しと参照渡しがある」
「ミュータブルは参照渡し、イミュータブルは値渡し」
といった説明を目にします。
一見すると分かりやすそうですが、この説明はPythonの仕様を正確に表しているとは言えません。
実際には、この言い回しが原因で「なぜこのコードは値が変わるのか」「なぜ別のケースでは変わらないのか」が分からなくなり、混乱してしまう人が少なくありません。
この誤解が生まれやすい最大の理由は、「値渡し」「参照渡し」という用語が、他のプログラミング言語の考え方を前提にしている点にあります。
C言語やC++、あるいは一部の解説書で使われる「参照渡し」という概念を、そのままPythonに当てはめてしまうと、挙動の説明が破綻してしまうのです。
Pythonには、一般的な意味での「参照渡し(pass by reference)」は存在しません。
それにもかかわらず、ミュータブルなオブジェクトを関数に渡したときに「呼び出し元の値が変わった」ように見えるため、結果だけを見て「これは参照渡しだ」と説明されてしまうケースが多くあります。
本記事では、このような表面的な分類を一度リセットし、Python公式の仕様に沿った形で、引数がどのように扱われているのかを順を追って解説します。
初心者の方でも理解できるよう、専門用語は必要最低限に抑えつつ、「なぜそうなるのか」という理由に重点を置いて説明していきます。
2. 結論先出し:Pythonに「参照渡し」は存在しない
最初に結論から述べます。
Pythonには、一般的な意味での「参照渡し(pass by reference)」は存在しません。
この一文だけを見ると、「え? でもリストを渡したら中身が変わるじゃないか」と感じるかもしれません。
その感覚は自然ですが、それは“参照渡しだから”ではありません。
Pythonの引数渡しは、公式には次のように整理されます。
Pythonの引数渡しは 代入によって行われる
英語では pass-by-assignment、あるいは call by object reference と説明される
「値渡し(call by value)」だが、その値は オブジェクトへの参照
ここで重要なのは、「参照そのものを渡す(参照渡し)」のではなく、
“オブジェクトへの参照という値”が渡されているという点です。
Pythonでは、関数を呼び出すときに次のことが起きています。
呼び出し元の変数が指しているオブジェクト
そのオブジェクトへの参照が
関数側の引数名に 代入 される
つまり、関数の引数は
「呼び出し元の変数そのもの」ではなく
「同じオブジェクトを指す別の名前」として作られます。
この仕組みのため、
関数内で 別のオブジェクトを代入 すると、呼び出し元には影響しない
関数内で 同じオブジェクトを変更 すると、結果として呼び出し元にも影響が見える
という挙動の違いが生まれます。
多くの解説で使われがちな
「ミュータブルは参照渡し」
「イミュータブルは値渡し」
という説明は、結果だけを見たラベル付けに過ぎません。
Pythonの仕様として本当に理解すべきなのは、
引数は常に代入で渡される
影響が出るかどうかは、オブジェクトの変更方法によって決まる
という点です。
3. Pythonの引数渡しの正確なモデル
ここまでで、Pythonには一般的な意味での「参照渡し」が存在しないことを説明しました。
この点を本当に理解するためには、Pythonにおける「変数」と「代入」の考え方を整理する必要があります。
3.1 変数は「箱」ではなく「名前(ラベル)」
他の言語の説明では、変数を「値を入れる箱」として表現することがあります。
しかし、Pythonではこのイメージが誤解の原因になります。
Pythonの変数は、オブジェクトそのものではなく、オブジェクトに付けられた名前です。
つまり、変数は「箱」ではなく「ラベル」や「タグ」に近い存在です。
たとえば、次のようなコードを考えてみましょう。
a = 10
b = a
このとき、
数値オブジェクト
10が1つ存在するそのオブジェクトに
aという名前が付く同じオブジェクトに
bという別の名前も付く
という状態になります。
ここで a と b は別々の変数名ですが、指しているオブジェクトは同じです。
ただし、a と b が「同一の変数」になるわけではありません。
この「名前がオブジェクトを指す」という考え方が、Pythonの引数渡しを理解する土台になります。
3.2 関数呼び出し時の内部動作
次に、関数を呼び出したときに何が起きているのかを見てみましょう。
def show_value(x):
print(x)
y = 5
show_value(y)
このコードでは、関数 show_value に y を渡しています。
ここで起きているのは、次のような流れです。
数値オブジェクト
5が存在する変数
yは、そのオブジェクト5を指している関数呼び出し時に、引数
xに 同じオブジェクトへの参照が代入される
重要なのは、x が y そのものになるわけではない点です。
x はあくまで「関数内で新しく作られた名前」であり、
たまたま y と同じオブジェクトを指しているだけです。
そのため、関数内で x に別の値を代入しても、
def change_value(x):
x = 100
y = 5
change_value(y)
print(y)
この場合、x は新しいオブジェクト 100 を指すようになりますが、
y が指しているオブジェクトは変わらないため、y の値は 5 のままです。
この仕組みこそが、「参照渡しではない」と言われる理由です。
関数内の代入は、呼び出し元の変数名には影響しません。
次のセクションでは、このモデルを踏まえた上で、
イミュータブルとミュータブルの違いが、なぜ挙動の差として現れるのか
を具体例とともに見ていきます。
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 の値は変わりません。
これは、次のような流れが起きているためです。
nとxは、最初は同じ数値オブジェクトを指しているx * 2によって、新しい数値オブジェクトが生成されるxはその新しいオブジェクトを指すように再代入される
ここで重要なのは、元の数値オブジェクトは一切変更されていないという点です。
イミュータブルなオブジェクトでは、「中身を書き換える」という操作自体が存在しないため、
結果として呼び出し元に影響が及ぶことはありません。
4.2 ミュータブルオブジェクトの場合(破壊的変更)
一方、ミュータブルオブジェクトは、生成後に中身を変更できる性質を持ちます。
代表例は次の通りです。
リスト(list)
辞書(dict)
集合(set)
これらを関数に渡すと、「参照渡しのような挙動」に見えるケースが現れます。
def add_item(items):
items.append("new")
data = ["a", "b"]
add_item(data)
print(data)
この場合、data の中身は関数呼び出し後に変化します。
これは、次の理由によるものです。
dataとitemsは、同じリストオブジェクトを指しているappendは、そのオブジェクト自体を変更する操作であるそのため、変更結果が呼び出し元からも見える
ここで起きているのは、**再代入ではなく破壊的変更(mutation)**です。
関数内で別のオブジェクトを代入しているわけではなく、
同一オブジェクトの中身を直接書き換えているため、影響が共有されます。
この挙動が、「ミュータブルは参照渡し」と誤解される最大の原因です。
しかし実際には、引数の渡し方が変わっているわけではありません。
5. よくある誤解とNGな説明
ここまで理解できていれば、Pythonの引数渡しについて、かなり正確なイメージを持てているはずです。
このセクションでは、学習中によく目にする 誤解を招きやすい説明 を取り上げ、「なぜそれが正確ではないのか」を整理します。
5.1 「Pythonには値渡しと参照渡しがある」という誤解
もっともよく見かける説明がこれです。
イミュータブルは値渡し
ミュータブルは参照渡し
一見すると挙動を説明できているように見えますが、Pythonの仕様としては誤りです。
Pythonでは、すべての引数が同じルール、つまり 代入によって渡されます。
挙動の違いは「渡し方」ではなく、
再代入しているのか
破壊的変更をしているのか
という 操作の違い によって生じています。
この説明を信じてしまうと、「参照渡しだから変わる」という曖昧な理解になり、
なぜあるコードでは影響が出て、別のコードでは出ないのかを説明できなくなります。
5.2 「参照がコピーされる」「アドレスが渡される」という表現
次によくあるのが、
参照がコピーされる
メモリアドレスが渡される
といった説明です。
これも直感的には分かりやすそうですが、Pythonの公式な考え方とはズレがあります。
Pythonでは、参照という概念をユーザーが直接操作することはできません。
重要なのは、
オブジェクトが存在する
変数名がそのオブジェクトを指す
関数呼び出し時に、その指し先が別の名前に代入される
というモデルです。
「アドレス」や「ポインタ」という言葉を使ってしまうと、
C言語的な参照渡しを連想させてしまい、かえって混乱を招きます。
5.3 「ミュータブルは危険だから使うな」という極端な理解
ミュータブルオブジェクトの挙動を学ぶと、
ミュータブルは危険
使わない方がよい
と感じる人もいますが、これは正しい理解ではありません。
ミュータブルなオブジェクトは、
状態を持つデータ構造
効率的な更新処理
に欠かせない存在です。
問題になるのは、「ミュータブルであること」そのものではなく、
意図せず共有された状態を変更してしまう設計です。
正しい仕組みを理解していれば、ミュータブルは非常に便利な道具になります。
6. 実例で理解する:再代入と破壊的変更の違い
ここまでの説明で、「影響が出る/出ない」を分けているのは
引数の渡し方ではなく、関数内で行っている操作の種類だという点が見えてきたはずです。
このセクションでは、具体的なコードを使ってその違いを整理します。
6.1 再代入の例(呼び出し元に影響しない)
まずは、関数内で再代入を行うケースです。
def replace_value(x):
x = 999
print("関数内:", x)
value = 10
replace_value(value)
print("関数外:", value)
このコードの結果は次のようになります。
関数内: 999
関数外: 10
ここで起きていることは非常に単純です。
valueとxは最初、同じオブジェクトを指している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']
この場合、
dataとitemsは同じリストオブジェクトを指している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とは無関係になる
という挙動になります。
このコードを正しく説明できるようになれば、
「ミュータブルは参照渡し」という説明が不正確であることがはっきり分かるはずです。
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 関数の役割を明確にする
引数の扱いでトラブルを防ぐためには、
その関数が「変更する関数」なのか「変更しない関数」なのかを明確にすることも重要です。
たとえば、
引数を変更する関数
→ ドキュメントや関数名で明示する変更しない関数
→ コピーを取る、または新しいオブジェクトを返す
といった設計にすると、コードを読む側も安心して使えます。
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らしいコードの読み書きができるようになります。
9. まとめ:覚えるべき本質は3つだけ
最後に、この記事の要点を整理します。
Pythonの引数渡しで本当に覚えるべきことは、次の3点だけです。
Pythonの引数渡しは pass-by-assignment(代入で渡される)
参照渡しという仕組みは存在しない。挙動の違いは 再代入か、破壊的変更か によって決まる
渡し方ではなく、操作内容が結果を分けている。ミュータブルは「危険」ではなく、理解せずに使うことが危険
コピーや設計で安全に扱える。
「値渡し/参照渡し」という言葉に頼らず、
オブジェクト・名前・代入という3つの視点で考えるようになると、
Pythonの関数引数に関する混乱はほぼなくなります。
この理解は、バグを減らすだけでなく、
読みやすく、意図が明確なコードを書くための土台になります。
ぜひ今後のPython学習や実務に活かしてください。



