【Python】参照渡しとは?初心者向けにわかりやすく解説

はじめに

皆さんはPythonでプログラムを書いているときに、リストや辞書といったデータを関数の引数に渡したら、なぜか外側の変数の中身まで変わってしまったという経験はないでしょうか。

これは参照渡しという言葉で説明されることが多く、関数にオブジェクトを渡すとき、Pythonがどのように内部でデータを処理しているのかを理解すると納得しやすいです。

ただし、厳密には「Pythonは参照渡しなのか、値渡しなのか」という議論がありますが、初心者の方はまず“リストや辞書など変更可能なオブジェクトを引数に渡すと、呼び出し先の関数で中身が変更されると呼び出し元にも影響が出る”というイメージをもっておくと理解がしやすいでしょう。

本記事では、こうしたPython特有の挙動をできるだけシンプルに整理しつつ、具体的なコード例とともに解説します。

皆さんのプログラム開発や学習で混乱しがちなポイントを、少しでもクリアにする助けになれば幸いです。

この記事を読むとわかること

  • Pythonにおける参照渡しの基本的な考え方
  • リストや辞書などのオブジェクトを関数に渡したときに起こりやすい挙動
  • ミュータブル(変更可能)とイミュータブル(変更不可)の違い
  • 実務で起きやすいトラブル事例と防止策
  • 破壊的操作を避けるための考え方

参照渡しの基本を理解しよう

Pythonで「参照渡し」という言葉が使われるときには、しばしば「リストや辞書を関数に渡すと、中身を変更すると呼び出し元も変わってしまう」という現象が取り上げられます。

一方で、「Pythonは厳密には参照渡しでも値渡しでもない」という意見もあります。

そこで最初のステップとして、Pythonのオブジェクトがメモリ上でどのように扱われ、どんなときに“参照の影響”を受けるのかを押さえておくと混乱が少なくなるでしょう。

Pythonがデータを扱う仕組みとは

Pythonでは、変数そのものがオブジェクトを抱えているのではなく、変数はオブジェクトを指し示す“ラベル”の役割を持つと考えてください。

例えば、x = 10 と書いたとき、数値10というオブジェクトがメモリ上に作られ、変数 x はそのオブジェクトを参照します。

これがもし y = x と代入されたら、y も同じオブジェクトを参照することになります。

リストや辞書などの変更可能なオブジェクトでは、こうした“参照”が多重に存在しているときに内容を変更すると、どの参照元から見ても変わったことになるわけです。

参照渡しと値渡しの区別

よく言われる「値渡し」は、関数に引数を渡した場合でもオブジェクトのコピーが関数側に送られるため、関数内で変更しても呼び出し元に影響しません。

しかし「参照渡し」では、同じオブジェクトを共有しているため、関数内で変更があれば呼び出し元のオブジェクトにも変化が及びます。

Pythonは一般的には「値渡し」と「参照渡し」を厳密に区切る従来の仕組みとはやや異なりますが、リストや辞書などのミュータブルなオブジェクトを扱うときは、実質的に“参照渡しのような挙動”になる場面が多いと考えてください。

ミュータブルとイミュータブルを押さえよう

Pythonでは、 ミュータブル (mutable) なオブジェクトとイミュータブル (immutable)なオブジェクトに分かれます。

ミュータブルというのは内容が変更できるオブジェクトで、リストや辞書、集合(set)などが代表例です。

一方、イミュータブルは内容を変更できないオブジェクトで、数値、文字列、タプルなどが該当します。

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

リストや辞書はデータを追加・削除・更新できるため、関数に渡したあとに中身を操作すると、その操作結果が呼び出し元にも反映されます。

例えば、リストを引数として渡した関数の中で append() を使ったり、辞書に新しいキーと値をセットしたりすると、同じアドレス(参照先)を見ているので呼び出し元にも影響が出ることがあるわけです。

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

逆に数値や文字列は変更ができません。
関数の中で x = x + 1 のように書いても、それは新しいオブジェクトを作り直して代入し直しているという動きになります。

そのため、同じ変数名を使っていても、外側の数値や文字列には影響しません。
この違いを意識しておくと、「なぜリストや辞書では値が変わるのに、数値や文字列は変わらないのだろう?」という疑問が解消しやすいでしょう。

関数呼び出し時の参照の挙動

ここからは、実際に関数を使った例を見ながら、Pythonでどのように参照が渡されるかを具体的に見てみましょう。

リストを引数に渡す例

次のコードでは、リスト nums を引数として受け取る関数 modify_list() を定義しています。

def modify_list(lst):
    lst.append(4)
    print("関数内のlst:", lst)

numbers = [1, 2, 3]
modify_list(numbers)
print("関数呼び出し後のnumbers:", numbers)
  1. modify_list(numbers) を呼び出すと、numbers と同じリストを lst が参照します
  2. 関数内で lst.append(4) が実行され、リストの内容が [1, 2, 3, 4] になります
  3. 関数内で print("関数内のlst:", lst) をすると [1, 2, 3, 4] が表示されます
  4. 関数が終了し、外側で numbers を確認すると同じく [1, 2, 3, 4] になっています

このように、リストは中身を変更すると同じ参照先に影響するため、呼び出し元の変数も書き換わってしまうのです。

辞書を引数に渡す例

辞書でも同様のことが言えます。
下記のコードを見てみましょう。

def modify_dict(d):
    d["age"] = 30
    print("関数内のd:", d)

person = {"name": "Alice", "age": 20}
modify_dict(person)
print("関数呼び出し後のperson:", person)
  1. modify_dict(person) を呼び出すと、person と同じ辞書が d に参照されます
  2. 関数内で d["age"] = 30 と書くと、辞書の "age" キーに紐づく値が20から30に変更されます
  3. 関数内のプリントでは {"name": "Alice", "age": 30} が表示されます
  4. 関数終了後の person を見ると、同じく {"name": "Alice", "age": 30} に更新されています

こちらもリストと同様、ミュータブルなオブジェクトが同じ参照を共有している結果といえます。

ミュータブルな引数を扱うときの注意点

ミュータブルなオブジェクトを関数引数として扱う場合、参照の影響で思わぬ副作用(呼び出し元が意図せず変更される)が起きるかもしれません。

実務の場面でも、関数の中で引数をいじってしまい、プログラム全体の動作に影響が及ぶ問題はよくあります。

それを避けるために、必要に応じてオブジェクトのコピーを作ってから処理するという手段をとることがあります。

シャローコピーとディープコピー

Pythonには copy モジュールというものがあり、copy.copy()シャローコピー(浅いコピー)、 copy.deepcopy()ディープコピー(深いコピー)を生成できます。

  • シャローコピーは、リストや辞書の最上位の構造だけコピーして、内部の要素がさらにリストや辞書になっている場合には参照をコピーします。
  • ディープコピーは、内部に入れ子構造がある場合でも再帰的にすべての要素をコピーします。

実務で配列や辞書を関数に渡すとき、「関数の中でどうしても中身を一時的に変えたいけど、外側には影響を与えたくない」という状況になったら、関数側でコピーを作ってから操作を行うことが多いです。

以下のような例を参考にしてみてください。

import copy

def process_data(data):
    # dataのディープコピーを作ってから操作
    local_data = copy.deepcopy(data)
    local_data["items"].append("new_item")
    return local_data

original = {"items": ["item1", "item2"]}
result = process_data(original)

print("process_data呼び出し後のoriginal:", original)
print("process_data呼び出し後のresult:", result)

このコードでは、process_data() 関数内で data のディープコピーを作ってから "items" に新たな要素を追加しています。

そうすると original は変更されずに済みます。
逆に言えば、コピーをしないと original の中身が書き換わってしまう可能性があるわけです。

破壊的操作とその回避方法

リストや辞書などを関数に渡して、そのまま要素を追加・変更する行為は破壊的操作と呼ばれます。

破壊的操作は必要に応じて使うことは悪いわけではないですが、意図しない更新が発生するとバグの温床になりやすいです。

破壊的操作を行う場面

  • データを更新するためだけに呼び出す関数
  • 処理効率を重視してコピーのコストを抑えたいとき

これらのケースでは、破壊的操作を意図的に使うことがあります。
ただし、どこがデータを変更するのかを明示しないと、あとからコードを読む人が混乱するかもしれません。

破壊的操作を避ける場面

  • 元のデータを絶対に変えたくないとき
  • 同じデータを複数の箇所で共有して使っているとき

こういった場面では、先ほど紹介したコピーを作ってから操作することを検討しましょう。

実務での活用シーン

ここでは、開発現場で起こりがちなケースをいくつか紹介します。

皆さんが実際にプログラムを書いているときも、思わぬところで参照渡しによるデータの変更が波及しないかを考えると、ミスを減らしやすくなります。

Webアプリケーションでのリクエストデータ処理

Webアプリケーションのフレームワークを使っていると、リクエストパラメータやJSONで受け取ったデータを辞書形式で扱う場面があります。

これを関数に渡すとき、中身を直接書き換えてしまうとリクエストの別の処理箇所にも影響が出る可能性があります。

そのため、一時変数にコピーを作ってから操作するか、関数内部で返すときに新しい辞書を生成する方法がよく使われます。

複数モジュールで共有する設定オブジェクト

設定ファイルを読み込んで辞書やリストにまとめ、それをいろんなモジュールで使い回すケースがあります。

この設定オブジェクトが破壊的操作で書き換わってしまうと、別のモジュールに影響が及ぶかもしれません。

そうした場合はコピーを渡す、あるいは“読み取り専用”として扱う方法を決めておくことが大切です。

テストコードにおけるデータ

テスト環境では、あるテストケースで使ったリストや辞書を後続のテストケースでも使いまわすことがあるでしょう。

もし前のテストでデータを書き換えてしまうと、次のテストに予期せぬ影響が及びます。

実務ではテストコードを書くときにリストや辞書を共通化せず、必要に応じて新しいオブジェクトを生成する方法が好まれます。

関数設計のポイント

参照渡しの挙動を意識したうえで、どのように関数を設計すればよいでしょうか。
大切なポイントは、「関数が外側のデータをどこまで変えてよいか」を明確にすることです。

変数名での工夫

Pythonでは型が明示されないため、引数に何が渡ってくるかがわかりづらいケースがあります。
関数の処理内容を想像しやすくするために、ミュータブルな引数を扱うときはわかりやすい名前をつけましょう。

例えば、update_user_info(user_data) のように、明らかに“情報を更新する”ことがわかる名前にしておくと、呼び出し元としても「この関数を呼ぶと user_data が書き変わるかもな」と意識しやすくなります。

ドキュメント文字列の利用

Pythonには関数の先頭に """ ... """ のようなドキュメント文字列(docstring)を書く文化があります。
ここに「この関数は引数のリストを直接編集します」などの注記を書いておくと、参照渡しで起こりやすい混乱を防ぐことにつながるでしょう。

中間オブジェクトの生成

先ほどの例のように、外側のオブジェクトを影響させたくないなら、関数の中でコピーを生成して作業するという方法をとりましょう。
これにより「関数内でガッツリ編集するけど、外側には影響を及ぼさない」という動きが保証されます。

参照渡しで起きがちな誤解とよくある疑問

参照渡しについて混乱するポイントはいくつかあります。
初心者の方によくある疑問を通して、整理してみましょう。

関数内で再代入したら元のオブジェクトは変わらないの?

たとえば、次のようなコードを見てください。

def replace_list(lst):
    lst = [100, 200]
    print("関数内のlst:", lst)

numbers = [1, 2, 3]
replace_list(numbers)
print("関数呼び出し後のnumbers:", numbers)

この場合、lst = [100, 200] という代入は新しいリストを lst という変数に割り当てただけで、呼び出し元の numbers が参照しているリストを変更しているわけではありません。
そのため、numbers の内容は [1, 2, 3] のままです。

ここで混乱しがちな点は、「lst に新しいリストを代入しても、それは外側の変数とは別の参照を持つ」ということです。
外側のオブジェクト自体を変化させるのではなく、lst が新規オブジェクトを指すようにしただけなので、呼び出し元には影響しません。

スライスや加算でリストを作り変える

lst = lst + [4]lst = lst[1:] のように、リストを演算子で結合したりスライスしたりして新しいリストを作った場合も、先ほどの例と同じ理由で呼び出し元を変化させません。
再代入そのものは外側の変数に影響しないのです。

ただし、lst.append(4)lst.remove(2) のように、同じオブジェクトの中身を直接書き換える操作は呼び出し元にも反映されます。

コード例:実務での活用をイメージ

ここでは、参照渡しを踏まえてどのようにコードを書けば混乱を最小限にできるかを示す例を用意しました。

def update_inventory(inventory, item, quantity):
    """
    在庫管理用の関数: 引数のinventoryを直接更新します。
    item: 追加または更新するアイテム名
    quantity: そのアイテムの在庫数(整数)
    """
    if item in inventory:
        inventory[item] += quantity
    else:
        inventory[item] = quantity

def process_shipment(inventory, shipment):
    """
    在庫データをコピーしてから処理して返す関数。
    もとのinventoryには影響を与えたくないケースを想定。
    shipment: [(item, quantity), (item, quantity), ...]
    """
    import copy
    new_inventory = copy.deepcopy(inventory)
    for (item, qty) in shipment:
        if item in new_inventory:
            new_inventory[item] += qty
        else:
            new_inventory[item] = qty
    return new_inventory

stock = {"apple": 50, "banana": 20}
update_inventory(stock, "banana", 5)
# stockは{"apple": 50, "banana": 25}に変わります

incoming = [("apple", 10), ("banana", 15)]
updated_stock = process_shipment(stock, incoming)
# process_shipment内でディープコピーを作っているので、stockは{"apple": 50, "banana": 25}のまま
# updated_stockは{"apple": 60, "banana": 40}になります

1. update_inventory() は、在庫オブジェクトを直接いじる関数です。

そのため、引数を変えてしまうことがわかっているならドキュメント文字列や関数名などで明示しておきましょう。

2. process_shipment() は、関数の中でディープコピーを作ってから在庫を更新しています。

よって、呼び出し元の stock は変更されず、新しい updated_stock にだけ更新結果が反映されます。

実務でも、このように参照渡しの特性を意識した関数を使い分けることが多いと考えられます。

参照渡しに関するトラブルを防ぐコツ

ここまでの内容を踏まえ、Pythonの開発現場で混乱を防ぐためのコツをまとめます。
ぜひ参考にしてみてください。

引数を変更するのか、しないのかを明確に

関数名やドキュメント文字列などで、外側のオブジェクトを変えるつもりなのか、それとも影響を与えないつもりなのかをはっきり書いておくと、後から読む人が誤解しにくくなります。

念のためコピーを取る

場合によっては、関数の中でコピーを取ってから処理を進めると安全です。
ただし、コピーのコストが高くなる可能性もあるので、処理量やパフォーマンスと相談して使い分けましょう。

コードレビューやチーム内での約束

チーム開発の場合は、コーディング規約として「引数を破壊的に変更する関数は関数名を mutate_~ のようにするといったルールを作る」など、命名規則を設けるのも手です。
これにより、意図しない更新を減らせるでしょう。

初心者の方が混乱しないようにするためには、まずはリストや辞書を関数に渡すときに「呼び出し元のデータまで変わるかも」と意識しておくとよいでしょう。

参照とオブジェクトの仕組みをテーブルで整理

ここで一度、イミュータブルとミュータブルについて、代表的なオブジェクトを表で確認しましょう。

オブジェクト例ミュータブル or イミュータブル関数内で破壊的操作が可能か
数値 (int, float など)イミュータブル不可(新しいオブジェクトが作られる)
文字列 (str)イミュータブル不可(追加や削除はできない)
タプル (tuple)イミュータブル不可(タプルの要素は変更できない)
リスト (list)ミュータブル可能(append, removeなど)
辞書 (dict)ミュータブル可能(要素の追加、変更、削除)
集合 (set)ミュータブル可能(add, removeなど)

イミュータブルなものは参照を共有していても内部を直接書き換えることができません。
一方、リストや辞書のようにミュータブルなオブジェクトは、“参照”を共有している状態で中身をいじると呼び出し元にも影響する点がポイントです。

実務におけるまとめと注意点

ここまで紹介したように、Pythonではリストや辞書などのオブジェクトを引数に渡すと、呼び出し元と呼び出し先で同じ参照を共有することになります。

そのため、関数内部の変更が外側の変数に影響することを理解しながらコードを書かなければなりません。

データを多く扱う処理や、規模の大きいプロジェクトでは「どこがデータを変更しているのか」を追うのが大変になりがちです。
破壊的操作の意思決定は慎重に行いましょう。

まとめ

皆さんがPythonで変数の中身を操作するとき、特にリストや辞書などのミュータブルなオブジェクトを扱う場合には参照渡しの概念をしっかり押さえておきましょう。
関数に引数を渡すとき、どのような形でデータが渡されるかを意識するだけで、思わぬバグやトラブルを未然に防ぎやすくなります。

ミュータブルかイミュータブルかによって挙動が変わることや、再代入と破壊的操作の違いなど、最初は少しとっつきにくいかもしれません。
しかし、実務のシーンを想像しながら学ぶと、どんなときに注意が必要なのかが見えてくるはずです。

「関数内での操作が外側に影響してほしくない」「明示的に破壊的な変更を行いたい」など、プロジェクトによってさまざまなニーズがあります。
そのときにどういった書き方をするのがベストかをチームで話し合い、またコードを整理するのがよいでしょう。

Pythonの参照渡しを把握しておくと、リストや辞書を多用するプログラムを書くときに大いに役立つはずです。
ぜひ、ここで得た知識を少しずつコードに取り入れて、トラブルの少ないプログラムを書くコツを身につけてみてください。

Pythonをマスターしよう

この記事で学んだPythonの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。