Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Python's Most Famous Gotcha: The Mutable Default Argument

2025年09月19日に「Dev.to」が公開したITニュース「Python's Most Famous Gotcha: The Mutable Default Argument」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Python関数でデフォルト引数にリストなど中身が変わるオブジェクトを使うと、関数定義時に一度だけ評価され、同じオブジェクトが共有される。結果、意図せず前回の呼び出し結果が残る問題が発生する。これを避けるには、デフォルトをNoneとし、関数内で新しいオブジェクトを作成するとよい。

ITニュース解説

Pythonでプログラミングをしていると、時々「あれ、なぜこんな挙動になるのだろう?」と疑問に感じるような場面に出くわすことがある。特に、関数にデフォルト引数を設定したときに起こる特定の現象は、多くのプログラマーが一度は経験する「Pythonの有名な落とし穴」として知られている。システムエンジニアを目指す皆さんにとって、この挙動を理解することは、Pythonの深部を理解し、より堅牢なプログラムを書くための重要なステップとなるだろう。

具体的にどのような問題か見てみよう。例えば、ある要素をリストに追加するシンプルな関数を考えてみる。この関数は、もしリストが指定されなければ、自動的に空のリストを作成して使うようにしたいとしよう。次のようなコードを想像してほしい。

1def add_to_list(item, target=[]):
2    target.append(item)
3    return target

この関数を使ってリストを作成していくと、予想外の事態が発生する。まず、list_1 = add_to_list('a') を呼び出すと、['a'] というリストが返ってくる。ここまでは問題ない。次に、list_2 = add_to_list('b') を呼び出すと、期待する結果は ['b'] だろう。しかし、実際に返ってくるのは ['a', 'b'] となる。なぜ最初の呼び出しで追加された 'a' が、二度目の呼び出しの結果にも含まれているのだろうか。さらに驚くべきことに、最初の呼び出しで得られたリストと、二度目の呼び出しで得られたリストは、メモリ上で全く同じオブジェクトであることが判明する。これは、list_1 is list_2 のように比較すると True が返ってくることからも確認できる。

この奇妙な挙動の根本原因は、Pythonが関数のデフォルト引数を評価するタイミングにある。Pythonでは、デフォルト引数は関数が「定義されたとき」に一度だけ評価され、その値が関数オブジェクトに紐付けられる。関数が呼び出されるたびにデフォルト引数が再評価されるわけではないのだ。

先ほどの add_to_list(item, target=[]) の例で考えてみよう。Pythonインタープリターがこの def 文を読み込んだとき、空のリスト [] が一度だけメモリ上に作成される。仮に、このリストがメモリ上の「ID 12345」という場所にあるとしよう。このID 12345のリストが、target 引数が省略された場合のデフォルト値として、add_to_list 関数自身にずっと結び付けられることになる。

関数の呼び出しの流れを追ってみると、この仕組みがより明確になる。

  1. 関数が定義されるとき: Pythonは target=[] という部分を解釈し、メモリ上に一つの空のリストオブジェクトを作成する。例えば、このリストには「ID 12345」という識別番号が割り当てられたとする。この「ID 12345」のリストが、add_to_list 関数のデフォルト引数として固定される。

  2. 初回呼び出し list_1 = add_to_list('a') のとき: target 引数が省略されたため、関数は事前に定義時に作成された「ID 12345」のリストを使う。 'a' をこのリストに追加する。すると、ID 12345のリストは ['a'] となる。 この変更されたリストが関数から返され、list_1 という変数に代入される。list_1 はID 12345のリストを参照している。

  3. 二回目呼び出し list_2 = add_to_list('b') のとき: 今回も target 引数が省略されたため、関数は再び定義時に作成された「ID 12345」のリストを使う。 'b' をこのリストに追加する。すると、ID 12345のリストは ['a', 'b'] となる(以前の'a'が残っている)。 このさらに変更されたリストが関数から返され、list_2 という変数に代入される。list_2 もID 12345のリストを参照している。

結果として、list_1list_2 はどちらもメモリ上の「ID 12345」という全く同じ一つのリストオブジェクトを参照していることになる。このため、どちらかの変数を通じてリストの内容を変更すると、もう一方の変数からもその変更が見えるのだ。id() 関数を使ってそれぞれの変数が参照するオブジェクトのIDを確認すると、同じ数値が出力されることでこの事実を確かめられる。

では、なぜPythonはこのように設計されているのだろうか。Pythonの生みの親であるGuido van Rossum氏によると、その主な理由はパフォーマンスにある。デフォルト引数を関数定義時に一度だけ評価する方が、関数が呼び出されるたびに毎回評価し直すよりも効率的だからだ。特にシンプルな関数が頻繁に呼び出される場合、この設計は処理速度の向上に貢献する。この効率性という利点と、一部の予期せぬ挙動という潜在的な欠点がトレードオフの関係になっていると言える。

この問題は、リスト(list)、辞書(dict)、セット(set)といった「変更可能なオブジェクト(ミュータブルなオブジェクト)」をデフォルト引数に指定した場合にのみ発生する。整数(int)、文字列(str)、タプル(tuple)のような「変更不可能なオブジェクト(イミュータブルなオブジェクト)」をデフォルト引数にした場合は、このような問題は起こらない。なぜなら、イミュータブルなオブジェクトは内容をその場で変更することができず、変更を試みると常に新しいオブジェクトが作成されて変数に再代入されるため、デフォルト値が意図せず共有されることがないからだ。

この落とし穴を避けるための「Pythonicな」解決策がある。それは、デフォルト引数に None を使い、関数が呼び出されたときに初めて変更可能なオブジェクトを作成するという手法だ。

1def add_to_list_fixed(item, target=None):
2    if target is None:
3        target = []  # ここで新しいリストが作成される
4    target.append(item)
5    return target

この修正版の関数では、target 引数が省略された場合、target の初期値は None となる。そして、if target is None: という条件文が真となり、target = [] という行が実行される。この行は関数が呼び出される「実行時」に評価されるため、add_to_list_fixed が呼び出されるたびに毎回新しい空のリストが作成されることになる。

この修正版を使えば、list_1 = add_to_list_fixed('a')['a'] を含む新しいリストを返し、list_2 = add_to_list_fixed('b')['b'] を含む別の新しいリストを返す。これにより、list_1list_2 が別々のオブジェクトを参照するようになり、期待通りの挙動が得られる。

このミュータブルなデフォルト引数の挙動は、通常は避けたい「落とし穴」だが、非常に稀なケースでは意図的に利用されることもある。例えば、関数が以前の呼び出しの状態を保持したい場合、キャッシュやメモ化といった高度なプログラミングパターンで利用されることがある。しかし、これは専門的なテクニックであり、他の開発者を混乱させないように、その意図を明確にドキュメント化する必要がある。

この「ミュータブルなデフォルト引数」の問題に遭遇し、その仕組みを理解することは、Pythonプログラマーとしての重要な成長の証だ。単にコードのバグを修正するだけでなく、Pythonの内部動作、つまりどのようにコードが実行され、メモリが管理されているのかという深い部分に触れる経験となる。リンター(コードの静的解析ツール)が「ミュータブルなデフォルト引数」に関する警告を表示した際には、それが何を意味し、どのように修正すべきかを即座に理解できるようになるだろう。これは、単なる構文の習得を超え、Pythonの奥深さを理解し始めたことの証だ。

関連コンテンツ