Artigo
· Jun. 4 9min de leitura

Um mergulho no Debugging de Embedded Python

1. Um Exemplo Motivador

O Python embarcado já existe há algum tempo. Você provavelmente seguiu os tutoriais e aprendeu sobre ele. No entanto, se você tentou combinar Python e ObjectScript em um trabalho de desenvolvimento real, provavelmente se deparou com situações em que recebeu uma mensagem de erro como esta:

USER>Do ##class(MyClass).SomeMethod()

ERROR #5002: ObjectScript error: <PYTHON EXCEPTION> *<class 'ZeroDivisionError'>: division by zero

É uma string longa para informações limitadas. Tudo o que sabemos por essa mensagem é que ocorreu um erro de Python onde:

(a) o tipo de erro é ZeroDivisionError; e (b) a mensagem de erro é "division by zero" (divisão por zero).

Agora, essa informação é suficiente para nós, desenvolvedores?

Bem, um desenvolvedor experiente procurará por todas as ocorrências da operação de divisão em seu código Python. Se houver uma quantidade limitada de Python na base de código ou se houver poucas operações de divisão envolvidas, é definitivamente depurável. Mas, e se estivermos lidando com uma base de código com muito Python e não estivermos suficientemente familiarizados com ela, e não pudermos restringir a busca com base no tipo de erro e na mensagem de erro? Isso seria um pesadelo!

Neste artigo, vamos nos aprofundar em como depurar erros ocultos na pilha de chamadas do Python embarcado.

 

2.Algumas Maneiras Que Não Funcionam

Você pode ficar tentado a tentar várias técnicas de depuração. Mas antes de começarmos a "hackear", aqui estão algumas maneiras que sabidamente não funcionam.

2.1 Utilizando o sys.excepthook do Python

Se você está familiarizado com Python, deve saber que ele chama sys.excepthook() em todo erro não tratado (ou seja, não capturado por um bloco try). O comportamento padrão é imprimir o rastreamento da pilha de erros em um formato legível por humanos. Podemos definir sys.excepthook para uma função de hook personalizada que registra o erro?

Decepcionantemente, sys.excepthook() nunca é executado. Pode parecer estranho, mas na verdade é o comportamento esperado. A lógica é que cada erro deve ser tratado apenas uma vez. Se ele fosse tratado no nível Python pelo sys.excepthook, não sobraria nada para o ObjectScript tratar! Como fizemos uma chamada ObjectScript — Do ##class(MyClass).SomeMethod() — faz sentido que o erro seja encapsulado como um erro ObjectScript para ser tratado por nós. É também por isso que, se você obtiver um erro no shell interativo do Python, $system.Python.Shell(), o erro é tratado no nível Python por sys.excepthook.

2.2 Acessando%objlasterror

Você também pode ser tentado a Zwrite %objlasterror e esperar que haja informações úteis codificadas nessa string longa e complicada. Infelizmente, você verá que a string contém apenas a pilha de chamadas do ObjectScript, mas não a do Python. As únicas informações relevantes para o erro Python são o tipo de erro e a mensagem de erro. Isso é o mesmo que a mensagem de erro 5002 que já estamos vendo.

2.3 Acessando%Exception.PythonException:ExceptionObject

Outra ideia é capturar a instância da exceção Python no ObjectScript e então examinar os detalhes (principalmente o traceback) do erro Python lá.

Try { 
  Do ##class(MyClass).SomeError()
} Catch ex { 
  ... 
}

Se você usar Zwrite ex, verá que ele tem o tipo %Exception.PythonException e possui uma propriedade ExceptionObject que é um objeto %SYS.Python! Se esta for a instância da exceção Python, nosso problema está resolvido. No entanto, este ExceptionObject é, na verdade, a classe da exceção, e não a instância da exceção em si. Em outras palavras, ele retorna a classe ZeroDivisionError em vez de uma instância de ZeroDivisionError.

Há uma enorme diferença entre a classe de exceção e a instância de exceção. Com a instância de exceção, podemos acessar a classe da exceção (via instance."__class__") e o rastreamento da pilha de chamadas (via instance."__traceback__") no nível do ObjectScript. No entanto, com a classe de exceção, geralmente a única informação disponível é a categoria do erro. Mais uma vez, isso já é impresso na mensagem de erro #5002 acima, então %Exception.PythonException:ExceptionObject não oferece muita informação.

Existem discussões internas para incluir a instância da exceção Python no lugar ou em adição à classe da exceção para %Exception.PythonException. Se isso for implementado, a depuração se tornará muito mais fácil, e eu atualizarei este artigo o mais rápido possível.

 

3. Três Abordagens para Depuração

Neste artigo, vamos discutir 3 abordagens para depurar Python embutido. Elas são aplicáveis à depuração de qualquer outra linguagem:

  1. Examine a mensagem de erro e o rastreamento da pilha de chamadas que levou à exceção não tratada.
  2. Adicione um breakpoint onde paramos a execução do programa e examinamos interativamente o estado do programa e avançamos passo a passo pelo código.
  3. Após a ocorrência de um erro, armazene o estado do programa que falhou e realize uma análise post-mortem. Esta é uma combinação das duas abordagens anteriores.

Devido ao problema com %Exception.PythonException:ExceptionObject, discutido na seção anterior, as abordagens 1 e 3 devem ocorrer no nível Python. Assim que saímos do ambiente Python, perdemos o acesso imediato ao traceback.

Se a instância da exceção, também discutida anteriormente, fosse preservada, as coisas seriam completamente diferentes – poderíamos examinar a mensagem de erro no nível ObjectScript e realizar análises post-mortem nesse mesmo nível.

3.1 Imprimir o callback

Então, é assim que funciona: envolvemos o método Python de interesse (vamos chamá-lo de ##class(User.MyClass).SomeError()) com um bloco try em Python. Note, novamente, que SomeError() deve ser um método [Language=python]. Quando o erro ocorrer em algum lugar na pilha de chamadas, capturamos o erro e realizamos alguma "mágica".

ClassMethod SomeError() [ Language = python ]
{
    // some code that may call into other functions and result in errors
}

ClassMethod Wrapper() [ Language = python ]
{
    import iris
    from my_magic import print_iris_exc

    try:
        return iris.cls("User.MyClass").SomeError()
    except:
        print_iris_exc()
}

Não vou entrar nos detalhes de my_magic.print_iris_exc(), mas anexei o conteúdo de my_magic aqui para os interessados. A ideia é que, quando ocorre um erro em uma thread Python, uma instância de exceção será registrada no estado da thread. A exceção pode ser acessada com sys.exc_info(), que é uma tupla de 3 elementos contendo, respectivamente, a classe da exceção, a instância da exceção e o objeto traceback. O traceback é (o nó principal de) uma lista encadeada, contendo uma referência a cada frame da pilha de chamadas e seu objeto de código associado. A função print_iris_exc essencialmente itera pela lista encadeada e coleta informações de frame/código para impressão. Voilà!

USER>do ##class(User.MyClass).Wrapper()
Embedded Python Traceback (most recent call last):
  ObjectScript Class "User.MyClass", Method "Wrapper", Line 5
    return iris.cls("User.MyClass").SomeError()
  ObjectScript Class "User.MyClass", Method "SomeError", Line 3
    return iris.cls("User.MyClass").DividedByZero()
  ObjectScript Class "User.MyClass", Method "DividedByZero", Line 3
    return x / y
ZeroDivisionError: division by zero

ERROR #5002: ObjectScript error: <PYTHON EXCEPTION> *<class 'ZeroDivisionError'>: division by zero

 

3.2 Configurando um Breakpoint para Depuração Interativa

Se temos uma ideia de onde as coisas provavelmente darão errado, podemos usar o pdb, o depurador do Python, para definir um breakpoint no código e examinar interativamente os estados das variáveis de cada stack frame e percorrer a execução. Isso pode ser feito adicionando uma chamada breakpoint() no arquivo Python ou executando manualmente import pdb; pdb.set_trace(). Isso é funcionalmente equivalente à instrução Break do ObjectScript sem argumentos.

ClassMethod DividedByZero() [ Language = python ]
{
    x = 1
    y = 0
    breakpoint()  # or, equivalently, import pdb and call pdb.set_trace()
    return x / y
}

A execução do código será interrompida no breakpoint, e uma interface de usuário do pdb será exibida. Nela, você pode escolher percorrer o código, avaliar expressões, inspecionar variáveis, modificá-las ou até mesmo pular para outra linha.

Existe uma incompatibilidade conhecida entre o pdb e o Python embutido. Por exemplo, o pdb não consegue recuperar o código-fonte de métodos [ Language = python ]. Isso ocorre porque o IRIS armazena o código Python embutido no global ^oddDEF. Isso pode ser corrigido criando uma subclasse de pdb.PDB e sobrescrevendo os métodos correspondentes. Consulte o código aqui para saber como acessar o código-fonte do Python embutido.

Além disso, com o próximo lançamento do Python 3.14 no ano que vem, haverá uma opção para passar uma lista de comandos pré-codificados para o breakpoint. Isso economiza muito trabalho repetitivo (como importar o módulo iris) envolvido na configuração de um ambiente de depuração. Por exemplo, quando o Python 3.14 for lançado e configurado para uso com o IRIS, os breakpoints do Python poderão ser adicionados assim:

breakpoint(commands=[
    'exec("import iris")',
    'p iris.gref(MyGlobal)[None]',
])

 

3.3 Análise Post-Mortem

Capturar o erro e imprimir o callback não exige que você saiba onde o erro acontece — ele pode estar em qualquer lugar na pilha de chamadas. A desvantagem é que precisamos predefinir uma série de lógicas para imprimir as informações do erro.

Definir um breakpoint nos permite inspecionar o erro dinamicamente e realizar quaisquer etapas de depuração ad-hoc. No entanto, isso exige uma estimativa razoável de onde as coisas podem dar errado, já que o breakpoint() precisa ser "hardcoded" antes que o código seja executado.

A abordagem que vamos discutir nesta seção, a análise post-mortem, combina as vantagens das duas anteriores. Você não precisa ter uma ideia de onde o erro ocorre e pode decidir dinamicamente quais variáveis ou stack frames inspecionar se e quando um erro acontecer.

ClassMethod PostMortem() [ Language = python ]
{
    import iris
    import pdb

    try:
        return iris.cls("User.MyClass").SomeError()
    except:
        pdb.post_mortem()
}

O código prossegue normalmente se nenhum erro ocorrer. Caso contrário, uma sessão interativa do pdb será iniciada para depuração post-mortem.

USER>do ##class(User.MyClass).PostMortem()
> /opt/EmbPy/mgr/user/MyClass(27)DividedByZero()
(Pdb) where
  /opt/EmbPy/mgr/user/MyClass(7)PostMortem()
  /opt/EmbPy/mgr/user/MyClass(21)SomeError()
> /opt/EmbPy/mgr/user/MyClass(27)DividedByZero()
(Pdb) p locals().keys()
dict_keys(['x', 'y'])
(Pdb) whatis x
<class 'int'>
(Pdb) whatis y
<class 'int'>
(Pdb) pp x, y
(1, 0)

4. Reflexões Aleatórias Sobre o Futuro

A dificuldade em depurar Python embarcado reside na perda de informações ao passar erros através da fronteira ObjectScript-Python. Veja uma ilustração de como um erro é retornado em uma pilha de chamadas com frames mistos de Python e ObjectScript. Nesse cenário, começamos a execução a partir do frame COS inferior e encontramos um erro no frame Python. A exceção é então propagada de volta, de cima para baixo. Ao passar a exceção entre ObjectScript e Python, algumas informações são retidas, enquanto o restante é perdido.

  • Quando um frame ObjectScript chama um frame Python, e um erro Python é levantado, ele é encapsulado como um %Exception.PythonException, e o rastreamento de pilha (traceback) Python é perdido, conforme discutido na Seção 2.3.
  • Quando um frame Python chama um frame ObjectScript, e um erro ObjectScript é lançado, ele é encapsulado como um Python RuntimeError, e o rastreamento de pilha (traceback) ObjectScript também é perdido.

Se conseguirmos anexar a instância original da exceção Python no %Exception.PythonException no nível do ObjectScript, e fizermos o mesmo no sentido inverso, toda a informação do rastreamento de pilha pode ser retida.

Ou seja,

Class %Exception.PythonException Extends %Exception.AbstractException [ Final ]
{

/// This contains the Python exception object
Property PyExc AS %SYS.Python;

// other properties and methods ...

}

e

// This may need to be defined as a PyTypeObject at C level and preloaded
class IRISException(Exception):
    def __init__(self, msg, irisexc):
        super().__init__(msg)
        self.irisexc = irisexc // the iris exception object

Se essa funcionalidade for implementada, poderíamos acessar a pilha de chamadas unificada inspecionando recursivamente %Exception.PythonException:PyExc e IRISException:irisexc. Ambos conteriam segmentos das informações do rastreamento de pilha. Além disso, também poderíamos realizar a análise post-mortem do pdb acessando o nível mais profundo da pilha de chamadas usando uma sintaxe como e.PyExc.irisexc.PyExc.irisexc.PyExc."__traceback__".


 

Discussão (0)1
Entre ou crie uma conta para continuar