【ITニュース解説】Understanding how Python evaluates default arguments and why mutable defaults can carry unintended memory
2025年09月20日に「Dev.to」が公開したITニュース「Understanding how Python evaluates default arguments and why mutable defaults can carry unintended memory」について初心者にもわかりやすく解説しています。
ITニュース概要
Pythonで関数のデフォルト引数にリストなどの変更可能なオブジェクトを設定すると、関数呼び出し時にそのオブジェクトが共有され、予期せぬ挙動を起こす。これを避けるには、Noneをデフォルトに設定し、関数内で新しいオブジェクトを生成するのが正しい。
ITニュース解説
Pythonでプログラミングをしていると、関数の「デフォルト引数」という便利な機能に出会うことがある。しかし、このデフォルト引数には、特にシステムエンジニアを目指す初心者が戸惑いやすい、意外な挙動がある。それは、関数が過去の呼び出しを「記憶」しているかのように振る舞う現象である。例えば、リストをデフォルト引数に設定した関数を複数回呼び出すと、期待に反して、以前の呼び出しで加えられた変更が残ってしまうことがある。
この現象の鍵は、Pythonがデフォルト引数を評価するタイミングにある。Pythonでは、デフォルト引数は関数が「定義されたとき」に一度だけ評価され、その値がメモリ上に生成される。関数が呼び出されるたびに新しいデフォルト値が作られるわけではない。例えば、def ring(bell=[]):のようにリストをデフォルト引数として設定した場合、Pythonはring関数が定義された瞬間に空のリストを一つ作り、それをbell引数のデフォルト値として紐づける。そのため、ring()のように引数を指定せずにこの関数を呼び出すたび、毎回同じリストオブジェクトが使われることになる。結果として、最初の呼び出しでリストに要素を追加すると、二回目、三回目の呼び出しでもその追加された要素が残った状態のリストを操作することになる。これは、id()関数を使ってオブジェクトのメモリ上のアドレスを確認することで、実際に同じオブジェクトが使われていることを確かめられる。
この問題が発生するのは、リストや辞書のような「ミュータブル(変更可能)な」オブジェクトをデフォルト引数として使った場合のみである。では、「イミュータブル(変更不可能)な」オブジェクト、例えばタプルや数値、文字列、Noneをデフォルト引数にするとどうなるだろうか。def ring(bell=()):のようにタプルをデフォルト引数に設定し、bell += ("clang",)のように要素を追加しようとすると、タプルはイミュータブルであるため、既存のタプルが変更されるのではなく、新しいタプルオブジェクトが作成される。そのため、関数を呼び出すたびに新しいタプルが作られ、以前の呼び出しの影響は残らない。つまり、イミュータブルなデフォルト引数を使った場合、関数は毎回新鮮な結果を返し、過去の記憶を引きずることはないのである。問題は、デフォルト引数そのものが危険なのではなく、そのオブジェクトが変更可能であること、そしてその変更が関数の呼び出し間で共有されてしまう可能性にある。
この意図しない「記憶」を避けるための最も一般的な解決策は、デフォルト引数にNoneを使うことである。Noneはイミュータブルなオブジェクトであり、また「引数が提供されなかった」という明確なマーカーとしても機能する。具体的には、def ring(bell=None):と定義し、関数の内部でif bell is None:という条件文を使って、引数が指定されなかった場合に初めて新しいリストなどのミュータブルなオブジェクトを作成するのである。この方法を使えば、関数が呼び出されるたびに新しいリストが生成され、それぞれの呼び出しが独立して動作するようになる。これにより、関数の挙動は予測可能になり、過去の記憶が不必要に残ることはなくなる。
ただし、ごく稀に、意図的にミュータブルなデフォルト引数を利用するケースも存在する。例えば、関数の計算結果をキャッシュしておき、同じ引数での再計算を避ける「メモ化」のような機能を実現する場合である。def factorial(n, cache={0: 1}):のように、辞書をデフォルト引数として使用し、その辞書を計算結果のキャッシュとして利用するケースがこれにあたる。この場合、辞書は関数呼び出し間で共有される永続的なメモリとして機能する。しかし、これは明確な意図と設計に基づいた選択であり、偶発的な副作用ではない。このような特殊な使い方をする際は、その意図を明確にコードのコメントなどで文書化することが非常に重要である。多くの場合、このような共有状態は、デコレータや外部のキャッシュメカニズムを利用する方が、より明確で管理しやすいコードになることが多い。
システムエンジニアとしてクリーンで予測可能なPythonコードを書くためには、以下のベストプラクティスを覚えておくことが重要だ。第一に、リストや辞書のようなミュータブルなオブジェクトをデフォルト引数として使用することは避けるべきである。これらは関数呼び出し間で永続化し、予期せぬ動作を引き起こす可能性があるためだ。第二に、関数が呼び出されるたびに新しいオブジェクトを生成したい場合は、デフォルト引数にNoneを使用し、関数内で必要に応じて新しいオブジェクトを作成するようにする。None、0、''、()のようなイミュータブルなデフォルト値は常に安全である。これらに対する+=のような操作は、新しいオブジェクトを返すだけで、元のデフォルト値を変更することはない。第三に、もしどうしてもミュータブルなデフォルト引数を使用する必要がある場合は、それが意図的な共有状態であることを明確に文書化し、その設計が本当に必要であることを確認する。最後に、デバッグ時にはid()関数を使ってオブジェクトのメモリ上のアドレスを確認することで、同じオブジェクトが再利用されているかどうかを確かめることができる。ただし、Pythonの内部的な最適化(インターニング)によって、特定の小さなオブジェクトは異なる呼び出しでも同じidを持つことがあるので、この点には注意が必要である。
結局のところ、この問題は「毎回新しいオブジェクトを使いたいのか、それとも同じオブジェクトを使い続けたいのか」という問いに帰結する。Pythonにおいて、もしデフォルト引数がミュータブルなオブジェクトである場合、それは関数の呼び出し間で成長し、過去の記憶を持ち続ける。どのオブジェクトが「記憶」を持ち、どのタイミングで「新しい」オブジェクトを生成すべきかを理解することは、堅牢で予測可能なPythonコードを書く上で不可欠な知識である。