5月 07 2009

Python のデフォルト引数は一度しか評価されない

Published by HoLY at 21:26:54 under tech

最近は仕事なんかで Python でコードを書くことが結構あるのだけど、知らずに惑ったことの一つに、関数のデフォルト引数が定義時にしか評価されない仕様というのがある。

デフォルトパラメタ値は関数定義を実行する際に値評価されます。 これは、デフォルトパラメタの式は関数を定義するときにただ一度だけ評価され、同じ”計算済みの”値が全ての呼び出しで使われることを意味します。

例えば無理矢理な例だけど、こんな風に使うことも可能。関数ローカルの変数扱いですな。

def fib(num, cache=[0,1]):
    assert len(cache) > 1

    if len(cache) > num:
        return cache[num]
    else:
        cache[num:num] = [fib(num-1) + fib(num-2)]
        return cache[num]

print fib(10) # => 55

試しに、この仕様に反して、 Python でも呼びだしの度にデフォルト引数が初期化されるようなコードを、デコレータを使って書いてみた。 デコレータというのは簡単に言うと関数に被せるラッパーを書くためのシンタックスシュガー。

def newdefaults(fn):
    import copy
    arity = fn.func_code.co_argcount
    newdefaults.defaults = copy.deepcopy(fn.func_defaults)

    def newfn(*args):
        args = list(args)

        while len(args) < arity:
            index = len(args) - arity + len(newdefaults.defaults)
            if index < 0: # 引数たりない
                return fn(*args)
            arg = newdefaults.defaults[index]
            args.append(copy.deepcopy(arg))
        return fn(*args)

    return newfn

if __name__ == '__main__':

    @newdefaults
    def f(v1, v2, d=0, a=[0]):
        print "f(%s, %s, %s, %s)" % (v1, v2, d, a)
        a.append(v1)
        return a

    assert f(1,0) == [0,1]
    assert f(2,0) == [0,2]
    try:
        f()
        assert False
    except TypeError, e:
        print e # OK
    except:
        print "Unexpected error"
        assert False

結果はこう。

f(1, 0, 0, [0])
f(2, 0, 0, [0])
f() takes at least 2 arguments (0 given)
  • *args および **kwargs を引数にとる関数には適用できないはず。
  • ポイント: 関数の様子はある程度は function のプロパティ、f.func_defaultsf.func_code などから分かる。もっと詳しく調べたいときは import inspect
  • deepcopy しちゃうとまずい時もありそうだけどまあいいか。

Tags:

No responses yet

Trackback URI | Comments RSS

Leave a Reply