maneira eficiente para determinar se uma determinada função é na pilha em Python
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
- a obtenção de referências a objetos de função na pilha de execução do objeto quadro -? Esta questão centra-se na obtenção dos objetos de função, em vez de determinar se estamos em uma função particular. Embora as mesmas técnicas podem ser aplicadas, eles podem acabar por ser extremamente ineficiente.
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.)