Una tarifa que cambia cada semana
Una empresa de repartos deja que su responsable escriba la tarifa en una página de ajustes: fee if km > 5 else 0. La regla cambia cada semana.
Nadie quiere compilar y reiniciar el servicio por cada cambio. El servicio guarda el árbol sintáctico de la regla y lo calcula, nodo a nodo, con cada pedido.
Al final de esta lección
Nota
Puedo ejecutar directamente un árbol sintáctico.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Intérprete | un programa que ejecuta otro sin crear código máquina | |
| Nodo | un punto del árbol | BinOp |
| Evaluar | calcular el valor de un nodo | |
| Hoja | un nodo sin nada debajo | Constant(value=3) |
| Entorno | los nombres al alcance y sus valores | {'base': 20} |
| Ligadura | un nombre unido a su valor |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Python te enseña su árbol
Python también es un compilador. Antes de ejecutar una línea, la analiza y construye su árbol sintáctico. El módulo ast imprime ese árbol para una línea que le llega por una tubería. --mode eval dice que la línea es una sola expresión.
Cada nodo tiene un nombre. Una hoja no tiene nada debajo. Compara los dos árboles.
El árbol de 10 - 4 - 3
echo "10 - 4 - 3" | python3 -m ast --mode evalSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un árbol con nombres y paréntesis
echo "base * (1 + rate)" | python3 -m ast --mode evalSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejemplo: los nodos que imprimió Python
| Nodo | Qué es | De dónde sale su valor |
|---|---|---|
| Constant(value=10) | Una hoja con un número | Del número mismo |
| Name(id='base') | Una hoja con un nombre | De buscar el nombre |
| BinOp(left, op, right) | Una operación con dos lados | De los dos lados y luego la operación |
| Sub(), Mult() o Add() | Qué operación es | De nada: es una etiqueta |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Primero los hijos, después el nodo
Un intérprete que recorre el árbol es una sola función que evalúa un nodo. Una hoja con un número da ese número. Una operación evalúa primero su hijo izquierdo y su hijo derecho, y después hace su cuenta con los dos resultados.
Como un hijo puede ser un árbol entero, la función se llama a sí misma. La demo sabe restar y multiplicar.
Un evaluador que recorre el árbol
# Evalúa un árbol: un número o [op, izquierda, derecha].
def evaluate(tree):
if type(tree) == int:
return tree
# Primero los dos hijos: aquí está la recursión.
left = evaluate(tree[1])
right = evaluate(tree[2])
if tree[0] == "-":
return left - right
if tree[0] == "*":
return left * right
return None
tree = ["-", ["-", 10, 4], 3]
print(evaluate(tree))
# Cambia tree por ["-", 10, ["-", 4, 3]] y vuelve a ejecutar.
Un error frecuente
Advertencia
Mucha gente cree que un intérprete convierte el árbol en código máquina a escondidas y ejecuta eso. Un intérprete que recorre el árbol no lo hace nunca. Visita los nodos y hace el trabajo sobre la marcha, otra vez en cada ejecución. Por eso es más lento que el código compilado, y por eso puede empezar al instante.
Los nombres se buscan en un entorno
Una hoja con un nombre necesita sacar su valor de algún sitio. El intérprete lleva un entorno: cada nombre al alcance con su valor. Cada pareja es una ligadura.
eval de Python recibe el entorno como un diccionario. Si le das un segundo diccionario, busca primero en ese. Así un ámbito interior tapa un nombre exterior.
Evalúa con un entorno
python3 -c "print(eval('base + tax', {'base': 20, 'tax': 4}))"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Deja fuera un nombre
python3 -c "print(eval('base + tax', {'base': 20}))"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Un entorno interior que se mira antes
python3 -c "print(eval('base + tax', {'base': 20, 'tax': 4}, {'tax': 0}))"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejemplo: buscar base + tax
| Run | base sale de | tax sale de | Imprime |
|---|---|---|---|
| Primero | El único diccionario: 20 | El único diccionario: 4 | 24 |
| Segundo | El único diccionario: 20 | De ninguna parte | NameError |
| Tercero | El exterior: 20 | El interior: 0 | 20 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una condición evalúa una sola rama
La tarifa del principio tiene una condición. Su nodo, IfExp, tiene tres hijos: la prueba, el valor si se cumple y el valor si no.
Aquí el orden cambia. El intérprete evalúa la prueba y después solo una de las dos ramas. El último Run pone en la rama que no toca un nombre que no existe.
El árbol de la tarifa
echo "fee if km > 5 else 0" | python3 -m ast --mode evalSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
La tarifa para 2 km
python3 -c "print(eval('fee if km > 5 else 0', {'fee': 3, 'km': 2}))"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Una rama que nunca se evalúa
python3 -c "print(eval('fee if km > 5 else missing', {'fee': 3, 'km': 9}))"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Para ir más lejos
Añade a tu evaluador un nodo ["let", name, value, body]. Calcula value con los entornos de siempre. Después evalúa body con un entorno nuevo encima, donde name vale ese resultado.
La lista envs del que llama no puede cambiar. Escribe una prueba que lo demuestre, y otra en la que let tape un nombre exterior.