0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】Python「デフォルト引数 × 可変オブジェクト」の罠

0
Posted at

はじめに

—— 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. 関数が読み込まれた瞬間(定義時)に [] が1回だけ作られる
  2. その list オブジェクトが __defaults__ に保存される
  3. 関数の引数が省略された場合、この同じ 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 を使う

  • ✔ デフォルト引数は“定義時”に一度だけ生成される

  • ✔ 可変オブジェクトはすべての呼び出しで共有される

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?