在Python中确定特定函数是否在堆栈上的有效方法

时间:2009-09-10 05:11:10

标签: python callstack

对于调试,通常可以判断特定函数是否在调用堆栈中更高。例如,我们通常只想在某个函数调用我们时运行调试代码。

一种解决方案是检查更高的所有堆栈条目,但这是在堆栈深处并重复调用的函数中,这会导致过多的开销。问题是找到一种方法,允许我们以合理有效的方式确定特定函数是否在调用堆栈中更高。

类似

2 个答案:

答案 0 :(得分:11)

除非你所针对的功能做了一些非常特殊的事情来标记“我的一个实例在堆栈中是活跃的”(IOW:如果该功能是原始的和不可触及的,并且不可能意识到这种特殊的需要(你的),没有任何可能的替代方案,逐帧逐帧地行走,直到你击中顶部(并且功能不在那里)或者你感兴趣的功能的堆栈帧。正如对该问题的一些评论所表明的那样,是否值得努力优化这一点是非常值得怀疑的。但是,假设为了论证它 值得...:

编辑:原始答案(由OP提供)有很多缺陷,但有些已经修复,所以我正在编辑以反映当前的情况以及为什么某些方面很重要。

首先,在装饰器中使用try / exceptwith至关重要,这样才能正确考虑从被监视函数的任何退出,而不仅仅是正常的(作为OP自己答案的原始版本)。

其次,每个装饰者应该确保它保持装饰函数的__name____doc__完整 - 这就是functools.wraps的用途(还有其他方法,但wraps使最简单)。

第三,和第一点一样重要,set,这是OP最初选择的数据结构,是错误的选择:函数可以在堆栈上多次(直接或间接递归) )。我们显然需要一种“多组”(也称为“包”),一种类似于集合的结构,用于跟踪每个项目的“多少次”。在Python中,multiset的自然实现是将数字映射到计数的字典,而这些数字最容易实现为collections.defaultdict(int)

第四,一般方法应该是线程安全的(当这可以很容易地实现,至少;-)。幸运的是,threading.local在适用时使其变得微不足道 - 在这里,它肯定应该是(每个堆栈都有自己独立的调用线程)。

第五,在一些评论中提到了一个有趣的问题(注意到某些答案中提供的装饰者与其他装饰者一起玩得多么糟糕:监视装饰器似乎必须是最后一个(最外层),否则检查会中断。这来自于使用函数对象本身作为监控字典的关键的自然但不幸的选择。

我建议通过一个不同的键选择来解决这个问题:让装饰器采用一个(字符串,说)identifier参数,该参数必须是唯一的(在每个给定的线程中)并使用标识符作为键入监控字典。检查堆栈的代码当然必须知道标识符并使​​用它。

在装饰时,装饰者可以检查唯一性属性(通过使用单独的集合)。标识符可以保留为函数名称的默认值(因此,只有明确要求保持在同一名称空间中监视同名函数的灵活性);当监视目的将多个被监视的函数视为“相同”时,可以明确地放弃uniqueness属性(如果给定的def语句要在稍微不同的上下文中执行多次,则可能会出现这种情况。程序员想要为监视目的考虑“相同功能”的几个函数对象。最后,应该可以选择恢复“功能对象作为标识符”,以用于那些难以进一步修饰的罕见情况(因为在这些情况下,它可能是保证唯一性的最便捷方式)。

因此,将这些考虑因素放在一起,我们可以(包括一个threadlocal_var实用程序函数,当然可能已经在工具箱模块中;-)类似于以下内容......:

import collections
import functools
import threading

threadlocal = threading.local()

def threadlocal_var(varname, factory, *a, **k):
  v = getattr(threadlocal, varname, None)
  if v is None:
    v = factory(*a, **k)
    setattr(threadlocal, varname, v)
  return v

def monitoring(identifier=None, unique=True, use_function=False):
  def inner(f):
    assert (not use_function) or (identifier is None)
    if identifier is None:
      if use_function:
        identifier = f
      else:
        identifier = f.__name__
    if unique:
      monitored = threadlocal_var('uniques', set)
      if identifier in monitored:
        raise ValueError('Duplicate monitoring identifier %r' % identifier)
      monitored.add(identifier)
    counts = threadlocal_var('counts', collections.defaultdict, int)
    @functools.wraps(f)
    def wrapper(*a, **k):
      counts[identifier] += 1
      try:
        return f(*a, **k)
      finally:
        counts[identifier] -= 1
    return wrapper
  return inner

我没有测试过这段代码,因此它可能包含一些拼写错误等,但我提供它是因为我希望它能涵盖我上面解释过的所有重要技术要点。

这一切都值得吗?可能不是,如前所述。但是,我认为,“如果它值得做,那就值得做正确”; - )。

答案 1 :(得分:1)

我真的不喜欢这种方法,但这是你所做的固定版本:

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()

它处理递归,线程和异常。

我不喜欢这种方法有两个原因:

  • 如果函数进一步修饰它不起作用:这必须是最终的装饰器。
  • 如果您正在使用它进行调试,则意味着您必须在两个地方编辑代码才能使用它;一个添加装饰器,一个使用它。只检查堆栈会更方便,因此您只需编辑正在调试的代码中的代码。

更好的方法是直接检查堆栈(可能作为速度的本机扩展),如果可能,找到一种方法来缓存堆栈帧生命周期的结果。 (不过我不确定是否可以在不修改Python核心的情况下实现。)