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

2024-07-06 20:25:39 发布

您现在位置:Python中文网/ 问答频道 /正文

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

一种解决方案是检查更高级别的所有堆栈项,但如果这是在堆栈深处反复调用的函数中,这会导致过多的开销。问题是找到一种方法,使我们能够以合理有效的方式确定某个特定函数是否位于调用堆栈的更高位置。在

相似


Tags: theto方法函数代码objects堆栈on
2条回答

除非你要找的函数做了一些非常特别的事情来标记“我的一个实例在堆栈上是活动的”(low:如果函数是原始的、不可接触的,并且不可能被告知你的这种特殊需求),在您找到感兴趣的函数的顶部(函数不在那里)或堆栈帧之前,没有任何可想象的替代方法。正如对这个问题的一些评论所表明的,是否值得努力优化这一点是非常值得怀疑的。但是,为了论证,假设这是值得的…:

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

首先,在decorator中使用try/except,或者with,这一点很重要,这样就可以正确地说明从被监视函数中的任何退出,而不仅仅是正常的退出(就像原始版本的OP自己的答案所做的那样)。在

其次,每个decorator都应该确保修饰函数的__name__和{}的完整性,这就是functools.wraps的作用(还有其他方法,但是wraps使其更简单)。在

第三,与第一点一样重要的是,OP最初选择的数据结构set是错误的选择:一个函数可以在堆栈上多次(直接或间接递归)。我们显然需要一个“多套”(也称为“包”),一套类似的结构,可以记录每件物品的“出现次数”。在Python中,multiset的自然实现是将键映射到counts的dict映射,而这又最容易实现为collections.defaultdict(int)。在

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

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

我建议通过不同的键选择来解决这个问题:让decorator接受一个(string,比如)identifier参数,该参数必须是唯一的(在每个给定的线程中),并将标识符用作监视dict的键。检查堆栈的代码当然必须知道标识符并使用它。在

在装饰时,装饰器可以检查唯一性属性(通过使用单独的集合)。标识符可以保留为函数名的默认值(因此只需要显式地保持监视同一命名空间中同名函数的灵活性);当多个被监视的函数被认为是“相同的”以进行监视时,唯一性属性可能被显式放弃(如果给定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

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

这一切值得吗?可能不是,正如前面解释的那样。然而,我认为“如果它值得做,那么它值得做正确的”;—)。在

我真的不喜欢这种方法,但这里有一个你正在做的修正版本:

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内核,我不确定这是否可行。)

相关问题 更多 >