tanihito’s blog

デジタル・新規事業開発・健康など、興味のあることについてつらつらと書いてきます。

Pythonのクロージャで末尾再帰最適化をする。

元ネタはPythonで末尾再帰最適化をする。 - wasabizの日記Pythonのデコレータを使って、関数の末尾再起最適化を行う、というものです。

元エントリではクラスを使ってデコレータを作っていますが、これではラップした関数がクラスになってしまいます。

>>> @tail_recursive
... def sum(n, acc=0):
...     if n == 0:
...         return acc
...     else:
...         return sum(n-1, acc+n)
... 
>>> type(sum)
<class '__main__.tail_recursive'>
>>> sum.func_name
AttributeError: 'tail_recursive' object has no attribute 'func_name'

関数だと思ったら実はクラスだった、というのはなんか気持ち悪いですよね。そこで末尾呼び出し最適化を行なうデコレータをクロージャを使って書き直してみます。

from functools import wraps

def tail_recursive(func):
    self_func = [func]
    self_firstcall = [True]
    self_CONTINUE = [object()]
    self_argskwd = [None]
    
    @wraps(func)
    def _tail_recursive(*args, **kwd):
        if self_firstcall[0] == True:
            func = self_func[0]
            CONTINUE = self_CONTINUE
            self_firstcall[0] = False
            try:
                while True:
                    result = func(*args, **kwd)
                    if result is CONTINUE:  # update arguments
                        args, kwd = self_argskwd[0]
                    else: # last call
                        return result
            finally:
                self_firstcall[0] = True
        else: # return the arguments of the tail call
            self_argskwd[0] = args, kwd
            return self_CONTINUE
    return _tail_recursive

変数名は元エントリの変数名と対応しています。self_funcなどの変数がリストになっていますが、これは「内側にある関数内で外側にある関数の変数に値を代入できない」ことに対処するためです。また、functools.wrapsは元の関数 (sum) の関数名やドキュメント文字列を保持するために使っています。

さきほどと同様にsum関数をラップしてみると、ちゃんと関数になっていることが分かります。

>>> @tail_recursive
... def sum(n, acc=0):
...     if n == 0:
...         return acc
...     else:
...         return sum(n-1, acc+n)
... 
>>> type(sum)
<type 'function'>
>>> sum.func_name
'sum'
>>> sum(100000)
5000050000L