【Python】デフォルト引数の罠:リストや辞書を使ってはいけない理由

Pythonの関数定義におけるデフォルト引数は、引数が指定されなかった場合に備えて初期値を設定できる、非常に便利な機能です。しかし、このデフォルト値にリスト ([])辞書 ({}) といった「ミュータブル(変更可能)」なオブジェクトを指定すると、多くの開発者が一度は遭遇する有名な「罠」にはまることがあります。

今回は、なぜこの問題が起きるのか、その仕組みを解き明かし、安全で意図通りに動作するコードの書き方を解説します。

目次

問題のコード:意図しないリストの共有

例えば、ある投稿にタグを追加する関数を考えてみましょう。タグが指定されなければ、新しい空のリストにタグを追加する仕様です。

# 悪い例:デフォルト引数にリスト[]を指定している
def add_tag(tag: str, tags: list[str] = []):
    """タグをリストに追加して返す"""
    tags.append(tag)
    return tags

# 実行例
# 1回目の呼び出し: 'Python'タグを最初の投稿に追加
post1_tags = add_tag("Python")
print(f"投稿1のタグ: {post1_tags}")

# 2回目の呼び出し: 'Ruby'タグを2番目の投稿に追加
post2_tags = add_tag("Ruby")
print(f"投稿2のタグ: {post2_tags}")

このコードを実行すると、どのような結果になるでしょうか?多くの方は、次のように表示されると期待するはずです。

投稿1のタグ: ['Python']
投稿2のタグ: ['Ruby']

しかし、実際の実行結果は衝撃的なものになります。

投稿1のタグ: ['Python']
投稿2のタグ: ['Python', 'Ruby']

2番目の投稿に、最初の投稿のタグである 'Python' が紛れ込んでしまいました。post1_tagspost2_tags は、同じリストを共有してしまっているのです。なぜ、このようなことが起こるのでしょうか?


原因:デフォルト引数は「定義時」に一度だけ評価される

この現象の鍵は、Pythonのデフォルト引数が、関数が呼び出されるたびではなく、関数が定義された時に一度だけ評価され、そのオブジェクトがメモリ上に生成されるという仕様にあります。

def add_tag(tag: str, tags: list[str] = []): という行がPythonインタプリタによって読み込まれた瞬間に、空のリスト []一つだけメモリ上に作成されます。そして、add_tag 関数のデフォルト引数 tags は、常にそのたった一つのリストを参照し続けるのです。

これを図でイメージすると、以下のようになります。

  1. 関数定義時: 空のリストオブジェクトが1つだけメモリに作られ、add_tag 関数のデフォルト引数 tags はそれを指す。
  2. 1回目の呼び出し: add_tag("Python") が実行される。tags はデフォルト値(メモリ上のあのリスト)を使い、そこに "Python" が追加される。
  3. 2回目の呼び出し: add_tag("Ruby") が実行される。tags は再びデフォルト値(先ほど変更が加えられた、メモリ上の全く同じリスト)を使い、そこに "Ruby" が追加される。

その結果、すべてのデフォルト呼び出しが同じリストオブジェクトを使い回してしまい、意図しない挙動を引き起こすのです。


解決策:不変の「None」を使い、関数内で初期化する

この問題を回避するためのPythonにおける定石は、デフォルト引数には不変(イミュータブル)なオブジェクトである None を使い、関数の中でオブジェクトの初期化を行うことです。

# 良い例:Noneをデフォルト値として使う
def add_tag_safe(tag: str, tags: list[str] | None = None) -> list[str]:
    """
    タグをリストに追加して返す(安全な実装)。
    tagsがNoneの場合、新しい空のリストを作成する。
    """
    if tags is None:
        tags = [] # 関数が呼び出されるたびに、新しいリストが作られる
    
    tags.append(tag)
    return tags

# 実行例
# 1回目の呼び出し
post1_tags = add_tag_safe("Python")
print(f"投稿1のタグ (安全な実装): {post1_tags}")

# 2回目の呼び出し
post2_tags = add_tag_safe("Ruby")
print(f"投稿2のタグ (安全な実装): {post2_tags}")

実行結果:

投稿1のタグ (安全な実装): ['Python']
投稿2のタグ (安全な実装): ['Ruby']

期待通りの結果になりました。

この「Noneイディオム」と呼ばれる手法では、if tags is None: の判定が関数呼び出しのたびに行われます。tags 引数が省略された場合(つまりNoneの場合)、その都度 tags = [] が実行され、新しい空のリストがメモリ上に確保されます。これにより、関数呼び出し同士が互いに影響を与えることはなくなります。

また、tags: list[str] | None = None のように型ヒントを使うことで、この引数がリストまたは None を受け付けることが静的に示され、より安全で分かりやすいコードになります。


まとめ

Pythonの関数を定義する際は、以下のルールを徹底しましょう。

  • デフォルト引数にリスト、辞書、セットなどのミュータブルなオブジェクトを使わない。
  • 代わりに None をデフォルト値として設定し、関数内で if an_arg is None: のようにチェックして初期化する。

このシンプルなルールを守るだけで、Pythonで最もよく知られた罠の一つを回避し、予測可能で堅牢なコードを書くことができます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次