Rastreo paso a paso de la evaluación de expresiones de Python

Estoy tratando de escribir el visualizador de evaluación de expresiones de Python, que mostrará cómo se evalúan las expresiones de Python paso a paso (con fines educativos). Philip Guo Python Tutor es excelente, pero evalúa el progtwig Python línea por línea, y descubrí que a veces los estudiantes no entienden cómo se sorted([4, 2, 3, 1] + [5, 6])[1] == 2 expresiones de una línea sorted([4, 2, 3, 1] + [5, 6])[1] == 2 son evaluados y me gustaría visualizar este proceso. (Parece que nadie lo hizo todavía, al menos no encontré nada). La solución ideal creará una secuencia de cadenas como esta:

 sorted([4, 2, 3, 1] + [5, 6])[1] == 2 sorted( >> [4, 2, 3, 1] + [5, 6] <> sorted([4, 2, 3, 1, 5, 6]) <> [1 2 3 4 5 6][1] <> 2 == 2 << True 

Aquí >> y << se usan para resaltar una parte de la expresión que se evalúa en el paso actual y luego se reemplaza por su valor. (Tal vez, intentaré convertir esta secuencia a algún tipo de animación más adelante).

Mi estrategia actual es usar ast.parse() para analizar la cadena en AST, luego encontrar un nodo que se evaluará primero, evaluarlo con eval(compile(node, '', 'eval')) (Definitivamente estoy no desea volver a implementar todo Python :)), convierta el resultado de la evaluación en un nodo AST (con repr y luego ast.parse() ?) y sustituya el nodo actual por el nodo del resultado, luego use codegen.to_source para producir el código modificado cadena desde (modificado) AST y continúe con el mismo proceso hasta que solo tenga un literal en el árbol.

Mi pregunta es: ¿cómo puedo encontrar un nodo que se evaluará primero? Parece que puedo atravesar la profundidad del árbol primero con subclasificación de ast.NodeVisitor , pero no estoy seguro de cómo puedo detectar que alcancé el nodo deseado y cómo puedo dejar de atravesarlo después.


EDITAR.

Es posible que mi enfoque inicial con la transformación del árbol no sea factible. De hecho, un paso elemental de evaluación de la expresión de Python no es necesariamente un reemplazo de una subexpresión por una más simple (como en la aritmética). Por ejemplo, las comprensiones de listas proporcionan un comportamiento mucho más complicado que no se puede express en términos que sustituyan esta cosa por esa cosa, luego se repiten recursivamente . Así que reitero un poco la pregunta. Necesito alguna forma de mostrar de manera programática cómo se evalúan las expresiones de Python paso a paso. Por ejemplo, la función de rastreo de MacroPy, mencionada por @jasonharper, es una solución aceptable en esta etapa. Desafortunadamente, MacroPy parece estar abandonado y no funciona con Python 3. ¿Hay alguna idea de cómo parecerse a este comportamiento de rastreo en Python 3 sin tener que cargar MacroPy completo?


EDIT2.

Justo después de otorgar esta recompensa, encontré una pregunta similar y un depurador con características muy cercanas. Sin embargo, como no hay una respuesta definitiva para esa pregunta, y no necesito el depurador completo, sigo buscando una respuesta que se pueda usar, por ejemplo, en el entorno de Jupyter.

El paso a paso de la expresión se implementa en Thonny IDE .

Utiliza la instrumentación AST, donde cada (sub) expresión e se transforma en after(before(), e) . Las funciones before y after son funciones ficticias para provocar eventos de llamada adicionales en el sistema de rastreo de Python. Estas llamadas adicionales notifican cuando la evaluación de (sub) expresión está por comenzar o acaba de finalizar. (Se agregan funciones ficticias similares para detectar el inicio y el final de cada statement).

La instrumentación e interpretación AST de estos nuevos eventos se realiza en thonny.backend.FancyTracer .

Los nodos AST de Python contienen la posición inicial de los rangos de texto correspondientes, pero a veces son incorrectos. Las posiciones finales están completamente perdidas. thonny.ast_utils.mark_text_ranges intenta hacerse cargo de esto (pero la solución está incompleta en este momento).

Sería bueno si alguien extrajera la funcionalidad relevante de Thonny a un paquete más general. Tal vez incluso dos paquetes: uno para calcular la información de ubicación de Python AST y otro para el seguimiento detallado del código de Python. Estaría dispuesto a ayudar con esto, si alguien tomara la iniciativa.

¿Por qué no usar el módulo dis ?

Dado que CPython comstack Python a bytecode y ejecuta eso, mirar el bytecode te da la mejor idea de lo que realmente sucede.

 In [1]: import dis In [2]: dis.dis('sorted([4, 2, 3, 1] + [5, 6])[1] == 2') 1 0 LOAD_NAME 0 (sorted) 3 LOAD_CONST 0 (4) 6 LOAD_CONST 1 (2) 9 LOAD_CONST 2 (3) 12 LOAD_CONST 3 (1) 15 BUILD_LIST 4 18 LOAD_CONST 4 (5) 21 LOAD_CONST 5 (6) 24 BUILD_LIST 2 27 BINARY_ADD 28 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 31 LOAD_CONST 3 (1) 34 BINARY_SUBSCR 35 LOAD_CONST 1 (2) 38 COMPARE_OP 2 (==) 41 RETURN_VALUE 

Edición : un método alternativo podría ser mostrar los pasos uno por uno en IPython:

 In [1]: [4, 2, 3, 1] Out[1]: [4, 2, 3, 1] In [2]: [4, 2, 3, 1] + [5, 6] Out[2]: [4, 2, 3, 1, 5, 6] In [3]: sorted([4, 2, 3, 1, 5, 6]) Out[3]: [1, 2, 3, 4, 5, 6] In [4]: [1, 2, 3, 4, 5, 6][1] Out[4]: 2 In [5]: 2 == 2 Out[5]: True 

La adición de las dos listas ciertamente no es el primer nodo evaluado en ese código; Creo que de hecho hay nueve evaluaciones de nodos anteriores – sorted , 4 , 2 , 3 , 1 , [4,2,3,1] , 5 , 6 , [5,6] . No solo tendría que determinar en qué orden se realizan las evaluaciones, sino que también debería decidir cuál de esas evaluaciones vale la pena mostrar.

Creo que un mejor enfoque para su problema sería modificar los nodos AST para que emitan su estado anterior / posterior como un efecto secundario de ser ejecutados. No te importaría su orden, solo ejecutarías la expresión completa una vez. Y ya existe un paquete llamado macropy que tiene una función de seguimiento que hace exactamente esto. Su salida no es exactamente lo que está pidiendo, pero probablemente podría modificarse para que sea más similar.