maneira eficiente para determinar se uma determinada função é na pilha em Python

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

  •  05-07-2019
  •  | 
  •  

Pergunta

Para depuração, muitas vezes é útil para dizer se uma determinada função é mais acima na pilha de chamadas. Por exemplo, muitas vezes só querem executar depuração de código quando uma determinada função nos chamou.

Uma solução é examinar todas as entradas de pilha mais acima, mas isso é em uma função que é profundo na pilha e repetidamente chamado, isso leva a sobrecarga excessiva. A questão é encontrar um método que nos permite determinar se uma determinada função é mais acima na pilha de chamadas de uma forma que é razoavelmente eficiente.

Semelhante

Foi útil?

Solução

A menos que a função que você está buscando faz algo muito especial para marcar "uma instância de mim está ativo na pilha" (IOW: se a função é intocada e intocável e não pode, eventualmente, estar cientes desta necessidade peculiar de seu), não há nenhuma alternativa concebível para uma caminhada quadro a quadro a pilha até que você bateu tanto o topo (e a função não está lá) ou um quadro de pilha para sua função de interesse. Como vários comentários à pergunta indicam, é extremamente duvidoso que vale a pena se esforçando para otimizar isso. Mas, supondo que por causa do argumento de que foi vale a pena ...:

Editar :. A resposta original (pelo OP) tinha muitos defeitos, mas alguns já foram fixo, então eu estou editando para refletir a situação atual e por que certos aspectos são importantes

Em primeiro lugar, é crucial para o uso try / except, ou with, no decorador, para que qualquer saída de um ser função monitorado é devidamente contabilizados, e não apenas os normais (como a versão original de resposta do próprio OP fez).

Em segundo lugar, cada decorador deve garantir que ele mantém __name__ e __doc__ da função decorado intacta -. Isso é o que functools.wraps é para (há outras maneiras, mas wraps torna mais simples)

Em terceiro lugar, tão crucial como o primeiro ponto, um set, que era a estrutura de dados originalmente escolhido pelo OP, é a escolha errada: a função pode ser na pilha várias vezes (diretos ou recursão indireta). Precisamos claramente de um "multi-set" (também conhecido como "saco"), uma estrutura de set-like que mantém o controle de "quantas vezes" cada item está presente. Em Python, a implementação natural de um multiset é como um mapeamento dict chaves para contagens, que por sua vez é mais convenientemente implementado como um collections.defaultdict(int).

Em quarto lugar, uma abordagem geral deve ser threadsafe (quando isso pode ser feito facilmente, pelo menos ;-). Felizmente, threading.local torna trivial, quando aplicável -. E aqui, ele deve certamente ser (cada pilha que tem seu próprio segmento separado de chamadas)

Em quinto lugar, uma questão interessante que tem sido abordado em alguns comentários (percebendo o quão mal os decoradores oferecidos em algumas respostas jogar com outros decoradores: o decorador monitoramento parece ter para ser o último (mais externa) um, caso contrário as quebras de verificação. Isto vem desde a escolha natural, mas infeliz de usar o próprio objeto de função como a chave para o dict monitoramento.

proponho para resolver este por uma escolha diferente de chave: fazer o decorador ter um argumento (string, por exemplo) identifier que deve ser exclusivo (em cada determinado segmento) e usar o identificador como a chave na dict monitoramento. O código de verificação do mosto pilha de curso estar ciente do identificador e usá-lo bem.

Ao decorar tempo, o decorador pode verificar a propriedade de exclusividade (usando um conjunto separado). O identificador pode ser deixado para o padrão para o nome da função (por isso só é exigido explicitamente para manter a flexibilidade de monitorar funções homónimos no mesmo namespace); a propriedade de exclusividade pode ser explicitamente renunciaram quando várias funções monitoradas estão a ser considerado "o mesmo" para efeitos de controlo (pode ser o caso se uma determinada declaração def é feito para ser executado várias vezes em contextos ligeiramente diferentes para fazer vários objetos de função que os programadores quer considerar "a mesma função" para efeitos de controlo). Finalmente, deve ser possível reverter opcionalmente com o "objeto de função como identificador" para aqueles casos raros em que ainda decoração é conhecido por ser impossível (uma vez que nesses casos, pode ser o caminho mais acessível para singularidade garantia).

Então, colocar essas muitas considerações juntos, poderíamos ter (incluindo uma função de utilidade threadlocal_var que provavelmente já estar em uma tmódulo oolbox é claro ;-) algo como o seguinte ...:

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

Eu não testei este código, para que ele possa conter algum erro de digitação ou similar, mas eu estou oferecendo isso porque eu espero que não cobre todos os pontos técnicos importantes expliquei acima.

É tudo vale a pena? Provavelmente não, como anteriormente explicado. No entanto, acho que ao longo das linhas de "se vale a pena fazer em tudo, então vale a pena fazer direito"; -).

Outras dicas

Eu realmente não gosto dessa abordagem, mas aqui está uma versão fixa-se de que você estava fazendo:

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

Esta alças recursão, threading e exceções.

Eu não gosto dessa abordagem por duas razões:

  • Não funciona se a função está decorado ainda:. Este deve ser o decorador final
  • Se você estiver usando este para depuração, isso significa que você tem que editar o código em dois lugares para usá-lo; um para adicionar o decorador, e um para usá-lo. É muito mais conveniente para apenas examinar a pilha, assim você só tem que editar o código no código que você está depuração.

Uma abordagem melhor seria examinar a pilha diretamente (possivelmente como uma extensão nativa para a velocidade) e, se possível, encontrar uma maneira de armazenar em cache os resultados para a vida útil do quadro de pilha. (Eu não tenho certeza se isso é possível sem modificar o núcleo Python, no entanto.)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top