5

For debugging, it is often useful to tell if a particular function is higher up on the call stack. For example, we often only want to run debugging code when a certain function called us.

One solution is to examine all of the stack entries higher up, but it this is in a function that is deep in the stack and repeatedly called, this leads to excessive overhead. The question is to find a method that allows us to determine if a particular function is higher up on the call stack in a way that is reasonably efficient.

Similar

Community
  • 1
  • 1
Casebash
  • 114,675
  • 90
  • 247
  • 350
  • Is it really useful when debugging python code to reference the call stack? If you have cases where your that concerned about efficiency you'd probably be better off using C instead. – monkut Sep 10 '09 at 05:16
  • Seems pretty obvious; logging function behavior when you're called from another function, etc. – Glenn Maynard Sep 10 '09 at 05:22
  • 1
    @monkut: The efficiency really can work out horribly. I'm not going to switch to another language just because the debugging function is a bit slow! – Casebash Sep 10 '09 at 05:45
  • @Glenn, updated question to make it clearer when logging isn't suitable – Casebash Sep 10 '09 at 06:09
  • I'm not sure what you mean. I was telling monkut why this is useful. – Glenn Maynard Sep 10 '09 at 06:17
  • @Glenn. nm, completely misunderstood you – Casebash Sep 10 '09 at 06:23
  • 1
    Who cares about efficiency if it's only for debugging purposes? The code will never run in release, right? Go ahead and look at the stack trace, if it helps you debug. It would be a heinous thing to have in production code, though. – bobince Sep 10 '09 at 08:04
  • -1: I can't see the use case for this at all. "we often only want to run debugging code when a certain function called us" Doesn't make sense. Why not simply use unit tests? – S.Lott Sep 10 '09 at 10:52
  • @Lott. Sometimes we need a lot of other code to set up the call. UNIT tests are one of the most effective ways of testing, but quite time consuming. Bobince has a better point, but, efficiency can make a difference even during testing. – Casebash Sep 10 '09 at 11:50
  • @Casebash, if unit tests are time consuming, you're doing them wrong: with plenty of mocks, stubs, dependency injection, and good design, the main systems I maintain at work (systems of tens of thousands of lines of app-specific code each, plus many shared and reused components and subsystems) have 95% coverage and up with unit-test suites that run entirely in 5 to 10 seconds for each system. Of course there are layers of integration tests, too, but those need to behave as close as possible to "real runs";-). – Alex Martelli Sep 10 '09 at 14:17

2 Answers2

14

Unless the function you're aiming for does something very special to mark "one instance of me is active on the stack" (IOW: if the function is pristine and untouchable and can't possibly be made aware of this peculiar need of yours), there is no conceivable alternative to walking frame by frame up the stack until you hit either the top (and the function is not there) or a stack frame for your function of interest. As several comments to the question indicate, it's extremely doubtful whether it's worth striving to optimize this. But, assuming for the sake of argument that it was worthwhile...:

Edit: the original answer (by the OP) had many defects, but some have since been fixed, so I'm editing to reflect the current situation and why certain aspects are important.

First of all, it's crucial to use try/except, or with, in the decorator, so that ANY exit from a function being monitored is properly accounted for, not just normal ones (as the original version of the OP's own answer did).

Second, every decorator should ensure it keeps the decorated function's __name__ and __doc__ intact -- that's what functools.wraps is for (there are other ways, but wraps makes it simplest).

Third, just as crucial as the first point, a set, which was the data structure originally chosen by the OP, is the wrong choice: a function can be on the stack several times (direct or indirect recursion). We clearly need a "multi-set" (also known as "bag"), a set-like structure which keeps track of "how many times" each item is present. In Python, the natural implementation of a multiset is as a dict mapping keys to counts, which in turn is most handily implemented as a collections.defaultdict(int).

Fourth, a general approach should be threadsafe (when that can be accomplished easily, at least;-). Fortunately, threading.local makes it trivial, when applicable -- and here, it should surely be (each stack having its own separate thread of calls).

Fifth, an interesting issue that has been broached in some comments (noticing how badly the offered decorators in some answers play with other decorators: the monitoring decorator appears to have to be the LAST (outermost) one, otherwise the checking breaks. This comes from the natural but unfortunate choice of using the function object itself as the key into the monitoring dict.

I propose to solve this by a different choice of key: make the decorator take a (string, say) identifier argument that must be unique (in each given thread) and use the identifier as the key into the monitoring dict. The code checking the stack must of course be aware of the identifier and use it as well.

At decorating time, the decorator can check for the uniqueness property (by using a separate set). The identifier may be left to default to the function name (so it's only explicitly required to keep the flexibility of monitoring homonymous functions in the same namespace); the uniqueness property may be explicitly renounced when several monitored functions are to be considered "the same" for monitoring purposes (this may be the case if a given def statement is meant to be executed multiple times in slightly different contexts to make several function objects that the programmers wants to consider "the same function" for monitoring purposes). Finally, it should be possible to optionally revert to the "function object as identifier" for those rare cases in which further decoration is KNOWN to be impossible (since in those cases it may be the handiest way to guarantee uniqueness).

So, putting these many considerations together, we could have (including a threadlocal_var utility function that will probably already be in a toolbox module of course;-) something like the following...:

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

I have not tested this code, so it might contain some typo or the like, but I'm offering it because I hope it does cover all the important technical points I explained above.

Is it all worth it? Probably not, as previously explained. However, I think along the lines of "if it's worth doing at all, then it's worth doing right";-).

Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 2
    Doesn't the edit that shoots the other response down belong as a comment to that other response? – Martin v. Löwis Sep 10 '09 at 05:31
  • @Martin, too long for a comment in SO. I think it's fine in an answer to point out why several possible alternatives are utter disasters (if and when that HORRIBLE self-answer is at long last deleted I can easily edit this answer to be more generic;-). – Alex Martelli Sep 10 '09 at 05:35
  • @Alex, thank you for your points. I added the note that it doesn't support multi-threading. Fixed up the issue with recursion. About to handle exceptions. I'm not going to delete the answer unless another better solution is posted. – Casebash Sep 10 '09 at 05:56
  • @Alex, try to play nice. I'm fine with brusqueness, seeing as what you said was useful, but we don't want to scare everyone off the site. – Casebash Sep 10 '09 at 06:07
  • @Alex, lol you have enough reputation to spare, are you really worried about losing 1 rep point? – Unknown Sep 10 '09 at 06:12
  • @Unknown, not especially "worried", but I just don't like downvoting when it can be avoided. @Casebash, I hope I didn't offend your sensibility with my peeved tone (though you did offend mine with your misconceived code;-) -- edited to be drier and punctual. And, also @Martin, if you want THIS kind of content to be in comments, go lobby on meta to completely change comments (currently limited to very small amounts of text AND horribly impoverished as to formatting facilities, so clearly **NOT** suitable for this kind of critique). – Alex Martelli Sep 10 '09 at 15:02
  • It's a waste of time to try. Apparently the people who run this site *like* the fact that comments are crippled, hinder useful dialogue, make it less likely that people will come to correct answers, and force people to submit comments as answers. – Glenn Maynard Sep 10 '09 at 19:11
  • @Glenn, OK, then it's no use asking for info that's long and/or requires paragraphs and such formatting to be presented in comments rather than in answers;-). – Alex Martelli Sep 10 '09 at 19:24
  • @Alex I suppose it just comes down to this difference. You write production code where all of these are big issues, my code is just for research, so I tend to take a few shortcuts – Casebash Sep 10 '09 at 22:45
  • Of course, I'll try to make sure any code I post is more production ready in the future – Casebash Sep 10 '09 at 22:46
  • @Casebash, I do write a lot of research-y/exploratory code, but then I never obsess about the peformance of THAT code -- when I do care about performance, that's code that I think is going to run many times and probably on many machines -- simple issue of return (less time waiting) on investment (more time developing;-). – Alex Martelli Sep 11 '09 at 00:36
  • @Alex - You are right that I obsess about efficiency. Comes because I used to compete in programming competitions which were all about efficiency. I'm think I'm getting better, but its still an issue – Casebash Sep 11 '09 at 00:44
  • @functools.wraps(f): has an extra colon – Casebash Sep 11 '09 at 01:55
  • 1
    @Casebash, tx for spotting, edited to fix. Re efficiency obsession, I've learned to vent it out in small personal "toys", so in exploratory/researchy code I can instead obsess on quick turnaroung, and in production code I obsess on solidity, robustness, reliability, scalability, maintainability -- fanatical testing, monitoring, sharding and replication, loose coupling and tight cohesion, right degree of generality, &c. I ignore small efficiencies, say 97% of the time: premature optimization is the root of all evil in programming...;-). profiling/loadtesting, 3% of the time, says otherwise;-). – Alex Martelli Sep 11 '09 at 03:11
1

I don't really like this approach, but here's a fixed-up version of what you were doing:

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

This handles recursion, threading and exceptions.

I don't like this approach for two reasons:

  • It doesn't work if the function is decorated further: this must be the final decorator.
  • If you're using this for debugging, it means you have to edit code in two places to use it; one to add the decorator, and one to use it. It's much more convenient to just examine the stack, so you only have to edit code in the code you're debugging.

A better approach would be to examine the stack directly (possibly as a native extension for speed), and if possible, find a way to cache the results for the lifetime of the stack frame. (I'm not sure if that's possible without modifying the Python core, though.)

Glenn Maynard
  • 55,829
  • 10
  • 121
  • 131
  • I made a quick attempt to implement per-stack-frame caching (in Python, not natively), but unfortunately frame objects can't be weak referenced. I guess that's probably performance-related, but it's still annoying. – Glenn Maynard Sep 10 '09 at 06:38
  • @Glenn: Thanks heaps. Next time I post something here I'll be sure to make it threadsafe and exception proof. PS. won't functions_on_stack be assigned only once, not once per thread? So shouldn't we call threading.local() every time we need it? – Casebash Sep 10 '09 at 06:48
  • "Next time I post something here I'll be sure to make it threadsafe and exception proof." Or if this is too complex, at least note the limitations – Casebash Sep 10 '09 at 06:49
  • Not functions_on_stacks, but functions_on_stacks.stacks (edited). – Glenn Maynard Sep 10 '09 at 07:07
  • Nice, another mystery downvote. Votes on this site pretty useless, frankly. – Glenn Maynard Sep 10 '09 at 19:04