特定の関数がPythonのスタックにあるかどうかを判断する効率的な方法
質問
デバッグの場合、特定の関数が呼び出しスタックの上位にあるかどうかを確認すると役立つことがよくあります。たとえば、特定の関数が呼び出されたときにのみデバッグコードを実行することがよくあります。
1つの解決策は、上位のすべてのスタックエントリを調べることですが、これはスタックの奥深くにある関数内にあり、繰り返し呼び出されるため、過剰なオーバーヘッドが発生します。問題は、特定の関数が合理的に効率的な方法で呼び出しスタックの上位にあるかどうかを判断できるメソッドを見つけることです。
類似
- フレームオブジェクトから実行スタック上の関数オブジェクトへの参照を取得しますか-この質問は、特定の関数にいるかどうかを判断するのではなく、関数オブジェクトを取得することに焦点を当てています。同じ手法を適用することもできますが、それらは非常に非効率になる可能性があります。
解決
目的の機能が「スタック上でアクティブな1つのインスタンス」をマークするために非常に特別なことをしない限り、 (IOW:関数が手付かずで手に負えないため、この特殊なニーズに気付かない場合)、トップに達するまでスタックをフレームごとに歩くことに代わる考えられる選択肢はありません(そして関数はまたは、目的の機能のスタックフレーム。質問へのいくつかのコメントが示すように、これを最適化するために努力する価値があるかどうかは非常に疑わしいです。しかし、議論のために、それは 価値があると仮定して...:
編集:元の回答(OPによる)には多くの欠陥がありましたが、いくつかは修正されているため、現在の状況と特定の側面が重要である理由を反映するように編集しています。
まず、デコレータで try
/ except
または with
を使用することが重要です。監視されていることは、通常のものだけでなく(OP自身の回答の元のバージョンがしたように)適切に考慮されます。
第二に、すべてのデコレータは、装飾された関数の __ name __
および __ doc __
をそのまま保持することを保証する必要があります-それが functools.wraps
の目的です(他の方法ですが、 wraps
を使用すると最も簡単になります。)
最初のポイントと同じくらい重要な3番目、元々OPによって選択されたデータ構造であった set
は間違った選択です。関数はスタックに数回(直接または間接再帰)。明らかに、「マルチセット」が必要です。 (「バッグ」とも呼ばれます)、「何回」を追跡するセットのような構造です。各アイテムが存在します。 Pythonでは、マルチセットの自然な実装はキーをカウントにマッピングするdictとしてであり、これは collections.defaultdict(int)
として最も便利に実装されます。
第4に、一般的なアプローチはスレッドセーフである必要があります(少なくとも簡単に達成できる場合は、-;)。幸いなことに、 threading.local
は、適切な場合、それを些細なものにします-そして、ここでは、確実にそうすべきです(各スタックは独自の呼び出しスレッドを持っています)。
第5に、いくつかのコメントでブローチされた興味深い問題です(いくつかの回答で提供されたデコレータが他のデコレータとどれだけひどく気づいているかに注意してください:監視デコレータは最後(最も外側)のものでなければならないように見えます、そうでなければチェックが壊れます。これは、監視ディクショナリのキーとして関数オブジェクト自体を使用するという自然ではあるが残念な選択から来ています。
別のキーの選択でこれを解決することを提案します:デコレータに(文字列、たとえば) identifier
引数を(それぞれのスレッドで)一意にし、識別子を監視辞書のキー。スタックをチェックするコードはもちろん識別子を認識し、同様に使用する必要があります。
装飾時に、装飾者は一意性プロパティを確認できます(別のセットを使用して)。識別子はデフォルトで関数名のままにすることができます(したがって、同じ名前空間内の同名の関数を監視する柔軟性を維持するために明示的にのみ必要です)。いくつかの監視対象機能が「同じ」と見なされる場合、一意性プロパティは明示的に放棄されます。監視目的のために(これは、特定の def
ステートメントがわずかに異なるコンテキストで複数回実行され、プログラマが「同じ関数」と見なしたい複数の関数オブジェクトを作成する場合に発生する可能性があります監視目的)。最後に、「関数オブジェクトを識別子として」にオプションで戻すことができるはずです。それ以上の装飾が不可能であることが知られているまれなケースのために(これらのケースでは一意性を保証する最も便利な方法かもしれないので)。
だから、これらの多くの考慮事項を入れて
他のヒント
私はこのアプローチがあまり好きではありませんが、ここにあなたがやっていることの修正版があります:
from collections import defaultdict
import threading
functions_on_stack = threading.local()
def record_function_on_stack(f):
def wrapped(*args, **kwargs):
if not getattr(functions_on_stack, "stacks", None):
functions_on_stack.stacks = defaultdict(int)
functions_on_stack.stacks[wrapped] += 1
try:
result = f(*args, **kwargs)
finally:
functions_on_stack.stacks[wrapped] -= 1
if functions_on_stack.stacks[wrapped] == 0:
del functions_on_stack.stacks[wrapped]
return result
wrapped.orig_func = f
return wrapped
def function_is_on_stack(f):
return f in functions_on_stack.stacks
def nested():
if function_is_on_stack(test):
print "nested"
@record_function_on_stack
def test():
nested()
test()
これは、再帰、スレッド、例外を処理します。
このアプローチは2つの理由で好きではありません:
- 関数がさらに装飾されている場合は機能しません。これは最終的な装飾子でなければなりません。
- これをデバッグに使用している場合、使用するにはコードを2か所で編集する必要があります。 1つはデコレータを追加し、もう1つはそれを使用します。スタックを調べるだけの方がはるかに便利なので、デバッグしているコード内のコードを編集するだけで済みます。
より良いアプローチは、スタックを直接(おそらく速度のネイティブ拡張として)調べて、可能であれば、スタックフレームの有効期間中、結果をキャッシュする方法を見つけることです。 (ただし、Pythonコアを変更せずにそれが可能かどうかはわかりません。)