はじめに
—— Pythonエンジニアなら一度は踏む“あのバグ”の正体を徹底解説
Python には、初心者から上級者まで必ず一度は引っかかる罠があります。
デフォルト引数に list や dict など「可変オブジェクト」を書いてはいけない。
なぜなら、そのオブジェクトは“関数定義時に一度だけ作られ、すべての呼び出しで共有される”から。
この仕組みは Python の設計思想に根ざしたものですが、仕組みを知らないとバグ製造マシンになります。
この記事では、この現象の理由・典型的な失敗例・正しい回避方法・応用パターンまでまとめていきます。
1. まずは「おかしな現象」を見てみよう
一見ふつうのコード:
def add_item(x=[]):
x.append(1)
return x
print(add_item()) # [1]
print(add_item()) # [1, 1]
print(add_item()) # [1, 1, 1]
えっ、なんで毎回 1 が追加され続けるの?
空リストじゃないの??
実は、[] は すべての呼び出しで使い回されている ため、前回の append が次回にも残るのです。
2. そもそも、なぜこうなる?
Python のデフォルト引数は “関数定義時” に評価される から。
つまり:
def add_item(x=[]):
というコードは、
- 関数が読み込まれた瞬間(定義時)に
[]が1回だけ作られる - その list オブジェクトが
__defaults__に保存される - 関数の引数が省略された場合、この同じ list をずっと使用する
という仕組み。
❌ 関数を呼ぶたびに新しい list が作られるわけではない
✔ 同じ list がずっと再利用される
これがバグの根本原因。
3. 可変(mutable) vs 不可変(immutable)で挙動が違う
| 種類 | 例 | 状態 | デフォルト引数として安全? |
|---|---|---|---|
| 不可変(immutable) | int, str, tuple | 変更できない | ✔ 安全 |
| 可変(mutable) | list, dict, set | 内容を変更できる | ❌ 危険 |
不可変オブジェクトなら OK
def f(n=10):
n += 1
return n
print(f()) # 11
print(f()) # 11
↑ 毎回新しい整数が生成されるため安全。
可変オブジェクトは絶対ダメ
def f(lst=[]):
lst.append(1)
return lst
↑ 共有されるので危険。
4. 正しい回避方法:None を使った「デフォルト初期化パターン」
Python で最も一般的で安全な書き方:
def add_item(x=None):
if x is None:
x = []
x.append(1)
return x
これのメリット:
-
Noneは不変なので安全 - 「引数を省略したとき」と「空リストを渡したとき」を区別できる
- Python コードの“お約束”として広く使われる
5. 実戦的な例:ログ蓄積・API パラメータ・データ集約の大事故
悪い例:
def collect(item, bucket=[]):
bucket.append(item)
return bucket
API を作るとこうなる:
- ユーザーAが item 1 を送る
→ bucket = [1] - ユーザーBが item 2 を送る
→ bucket = [1, 2]
全員のデータが混ざる“地獄”が誕生する。
正しい書き方:
def collect(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
6. 実は「敢えて使う」ケースもある(キャッシュ・メモ化)
あえて可変オブジェクトを共有したいときもある。
例:再帰フィボナッチのキャッシュ
def fib(n, cache={}):
if n in cache:
return cache[n]
if n < 2:
return n
res = fib(n-1) + fib(n-2)
cache[n] = res
return res
- cache を使い回すことで超高速化
- 「意図して共有」しているなら OK
ただし用途は限定的。
7. 危険なデフォルト引数を見分ける方法
関数の __defaults__ を見れば一発。
print(add_item.__defaults__)
例:
([],)
→ list が入っているなら 危険サイン。
まとめ:これだけ覚えれば100点
-
❌ デフォルト引数に list/dict/set を書くな
-
✔ 初期化したい場合は
Noneを使う -
✔ デフォルト引数は“定義時”に一度だけ生成される
-
✔ 可変オブジェクトはすべての呼び出しで共有される