Code Fix

上級

可変デフォルト引数が呼び出しをまたいで共有される

リストや辞書をデフォルト引数にすると、その1つのオブジェクトが全ての呼び出しで共有されます。呼び出すたびに新しく作られるわけではありません。

なぜエラーが出ないのか

出力: ['a']
(エラーなし)
Pythonは何も報告しません。文法として正しいためです
出力: ['a']
実際の挙動 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます

このエラーが出る典型パターン

パターン1

 1  def add_item(item, items=[]    ):
                                ^
 2      if items is None:
 3          items = []
 4      items.append(item)
 5      return items
 6  
 7  print(add_item("a"))
 8  print(add_item("b"))
出力: ['a'] ['a', 'b']期待: ['a'] / ['b'] (エラーにはなりません)

デフォルト引数の[]は関数定義時に一度だけ作られ、呼び出しをまたいで共有されます。Noneを既定値にして、関数内で空リストを作り直すのが定石です。

直し方: [] を None にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  def record(name, log={}    ):
                            ^
 2      if log is None:
 3          log = {}
 4      log[name] = True
 5      return log
 6  
 7  print(record("a"))
 8  print(record("b"))
出力: {'a': True} {'a': True, 'b': True}期待: {'a': True} / {'b': True} (エラーにはなりません)

辞書のデフォルト引数も同じです。共有された1つの辞書に、呼び出しのたびに項目が積み上がっていきます。

直し方: {} を None にします。

この問題を解いてみる →

パターン3

 1  def visit(page, history=[]    ):
                               ^
 2      if history is None:
 3          history = []
 4      history.append(page)
 5      return history
 6  
 7  print(visit("home"))
 8  print(visit("about"))
出力: ['home'] ['home', 'about']期待: ['home'] / ['about'] (エラーにはなりません)

履歴を記録するような関数でも同じ罠にかかります。呼び出し履歴だけのつもりが、他の呼び出しの記録まで混ざります。

直し方: [] を None にします。

この問題を解いてみる →

パターン4

 1  def tag(label, tags=set()):
                          ^
 2      if tags is None:
 3          tags = set()
 4      tags.add(label)
 5      return tags
 6  
 7  print(tag("a"))
 8  print(tag("b"))
出力: {'a'} {'a', 'b'}期待: {'a'} / {'b'} (エラーにはなりません)

集合(set)を既定値にしても同じです。関数定義時に1つだけ作られたsetが、全ての呼び出しで共有されます。

直し方: set() を None にします。

この問題を解いてみる →

パターン5

 1  def enroll(student, roster=[]    ):
                                  ^
 2      if roster is None:
 3          roster = []
 4      roster.append(student)
 5      return roster
 6  
 7  print(enroll("Ken"))
 8  print(enroll("Yui"))
出力: ['Ken'] ['Ken', 'Yui']期待: ['Ken'] / ['Yui'] (エラーにはなりません)

名簿への登録関数でも同じ罠です。呼び出しごとに新しい名簿を渡しているつもりが、1つの名簿を使い回してしまいます。

直し方: [] を None にします。

この問題を解いてみる →

よくある誤解

デフォルト引数は呼び出しのたびに新しく作られると誤解されがちですが、実際には関数定義時に一度だけ作られます。可変オブジェクトを既定値にすると、前回の呼び出しの変更が残ったまま次の呼び出しに引き継がれます。

実務での勘所

この挙動の直接の原因は、デフォルト値がfunc.__defaults__という関数オブジェクト自身の属性として、関数定義時に一度だけ計算されて保存される仕組みにあります。呼び出しのたびに再評価されるのではなく、同じオブジェクトを毎回使い回しているだけなので、可変オブジェクトなら前回までの変更がそのまま残ります。定石の回避策は、デフォルト値をNoneにしておき、def add_item(item, lst=None): lst = [] if lst is None else lstのように関数の中で毎回新しいリストを作ることです。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)