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

StackOverflow https://stackoverflow.com/questions/1403471

  •  05-07-2019
  •  | 
  •  

Pregunta

Para la depuración, a menudo es útil saber si una función en particular está más arriba en la pila 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 pila más arriba, pero esto es en una función que está en la pila y que se llama repetidamente, esto lleva 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 pila de llamadas de una manera razonablemente eficiente.

Similar

¿Fue útil?

Solución

A menos que la función a la que te apuntas haga algo muy especial para marcar "una instancia de mí está activa en la pila" (IOW: si la función es prístina e intocable y no se puede hacer consciente de esta peculiar necesidad de la suya), no hay una alternativa concebible para caminar de marco en marco hasta la pila hasta que llegue a la parte superior (y la función es no allí) o un marco de pila 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 valía ...:

Editar : la respuesta original (por parte del OP) tenía muchos defectos, pero algunos ya se han solucionado, 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 / excepto , o con , en el decorador, para que CUALQUIER salida de una función ser monitoreado se tiene en cuenta adecuadamente, no solo los 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 el __name__ de la función decorada y __doc__ intactos, para eso está functools.wraps (hay son otras formas, pero wraps lo hace más simple).

Tercero, tan crucial como el primer punto, un set , que era la estructura de datos elegida originalmente por el OP, es la elección incorrecta: una función puede estar en la pila varias veces (directo o recursión indirecta). Claramente necesitamos un "multi-set" (también conocido como `` bolsa ''), una estructura en forma de conjunto que realiza un seguimiento de `` cuántas veces '' Cada artículo está presente. En Python, la implementación natural de un multiconjunto es como un dict mapeo de claves para conteos, que a su vez se implementa de manera más práctica como collections.defaultdict (int) .

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

En quinto lugar, 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), de lo contrario, los saltos de control). Esto proviene de 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 con una elección diferente de clave: hacer que el decorador tome un argumento (cadena, digamos) identificador que debe ser único (en cada hilo dado) y usar el identificador como Clave en el dictado de monitoreo. Por supuesto, el código que comprueba la pila debe conocer el identificador y usarlo también.

En el momento de decorar, el decorador puede verificar la propiedad de unicidad (usando 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 se consideran `` iguales '' para fines de monitoreo (este puede ser el caso si una determinada instrucción def debe ejecutarse varias veces en contextos ligeramente diferentes para crear varios objetos de función que los programadores quieran considerar "la misma función" fines de seguimiento). Finalmente, debería ser posible revertir 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 práctica de garantizar la singularidad).

Entonces, juntando estas muchas consideraciones, podríamos tener (incluyendo una función de utilidad threadlocal_var que probablemente ya estará en un módulo de 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, creo que en la línea de " si vale la pena hacerlo, entonces vale la pena hacerlo correctamente " ;-).

Otros consejos

Realmente no me gusta este enfoque, pero aquí hay una versión arreglada 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, subprocesos y excepciones.

No me gusta este enfoque por dos razones:

  • No funciona si la función se decora aún más: este debe ser el decorador final.
  • Si está usando esto para depurar, significa que debe 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 pila, por lo que solo tiene que editar el código en el código que está depurando.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top