【问题标题】:Efficient way to determine whether a particular function is on the stack in Python在 Python 中确定特定函数是否在堆栈上的有效方法
【发布时间】:2010-11-27 01:41:50
【问题描述】:

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

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

类似

【问题讨论】:

  • 调试python代码引用调用栈真的有用吗?如果您在某些情况下担心效率,则最好使用 C。
  • 看起来很明显;当你被另一个函数调用时记录函数行为,等等。
  • @monkut:效率真的可以很糟糕。不会因为调试功能有点慢就换其他语言了!
  • @Glenn,更新了问题,以便在不适合记录时更清楚
  • 如果只是为了调试,谁在乎效率?代码永远不会在发行版中运行,对吧?继续查看堆栈跟踪,如果它可以帮助您调试。但是,在生产代码中拥有这将是一件令人发指的事情。

标签: python callstack


【解决方案1】:

除非您的目标函数做了一些非常特殊的事情来标记“我的一个实例在堆栈上处于活动状态”(IOW:如果该函数是原始且不可触及并且不可能意识到这种特殊需要你的),没有其他替代方法可以逐帧遍历堆栈,直到您到达顶部(并且该函数不存在)或您感兴趣的函数的堆栈帧。正如该问题的几位专家所指出的那样,是否值得努力优化这一点非常值得怀疑。但是,为了论证的目的,假设它值得的......:

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

首先,在装饰器中使用try/exceptwith 非常重要,以便正确考虑从被监视函数的任何退出,而不仅仅是正常退出(如原始OP自己的答案的版本确实如此)。

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

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

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

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

我建议通过选择不同的键来解决这个问题:让装饰器采用(例如)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

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

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

【讨论】:

  • 将其他回复击倒的编辑不属于对其他回复的评论吗?
  • @Martin,SO 中的评论太长了。我认为在回答中指出为什么有几种可能的选择是彻底的灾难是很好的(如果以及当那个可怕的自我答案最终被删除时,我可以轻松地编辑这个答案以使其更通用;-)。
  • @Alex,谢谢你的意见。我添加了它不支持多线程的注释。修复了递归的问题。即将处理异常。除非发布另一个更好的解决方案,否则我不会删除答案。
  • @Alex,试着表现得很好。我可以接受直率,因为你说的很有用,但我们不想吓跑网站上的所有人。
  • @Casebash,tx 用于发现,已编辑修复。对效率的痴迷,我已经学会在小型个人“玩具”中发泄它,所以在探索性/研究性代码中,我可以专注于快速转变,而在生产代码中,我专注于坚固性、稳健性、可靠性、可扩展性、可维护性 - - 狂热的测试、监控、分片和复制、松耦合和紧内聚、正确的通用性等。我忽略了小的效率,比如 97% 的时间:过早的优化是编程中万恶之源……;-)。分析/负载测试,3% 的时间,另有说明;-)。
【解决方案2】:

我不太喜欢这种方法,但这里是您所做工作的修正版:

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 核心的情况下是否可行。)

【讨论】:

  • 我快速尝试实现每栈帧缓存(在 Python 中,而不是本机),但不幸的是帧对象不能被弱引用。我想这可能与性能有关,但仍然很烦人。
  • @Glenn:非常感谢。下次我在这里发布一些东西时,我一定会使其成为线程安全和异常证明。 PS。 functions_on_stack 不会只分配一次,而不是每个线程一次?那么我们不应该在每次需要的时候调用 threading.local() 吗?
  • “下次我在这里发布一些东西时,我一定会确保它是线程安全和异常证明的。”或者如果这太复杂,至少要注意限制
  • 不是functions_on_stacks,而是functions_on_stacks.stacks(已编辑)。
  • 很好,又一个神秘的反对票。坦率地说,在这个网站上投票毫无用处。
猜你喜欢
  • 1970-01-01
  • 2011-01-04
  • 2021-05-25
  • 1970-01-01
  • 1970-01-01
  • 2017-05-20
  • 2019-09-04
  • 2012-06-30
  • 1970-01-01
相关资源
最近更新 更多