Manera eficiente de determinar si una función particular está en la stack en Python

Para la depuración, a menudo es útil saber si una función en particular está más arriba en la stack de llamadas. Por ejemplo, a menudo solo queremos ejecutar código de depuración cuando una determinada función nos llama.

Una solución es examinar todas las entradas de la stack más arriba, pero esto se encuentra en una función que se encuentra en lo profundo de la stack y se llama repetidamente, lo que conduce a una sobrecarga excesiva. La pregunta es encontrar un método que nos permita determinar si una función en particular está más arriba en la stack de llamadas de una manera que sea razonablemente eficiente.

Similar

  • ¿Obteniendo referencias a objetos de función en la stack de ejecución del objeto de marco? – Esta pregunta se enfoca en obtener los objetos de la función, en lugar de determinar si estamos en una función particular. Aunque podrían aplicarse las mismas técnicas, pueden llegar a ser extremadamente ineficientes.

    A menos que la función a la que apuntas haga algo muy especial para marcar “una instancia de mí está activa en la stack” (IOW: si la función es prístina e intocable y no se puede hacer consciente de esta necesidad peculiar de la tuya) , no hay una alternativa concebible para caminar cuadro por cuadro hasta la stack hasta que llegue a la parte superior (y la función no está allí) o al marco de la stack para su función de interés. Como lo indican varios comentarios a la pregunta, es extremadamente dudoso que valga la pena esforzarse por optimizar esto. Pero, suponiendo por el bien del argumento que valió la pena …:

    Edición : la respuesta original (por el OP) tuvo muchos defectos, pero algunos se han corregido, por lo que estoy editando para reflejar la situación actual y por qué ciertos aspectos son importantes.

    En primer lugar, es crucial usar try / except , o with , en el decorador, para que CUALQUIER salida de una función que se está monitoreando se tenga en cuenta, no solo las normales (como lo hizo la versión original de la propia respuesta del OP).

    En segundo lugar, todos los decoradores deben asegurarse de mantener intactos los __name__ y __doc__ la función decorada: para eso están los functools.wraps (hay otras formas, pero los wraps hacen más simple).

    En tercer lugar, tan importante como el primer punto, un set , que fue la estructura de datos originalmente elegida por el OP, es la elección incorrecta: una función puede estar en la stack varias veces (recursión directa o indirecta). Claramente necesitamos un “conjunto múltiple” (también conocido como “bolsa”), una estructura similar a un conjunto que mantiene un registro de “cuántas veces” está presente cada elemento. En Python, la implementación natural de un multiset es como una asignación de claves de conteo a recuentos, que a su vez se implementa de manera más práctica como collections.defaultdict(int) .

    Cuarto, un enfoque general debe ser seguro para subprocesos (cuando eso se puede lograr fácilmente, al menos ;-). Afortunadamente, threading.local hace trivial, cuando es aplicable, y aquí, seguramente debería serlo (cada stack tiene su propio hilo de llamadas separado).

    Quinto, un tema interesante que se ha abordado en algunos comentarios (al darse cuenta de lo mal que juegan los decoradores en algunas respuestas con otros decoradores: el decorador de monitoreo parece ser el ÚLTIMO (más externo); la elección natural pero desafortunada de usar el objeto de función en sí mismo como la clave en el dictado de monitoreo.

    Propongo resolver esto mediante una elección de clave diferente: hacer que el decorador tome un argumento de identifier (cadena, digamos) que debe ser único (en cada subproceso dado) y use el identificador como la clave en el dictado de monitoreo. El código que comprueba la stack debe, por supuesto, conocer el identificador y utilizarlo también.

    En el momento de decorar, el decorador puede verificar la propiedad de unicidad (utilizando un conjunto separado). El identificador puede dejarse como predeterminado para el nombre de la función (por lo que solo se requiere explícitamente para mantener la flexibilidad de monitorear las funciones homónimas en el mismo espacio de nombres); la propiedad de unicidad se puede renunciar explícitamente cuando varias funciones supervisadas deben considerarse “las mismas” para fines de supervisión (este puede ser el caso si se pretende que una determinada instrucción def se ejecute varias veces en contextos ligeramente diferentes para realizar varios objetos de función que los progtwigdores quieren considerar “la misma función” para propósitos de monitoreo). Finalmente, debería ser posible volver opcionalmente al “objeto de función como identificador” para aquellos casos raros en los que se sepa que una decoración adicional es imposible (ya que en esos casos puede ser la forma más fácil de garantizar la singularidad).

    Entonces, al juntar estas muchas consideraciones, podríamos tener (incluida una función de utilidad threadlocal_var que probablemente ya esté en un módulo de la caja de herramientas, por supuesto 😉 algo como lo siguiente …

     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 

    No he probado este código, por lo que podría contener algún error tipográfico o similar, pero lo ofrezco porque espero que cubra todos los puntos técnicos importantes que expliqué anteriormente.

    ¿Vale la pena? Probablemente no, como se explicó anteriormente. Sin embargo, pienso en la línea de “si vale la pena hacerlo, entonces vale la pena hacerlo bien” ;-).

    Realmente no me gusta este enfoque, pero aquí hay una versión corregida de lo que estaba haciendo:

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

    Esto maneja la recursividad, hilos y excepciones.

    No me gusta este enfoque por dos razones:

    • No funciona si la función está decorada aún más: este debe ser el decorador final.
    • Si está utilizando esto para la depuración, significa que tiene que editar el código en dos lugares para usarlo; uno para agregar el decorador, y otro para usarlo. Es mucho más conveniente simplemente examinar la stack, de modo que solo tiene que editar el código en el código que está depurando.

    Un mejor enfoque sería examinar la stack directamente (posiblemente como una extensión nativa de la velocidad) y, si es posible, encontrar una manera de almacenar en caché los resultados durante la vida útil del marco de la stack. (No estoy seguro si eso es posible sin modificar el núcleo de Python, sin embargo).