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

Cas*_*ash 5 python callstack

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

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

类似

Ale*_*lli 12

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

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

首先,使用try/ except或者with在装饰器中是至关重要的,这样才能正确地考虑任何被监视函数的退出,而不仅仅是正常的(正如OP自己的答案的原始版本所做的那样).

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

第三,和第一点一样重要,a set,最初由OP选择的数据结构,是错误的选择:函数可以在堆栈上多次(直接或间接递归).我们显然需要一种"多组"(也称为"包"),一种类似于集合的结构,用于跟踪每个项目的"多少次".在Python中,multiset的自然实现是作为一个dict映射键到计数,而这反过来最简单地实现为collections.defaultdict(int).

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

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

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

在装饰时,装饰者可以检查唯一性属性(通过使用单独的集合).标识符可以保留为函数名称的默认值(因此,只有明确要求保持在同一名称空间中监视同名函数的灵活性); 当多个被监视的函数被认为是"相同的"用于监视目的时,可以明确地放弃唯一性属性(如果给定的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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 拍摄其他回复的编辑不属于对其他回复的评论吗? (2认同)