はじめに
pythonでは関数の引数にデフォルト値を設定することができます。
この機能を使うと、引数が与えられなかったときの挙動を定義することができ、とても便利です。
>>> def f(x="hoge"): ... print(x) ... >>> f("aiu") aiu >>> f(x="aiu") aiu >>> f() hoge
ただし、デフォルト値にlistやdictなどのmutableな型を使うと容易にハマります。それについて説明します。
ハマる例
デフォルト値に空のリストを追加したとします。
>>> def g(x, l=[]): ... l.append(x) ... return l ... >>> g(10) [10] >>> g(20) [10, 20]
さっそくおかしな感じになっています。lが呼び出しのたびに更新されていないようです。そのせいで、前の値が残ってしまうようです。
検証
listを継承したクラスを作り、コンストラクタが呼ばれるとメッセージを表示するようにします。
>>> class mylist(list): ... def __init__(self, *args, **kargs): ... print("__init__ of mylist") ... list.__init__(self, *args, **kargs) ... >>> l = mylist() __init__ of mylist >>> l [] >>> l.append(1) >>> l [1]
__init__にメッセージを挟んだ以外は何もいじっていないので、元のlistと同様に扱えます。
このインスタンスをデフォルト値にしてみます。
>>> def h(x, l=mylist()): ... l.append(x) ... return l ... __init__ of mylist >>> h(10) [10] >>> h(20) [10, 20]
なんということでしょう、defが実行されたタイミングで評価されて、それっきりです。
解説と対策
これは、実はドキュメントにも説明がある事項です。
重要な警告: デフォルト値は 1 度だけしか評価されません。デフォルト値がリストや辞書のような変更可能なオブジェクトの時にはその影響がでます。
要するに「仕様」ということですね。
対策は、これも上のページに書いてあることですが、デフォルト値はNoneにしておき、都度関数の中でインスタンス化することです。
>>> def i(x, l=None): # ここは定義時評価 ... if l is None: # ここは実行時評価 ... l = [] ... l.append(x) ... return l ... >>> i(10) [10] >>> i(20) [20] >>> i(10, l=[-10, 0]) [-10, 0, 10]
わかってしまえば簡単な話ですが、気づかないと大変なミスをやらかすので注意してください。
逆に、これを利用してクロージャもどきとして使ったり、難読コードを書くことも可能です(やらないけど)。
まとめ
再帰的にリストを処理するような関数を書くとき、def f(l=[]):のように書きたくなってしまいますが、バグを産むので注意してください。