はじめに

Pythonの関数定義における「デフォルト引数」は、一見便利に見える構文でありながら、バグの温床となる設計ミスを数多く誘発する。

その原因の大半は、リストや辞書などのミューテーブル(可変)オブジェクトがデフォルトとして使用された場合に、予期せぬ動作が発生することにある。
レビューアーはこの特性を深く理解し、副作用・再利用性・意図の曖昧さといった観点でコードを精査する必要がある。

なぜミューテーブルなデフォルト引数は問題なのか?

以下は、典型的な設計ミスの例である。

バグを生む典型例
def append_item(item, items=[]):
    items.append(item)
    return items

この関数は、初回の呼び出しでは正常に動作する。しかし、2回目以降の呼び出しで予期せぬ共有状態が継続される

実行例
print(append_item("a"))  # ['a']
print(append_item("b"))  # ['a', 'b'] ← バグの原因
Comment
@Reviewer: `items=[]`は関数定義時に一度だけ評価されるため、すべての呼び出しで共有されます。副作用を避けるため、デフォルトは`None`を使い、関数内で初期化してください。
ミューテーブルとは

「変更可能なオブジェクト」を意味し、listdictsetなどのインスタンスが該当する。デフォルト引数に設定すると、関数間で状態が意図せず保持・共有されるため、バグの原因となる。

正しい実装例:防御的初期化

修正済みの安全な定義
def append_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

このように、デフォルト引数にNoneを設定し、関数内で明示的に初期化することで、安全な設計となる。

評価タイミングの違い

デフォルト引数が共有される構造

UML Diagram

毎回初期化される安全な構造

UML Diagram

よくあるレビュー指摘ポイントとチェック観点

状況 検出すべき記述例 指摘内容
ミューテーブルのデフォルト引数 def foo(x, lst=[]) []が共有状態になる可能性
意図不明な初期値 opts={} 呼び出し元で変更が影響しないか
Noneでガードしていないケース if not opts: dictlist以外もFalseになるので意図を明確に
デフォルト引数が動的に変わる構成 default=datetime.now() 定義時に評価されるため常に同じ値になる危険性

会話形式レビュー例:チーム内でのやりとり

チーム内のレビュー対応
def log_messages(msg, log_list=[]):
    log_list.append(msg)
    return log_list
@Reviewer
`log_list=[]`はミューテーブルの共有が発生します。ログが全呼び出し間で蓄積されてしまう危険があります。
@Developer
初期化済みのリストを使いたかったのですが、個別でリセットする方がよいですか?
@Reviewer
状態の独立性を担保するため、関数内で`None`から明示初期化してください。

このように、レビューでは単なる記法指摘にとどまらず、設計上の構造と状態保持の意味をチームで共有することが重要である。

デフォルト引数が原因となった実害例(バグ事例)

過去のPythonバージョンでも、argparseなどのライブラリにおいて、以下のような設計バグが報告されている。

  • バグ例: リストの初期値が共有されることで、意図しないコマンドラインオプションが蓄積される
  • 対応: ライブラリ側でdefault=Noneとし、初回実行時にオブジェクト生成

このように、「一見正常に動く」コードが、ある時点で壊れるという形でユーザーに不具合が発生する。

レビューアーは、このような検索意図=実務で直面する問題であると捉え、事前に防止できる設計かを評価する立場にある。

まとめ:レビューアーとしての責務

デフォルト引数の扱いは、見落としがちな初学者的ミスでありながら、ベテランでも設計時に混入するリスクがある
レビューアーは以下の観点を持ち、構造的な判断を下す必要がある。

  • 初期値の型と評価タイミングを理解し、その仕様による副作用を検出する
  • 可変な状態が共有される可能性を常に意識する
  • 冗長に見えても関数内での初期化が最も安全であることを明示する

この構造的な視点が、レビューの質を根本から支える土台となる。