Stackless Pythonで関数呼び出しの深さ制限を突破する
PythonでLISPっぽい何かを作ってみた - ラシウラでは、evalメソッドではfuncオブジェクト内でも再帰的に呼び出されるため、例中のfactでも引数を100程度にしてしまうと、"RuntimeError: maximum recursion depth exceeded"が発生してしまいます。
再帰にしないよう、eval(やそれを呼び出してる部分)のコード自体を書き換えることも可能ですが、それをやるとコードが複雑になります。
そこで、Stackless Pythonを使って、なるべく元のコードのままで、深さ制限をはずしてみることにします。
Stackless Python自体は、自動的には深さ制限が外れません。stacklessモジュールを使うことで、関数呼び出しをフラットにすることができます。
元コードでの変更部分はScopeクラスのevalのみで、以下のようなcall経由で呼び出すようにするだけです
class Scope(Atom): ... def eval(self, obj): return call(self._eval, obj) def _eval(self, obj): if isinstance(obj, Symbol): return self.get(obj.name) if isinstance(obj, Cons): func = self.eval(obj.head) return func.call(obj.tail, self) return obj
このcallは以下のようなになります。stackless pythonのtaskletを使う実装です
import stackless def call(f, *args, **kargs): ch = stackless.channel() stackless.tasklet(lambda:ch.send(f(*args,**kargs)))() return ch.receive()
これによって、(fact 100)でも(fact 1000)でも実行可能になりました。
軽量でないthreadでは
このtaskletはstackless pythonの特徴である軽量スレッド呼び出しで、以下のような標準ライブラリのthreadを使った呼び出しと等価です
from thread import start_new_thread from Queue import Queue def call(f, *args, **kargs): ch = Queue() start_new_thread(lambda:ch.put(f(*args,**kargs)), ()) return ch.get()
再帰段数以上に厳しいthread数制限があるので、この実装では呼び出し深さ問題解決にはなりません。深さが低い場合には、このcallでも同様に稼動します。
上のcall実装はマルチスレッドコードであるけれど、呼び出し側ではスレッドを作ってすぐにch.getで待ち状態に入っていて、スレッド側もch.putの後即終了するため、平行で動くコード部分は存在しない実装になってます。普通に以下のcall実装と同じ呼び出し順になります。
def call(f, *args, **kargs): return f(*args, **kargs)
以下のように、上記二つをミックスすることで、深さ制限を標準より少し緩めることは可能です。
import threading from thread import start_new_thread from Queue import Queue reccount = threading.local() def call(f, *args, **kargs): if hasattr(reccount, "count"): reccount.count += 1 else: reccount.count = 1 try: if reccount.count > 100: ch = Queue() start_new_thread(lambda:ch.put(f(*args,**kargs)), ()) return ch.get() else: return f(*args, **kargs) pass finally: reccount.count -= 1 pass pass
この実装であれば、(fact 1000)程度は実行可能です
put it together
try: # for stackless-python import stackless def call(f, *args, **kargs): ch = stackless.channel() stackless.tasklet(lambda:ch.send(f(*args,**kargs)))() return ch.receive() pass except: # for standard python import threading from thread import start_new_thread from Queue import Queue reccount = threading.local() def call(f, *args, **kargs): if hasattr(reccount, "count"): reccount.count += 1 else: reccount.count = 1 try: if reccount.count > 100: ch = Queue() start_new_thread(lambda:ch.put(f(*args,**kargs)), ()) return ch.get() else: return f(*args, **kargs) pass finally: reccount.count -= 1 pass pass pass
Python2.5ならdecoratorにしてもいいかも
def rec(f): def wrapper(*args, **kargs): return call(f, *args, **kargs) return wrapper class Scope(Atom): ... @rec def eval(self, obj): if isinstance(obj, Symbol): return self.get(obj.name) if isinstance(obj, Cons): func = self.eval(obj.head) return func.call(obj.tail, self) return obj