mei_13のPython講座 ロゴ

【解説】Pythonにおける「値」と「参照」の正体 — データの受け渡しを深く理解する




Pythonにおける「値」と「参照」の正体 — データの受け渡しを深く理解する


Hirokiのアイコン
【Hiroki】 あの、Yukiさん。今、Pythonの関数について勉強しているんですけど、少し混乱してしまって……。 関数の引数にリストを渡して、その中身を関数の中で書き換えたら、関数の外にある元のリストまで変わってしまったんです。 これって、他のプログラミング言語で言う「参照渡し」というものなんでしょうか?


Yukiのアイコン
【Yuki】 Hirokiくん、こんにちは。関数の挙動で迷ってしまったんですね。 Pythonのデータの扱いは、他の言語を触ったことがある人ほど、少し不思議に感じることがあるかもしれません。 実は、Pythonの引数の渡し方は、厳密には「参照渡し」でも「値渡し」でもなく、「オブジェクト参照渡し(Call by Object Reference)」と呼ばれているんです。 今日は、その仕組みについて、ゆっくり一緒に見ていきましょう。


Hirokiのアイコン
【Hiroki】 オブジェクト参照渡し……ですか。聞き慣れない言葉ですね。 具体的に、普通の「参照渡し」とは何が違うんでしょうか?


Yukiのアイコン
【Yuki】 それを理解するためには、まずPythonが「変数」をどのように扱っているかを知る必要があります。 多くの入門書では、変数は「データを入れる箱」だと説明されますが、Pythonの場合は「データに貼り付ける名札」だと考えるのが分かりやすいと思います。

変数は「箱」ではなく「名札」


Yukiのアイコン
【Yuki】 まず、簡単な代入の例を見てみましょう。 たとえば、a = [1, 2, 3] というコードを書いたとします。 このとき、メモリ上のどこかに [1, 2, 3] というリストの本体(オブジェクト)が作られて、そこに a という名前の名札がペタッと貼られるイメージです。


Hirokiのアイコン
【Hiroki】 名札……。箱に入れるんじゃなくて、既にあるものに名前を付けるだけなんですね。


Yukiのアイコン
【Yuki】 そうです。そして、次に b = a と書くと、どうなると思いますか? 新しい箱が作られて中身がコピーされるのではなく、同じ [1, 2, 3] というオブジェクトに、新しく b という名札が追加で貼られるだけなんです。 コードで確認してみましょう。

a = [1, 2, 3]
b = a

print(f"aのID: {id(a)}")
print(f"bのID: {id(b)}")

# bを通じて中身を変更してみる
b.append(4)
print(f"aの中身: {a}")


Hirokiのアイコン
【Hiroki】 あ、本当だ。id() 関数で確認すると、ab のIDが全く同じになりますね。 だから、b を操作すると、同じ実体を見ている a の中身も変わって見えるんだ。


Yukiのアイコン
【Yuki】 その通りです。これがPythonの基本原則なんです。 変数そのものがデータを持っているわけではなく、データがある場所を指し示しているだけ……。 これを「参照(リファレンス)」と呼びます。

変更できるもの(ミュータブル)とできないもの(イミュータブル)


Hirokiのアイコン
【Hiroki】 なるほど。でも、数値の場合は少し挙動が違いませんか? たとえば、x = 10 として y = x とした後で、y = 20 と書き換えても、x10 のままですよね。 リストの時みたいに x20 になったりはしません。


Yukiのアイコン
【Yuki】 Hirokiくん、鋭いですね。そこがPythonを理解する上でとても大切なポイントです。 Pythonのオブジェクトには、大きく分けて2つの種類があるんです。

  1. ミュータブル(Mutable): 変更可能なオブジェクト(リスト、辞書、集合など)
  2. イミュータブル(Immutable): 変更不可能なオブジェクト(数値、文字列、タプルなど)


Hirokiのアイコン
【Hiroki】 数値は「変更不可能」なんですか? でも、現に y = 20 って変更できていますけど……。


Yukiのアイコン
【Yuki】 そこが「名札」の考え方の面白いところなんです。 数値のようなイミュータブルなオブジェクトに対して y = 20 とするのは、「今ある 10 というデータを 20 に書き換える」のではなく、「新しく用意された 20 というデータに、名札 y を貼り替える」という動作になるんです。


Hirokiのアイコン
【Hiroki】 貼り替え! つまり、元の 10 というデータ自体は何も変わっていない、ということですね。


Yukiのアイコン
【Yuki】 その通りです。ちょっとコードを書いてみますね。

x = 10
y = x

print(f"変更前 - xのID: {id(x)}, yのID: {id(y)}")

y = y + 5  # 新しい値を作成して貼り替える

print(f"変更後 - xの値: {x}, yの値: {y}")
print(f"変更後 - xのID: {id(x)}, yのID: {id(y)}")


Yukiのアイコン
【Yuki】 このコードを実行すると、変更後の y のIDが変わっているのが分かるはずです。 一方、リストのようなミュータブルなオブジェクトは、オブジェクトそのものを「中から書き換える(破壊的な変更)」ことができます。 だから、名札を貼り替えなくても中身が変わってしまうんですね。

関数に引数を渡すときの挙動


Hirokiのアイコン
【Hiroki】 だんだん分かってきました。 じゃあ、僕が最初に悩んでいた「関数の引数」の話に戻ると……。 関数にリストを渡すときも、結局は「名札のコピー」を渡しているだけ、ということですか?


Yukiのアイコン
【Yuki】 はい、まさにそうです。 Pythonで関数を呼び出すとき、引数として渡されるのは「オブジェクトへの参照(名札の情報)」です。 関数の中の変数(仮引数)は、外側で渡したオブジェクトと同じものを指す新しい名札になります。


Hirokiのアイコン
【Hiroki】 つまり、関数の中でその名札を使って中身を書き換えると、外側のオブジェクトも変わってしまう……。 でも、関数の中で「貼り替え」を行ったらどうなるんでしょう?


Yukiのアイコン
【Yuki】 いい質問ですね。それを比較するコードを作ってみました。

def modify_list(target_list):
    # パターン1: オブジェクトそのものを操作する(破壊的変更)
    target_list.append("追加")
    print(f"関数内(append後): {target_list}")

def replace_list(target_list):
    # パターン2: 新しいオブジェクトを代入する(貼り替え)
    target_list = ["新しい", "リスト"]
    print(f"関数内(代入後): {target_list}")

my_list = [1, 2, 3]

print("--- modify_listを呼び出し ---")
modify_list(my_list)
print(f"関数呼び出し後のmy_list: {my_list}")

print("\n--- replace_listを呼び出し ---")
replace_list(my_list)
print(f"関数呼び出し後のmy_list: {my_list}")


Hirokiのアイコン
【Hiroki】 あ! replace_list の方は、関数の外にある my_list に影響を与えていませんね!


Yukiのアイコン
【Yuki】 そうなんです。 target_list = [...] と代入した瞬間に、関数内の target_list という名札は、外側の my_list とは別の、新しいオブジェクトを指すように貼り変わってしまったんです。 一方で append を使った場合は、名札が指している「同じオブジェクト」の中身をいじっているので、外側にも影響が出る……。 これが、Pythonにおける「参照」の正体だと思います。

なぜ「参照渡し」ではないと言われるのか


Hirokiのアイコン
【Hiroki】 すごくスッキリしました。 でも、これって結局「参照」を渡しているんだから、「参照渡し」って呼んでもいいような気がするんですけど……だめなんですか?


Yukiのアイコン
【Yuki】 うーん、そこは言葉の定義の問題なのですが……。 伝統的な言語(例えばC++など)での「参照渡し」は、「変数そのもの(エイリアス)」を渡します。 そのため、関数の中で代入(貼り替え)を行っても、呼び出し元の変数が指す先を強制的に変えることができるんです。


Hirokiのアイコン
【Hiroki】 Pythonだと、関数の中で target_list = ... としても、外側の my_list の指す先までは変えられませんでしたよね。


Yukiのアイコン
【Yuki】 はい。Pythonでは、「名札がどのオブジェクトを指しているか」という情報はコピーして渡されますが、「名札そのもの」を渡すわけではないんです。 だから、関数の中で名札を別の対象に貼り替えても、外側の名札には影響が及びません。 この「参照のコピーを渡す」という性質を指して、「オブジェクト参照渡し」や「値渡し(参照という値を渡している)」と呼ぶことが多いんです。

デフォルト引数の罠


Hirokiのアイコン
【Hiroki】 なるほど……。仕組みを理解していないと、思わぬところでバグを生んでしまいそうですね。


Yukiのアイコン
【Yuki】 そうですね。特によくある失敗として、「デフォルト引数にミュータブルなオブジェクト(空リストなど)を指定する」というものがあります。 これは、プログラミング初心者の方が一番驚くポイントかもしれません。


Hirokiのアイコン
【Hiroki】 えっ、デフォルト引数ですか?


Yukiのアイコン
【Yuki】 はい。ちょっとこのコードを見てみてください。

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

print(add_item("リンゴ"))
print(add_item("バナナ"))
print(add_item("ミカン"))


Hirokiのアイコン
【Hiroki】 えーっと、普通に考えたら、毎回新しい空のリストにアイテムが一つずつ入って、['リンゴ']['バナナ']……と表示されるはずですよね?


Yukiのアイコン
【Yuki】 そう思いますよね。でも、実際に実行すると結果はこうなります。

['リンゴ']
['リンゴ', 'バナナ']
['リンゴ', 'バナナ', 'ミカン']


Hirokiのアイコン
【Hiroki】 うわっ、中身が増えてる! なんでですか!?


Yukiのアイコン
【Yuki】 実は、Pythonのデフォルト引数は関数が定義された時に一度だけ評価され、そのオブジェクトがずっと使い回されるんです。 この場合、box=[] という空リストのオブジェクトが一つだけ作られ、関数を呼び出すたびに同じオブジェクトが参照されます。 ミュータブルなリストをデフォルト引数にすると、呼び出しの間で状態が共有されてしまうんですね。


Hirokiのアイコン
【Hiroki】 これは怖いですね……。もしこれを知らずに大規模なプログラムを書いていたら、原因不明のバグに頭を抱えそうです。


Yukiのアイコン
【Yuki】 そうかもしれませんね……。 この問題を避けるためには、デフォルト引数には None を指定して、関数の中でリストを作るのが一般的です。

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


Yukiのアイコン
【Yuki】 これなら、毎回新しくリストが作られるので安心ですね。

意図しない変更を防ぐには


Hirokiのアイコン
【Hiroki】 仕組みはよく分かりました。 でも、関数の外にあるリストを汚したくないけれど、リストの内容を元にした計算はしたい、という時はどうすればいいんでしょうか?


Yukiのアイコン
【Yuki】 その場合は、オブジェクトのコピーを渡すか、関数内でコピーを作る必要があります。 一番簡単なのは、リストのスライスを使う方法です。

my_list = [1, 2, 3]

# スライスを使ってコピーを渡す
modify_list(my_list[:])


Yukiのアイコン
【Yuki】 [:] と書くことで、元のリストと同じ内容の新しいリストオブジェクトが作成されます。 これを渡せば、関数の中で append などの破壊的な操作をしても、元のリストは守られます。 もっと複雑な、リストの中にリストが入っているような多重構造の場合は、copy モジュールの deepcopy を使うのが安全かもしれません。


Hirokiのアイコン
【Hiroki】 deepcopy……「深いコピー」ですね。


Yukiのアイコン
【Yuki】 はい。表面的な名札の並びだけじゃなくて、中身のオブジェクトまで全部新しく作ってくれるんです。 少しメモリを贅沢に使いますが、絶対に元のデータを壊したくないときには頼りになります。

まとめ


Hirokiのアイコン
【Hiroki】 今日はありがとうございました、Yukiさん。 「変数は名札」というイメージを持てたおかげで、Pythonの不思議な挙動がすんなり理解できました。


Yukiのアイコン
【Yuki】 そう言ってもらえると嬉しいです。 Pythonはコードが書きやすい分、こうした裏側の仕組みが隠れがちですが、知っておくとデバッグの時にきっと役に立つと思います。

  1. 変数はオブジェクトを指し示す「名札」である。
  2. 数値や文字列(イミュータブル)は書き換えられず、「貼り替え」が発生する。
  3. リストや辞書(ミュータブル)は、名札を貼ったまま中身を書き換えられる。
  4. 関数の引数には「名札の情報(参照)」が渡される。
  5. デフォルト引数にミュータブルなものを使うときは注意が必要。


Yukiのアイコン
【Yuki】 このあたりを意識しておけば、もう迷うことは少ないはずです。 Hirokiくんのプログラミング、これからも応援していますね。


Hirokiのアイコン
【Hiroki】 はい! また分からないことがあったら教えてください。 次は、さっき教えてもらった deepcopy についても自分でもっと調べてみようと思います。


Yukiのアイコン
【Yuki】 その意気です。一歩ずつ、楽しみながら学んでいきましょう。 それでは、今日はこのあたりで……。


参考文献



< マルチスレッドプログラミング
コラム一覧に戻る
pyenv >

この記事では基礎を解説しましたが、実務においては「もっと複雑なデータを扱いたい」「独自のシステムに組み込みたい」といった、個別の課題に直面することも多いはずです。

「自分で書く時間は最小限に抑え、プロの品質でツールを完成させたい」という方は、ぜひ一度ご相談ください。

「教わる」だけでなく「形にする」パートナーとして、フリーランスエンジニアのmei_13が最短ルートでの解決をサポートします。

➡ ココナラで制作・相談を依頼する(見積もり無料)


初心者から始められるPythonレッスン

プログラミング未経験者・初心者歓迎!
月額4,000円で質問し放題!!
● 完全オンライン
● 翌日までには必ず返信
● 挫折しない独自の学習メソッド
● 圧倒的高評価!!
テキストベースで時間を選ばない
● 高品質なサンプルコード
詳細はこちら
興味がある方はまず質問だけでもどうぞ!



AIアシスタント Yuki