Cesar Systems

Herramientas Informaticas

20.2. Recorrer árboles

Siempre que usted vea una nueva estructura de datos, su primera pregunta
debería ser: “¿Como la recorro?” La forma mas natural de recorrer un árbol
es recursivamente. Por ejemplo, si el árbol contiene enteros como carga, esta
función nos devuelve su suma:

   1: def total(arbol):

   2: if arbol == None: return 0

   3: return total(arbol.izquierda) + total(arbol.derecha) +

   4: arbol.carga

El caso base es el árbol vacío, que no tiene carga, así que la suma es 0. El
paso recursivo hace dos llamadas recursivas para hallar la suma de los arboles
hijos. Cuando terminan las llamadas recursivas, sumamos la carga del padre y
devolvemos el total.

20.3. Árboles de expresión

Un árbol es un forma natural de representar la estructura de una expresión.
Al contrario que otras notaciones, puede representar el calculo de forma no
ambigua. Por ejemplo, la expresión infija 1 + 2 * 3 es ambigua a no ser que
sepamos que la multiplicación se realiza antes que la suma.

Este árbol de expresión representa el mismo calculo:

Sin título

 

Los nodos de un árbol de expresión pueden ser operandos como 1 y 2 u operado-
res como + y *. Los operandos son nodos hojas; los nodos operadores contienen
referencias a sus operandos. (Todos estos operadores son binarios, lo que sig-
nifica que tienen exactamente dos operandos.)Así podemos construir este árbol:

1: >>> arbol = Arbol('+', Arbol(1), Arbol('*', Arbol(2), Arbol(3)))

Mirando la figura, no hay duda del orden de las operaciones; la multiplicación
se realiza antes para calcular el segundo operando de la suma.

Los arboles de expresión tienen muchos usos. El ejemplo de este capítulo usa
arboles para traducir expresiones a postfijo, prefijo e infijo. Arboles similares se
usan dentro de los compiladores para analizar, optimizar y traducir programas.

20.4. Recorrido de un árbol

Podemos recorrer un árbol de expresión e imprimir el contenido así:

   1: def imprimeArbol(arbol):

   2:     if arbol == None: return

   3:         print arbol.carga,

   4:         imprimeArbol(arbol.izquierda)

   5:         imprimeArbol(arbol.derecha)

En otras palabras, para imprimir un árbol imprima primero el contenido de la raíz, luego el subárbol izquierdo entero y después el subárbol derecho entero.

Esta forma de recorrer un árbol se llama orden prefijo, porque el contenido de la raíz aparece antes del contenido de los hijos.

La salida del ejemplo anterior es:

   1: >>> arbol = Arbol('+', Arbol(1), Arbol('*', Arbol(2), Arbol(3)))

   2: >>> imprimeArbol(arbol)

   3: + 1 * 2 3

Este formato es diferente tanto del postfijo como del infijo; es otra notación llamada prefija, en la que los operadores aparecen delante de sus operandos.

Puede sospechar que si recorre el árbol en un orden diferente obtendrá la expresión en una notación diferente. Por ejemplo, si imprime primero los subárboles
y luego la raíz, tendrá:

   1: def imprimeArbolPostfijo(arbol):

   2:     if arbol == None: return

   3:         imprimeArbolPostfijo(arbol.izquierda)

   4:         imprimeArbolPostfijo(arbol.derecha)

   5:         print arbol.carga

¡El resultado, 1 2 3 * +, esta en notación posfija! Este orden de recorrido se llama orden postfijo.

Finalmente, para recorrer un árbol en orden infijo, usted imprime el árbol izquierdo, luego la raíz y después el árbol  derecho:

   1: def imprimeArbolInfijo(arbol):

   2:     if arbol == None: return

   3:         imprimeArbolInfijo(arbol.izquierda)

   4:         print arbol.carga,

   5:         imprimeArbolInfijo(arbol.derecha)

 

El resultado es 1 + 2 * 3, que es la expresión en notación infija.

Para ser justos debemos señalar que hemos omitido una importante complicación. A veces, al escribir una expresión infija, debemos usar paréntesis para preservar el orden de las operaciones. De modo que una exploración de orden infijo no es suficiente para generar una expresión infija.

No obstante, con unas pequeñas mejoras, el árbol de expresión y los tres recorridos nos dan una forma general de traducir expresiones de un formato a otro.

Como ejercicio, modifique imprimeArbolInfijo de modo que pon-
ga entre paréntesis cada operador con sus operandos. >La salida es
correcta y sin ambigüedades? >Se necesitan siempre los paréntesis?

 

Si hacemos un recorrido en orden infijo y tomamos nota de en que nivel del árbol estamos, podemos generar una representación grafica de un árbol:

   1: def imprimeArbolSangrado(arbol, nivel=0):

   2:     if arbol == None: return

   3:         imprimeArbolSangrado(arbol.derecha, nivel+1)

   4:         print ' '*nivel + str(arbol.carga)

   5:         imprimeArbolSangrado(arbol.izquierda, nivel+1)

 

El parámetro nivel lleva la cuenta de donde estamos en el árbol. Por omisión,
inicialmente es 0. Cada vez que hacemos una llamada recursiva, pasamos nivel+1
porque el nivel del hijo es siempre uno mayor que el del padre. Sangramos cada
elemento con dos espacios por nivel. El resultado del árbol de ejemplo es:

   1: >>> imprimeArbolSangrado(arbol)

   2:     3

   3:    *

   4:   2

   5:  +

   6:   1

 

Si mira la salida de lado, vera una versión simplificada de la figura original.

20.5. Construir un árbol de expresión

En esta sección analizamos expresiones infijas y formamos los correspondientes arboles de expresión. Por ejemplo, la expresión (3+7)*9 nos da el siguiente árbol:

Sin título 

Fíjese en que hemos simplificando el diagrama omitiendo los nombres de los atributos.

El analizador que vamos a escribir maneja expresiones que incluyen números, paréntesis y los operadores + y *.

Suponemos que la cadena de entrada ya ha sido tokenizada y almacenada en una lista de Python. La lista de tokens d (3+7)*9 es:

[‘(‘, 3, ‘+’, 7, ‘)’, ‘*’, 9, ‘fin’]

El token fin es útil para evitar que el analizador lea mas allá del final de la lista.

A modo de ejercicio, escriba una función que tome una cadena conteniendo una expresión y devuelva una lista de tokens.

La primera función que vamos a escribir es tomaToken, que toma como parámetros una lista de tokens y el token que esperamos. Compara el token esperado con el primer token de la lista: si coinciden, elimina el token de la lista y devuelve
verdadero, en caso contrario, devuelve falso:

   1: def tomaToken(listaToken, esperado):

   2:     if listaToken[0] == esperado:

   3:         del listaToken[0]

   4:         return 1

   5:     else:

   6:         return 0

Como listaToken apunta a un objeto mutable, los cambios hechos aquí son visibles para cualquier otra variable que apunte al mismo objeto.

La siguiente función, obtieneNumero, maneja operandos. Si el siguiente token de listaToken es un numero, obtieneNumero lo elimina y devuelve un nodo hoja que contiene el numero; en caso contrario, devuelve None.

   1: def obtieneNumero(listaToken):

   2:     x = listaToken[0]

   3:     if type(x) != type(0): return None

   4:         del listaToken[0]

   5:     return Arbol (x, None, None)

Antes de seguir, debemos probar obtieneNumero aisladamente. Asignamos una lista de números a listaToken, extraemos el primero, imprimimos el resultado e imprimimos lo que quede de la lista de tokens:

   1: >>> listaToken = [9, 11, 'fin']

   2: >>> x = obtieneNumero(listaToken)

   3: >>> imprimeArbolPostfijo(x)

   4: 9

   5: >>> print listaToken

   6: [11, 'fin']

El siguiente metodo que necesitamos es obtieneProducto, que construye un árbol de expresión para productos. Un producto simple tiene dos números como
operandos, como 3 * 7.

Aquí tenemos una versión de obtieneProducto que maneja productos simples.

   1: def obtieneProducto(listaToken):

   2:     a = obtieneNumero(listaToken)

   3:     if tomaToken(listaToken, '*'):

   4:         b = obtieneNumero(listaToken)

   5:         return Arbol ('*', a, b)

   6:     else:

   7:         return a

Suponiendo que obtieneNumero se ejecuta con éxito y devuelve un árbol simple, asignamos el primer operando a a. Si el siguiente carácter es *, obtenemos el segundo numero y construimos un árbol de expresión con a, b, y el operador.

Si el siguiente carácter es cualquier otra cosa, simplemente devolvemos el nodo hoja con a. Veamos dos ejemplos:

   1: >>> listaToken = [9, '*', 11, 'fin']

   2: >>> arbol = obtieneProducto(listaToken)

   3: >>> imprimeArbolPostfijo(arbol)

   4: 9 11 *

   5: >>> listaToken = [9, '+', 11, 'fin']

   6: >>> arbol = obtieneProducto(listaToken)

   7: >>> imprimeArbolPostfijo(arbol)

   8: 9

El segundo ejemplo implica que entendemos un operando suelto como un tipo de producto. Esta definición de producto” es contraria a la intuición, pero resulta ser útil.

Ahora tenemos que vernos las con productos compuestos, como 3 * 5 * 13.

Tratamos esta expresión como un producto de productos, es decir, 3 * (5 *13). El árbol resultante es:

Sin título2

Con un pequeño cambio en obtieneProducto podemos manejar productos arbitrariamente largos:

   1: def obtieneProducto(listaToken):

   2:     a = obtieneNumero(listaToken)

   3:     if tomaToken(listaToken, '*'):

   4:         b = obtieneProducto(listaToken) # cambiamos esta l¶³nea

   5:         return Arbol ('*', a, b)

   6:     else:

   7:         return a

En otras palabras, un producto puede ser bien un operando aislado o un árbol con * en su raíz, un numero en la derecha y un producto en la izquierda. Este tipo de definición recursiva deber³a empezar a resultar familiar.

Comprobemos la nueva versión con un producto compuesto:

   1: >>> listaToken = [2, '*', 3, '*', 5 , '*', 7, 'fin']

   2: >>> arbol = obtieneProducto(listaToken)

   3: >>> imprimeArbolPostfijo(arbol)

   4: 2 3 5 7 * * *

Ahora añadiremos la capacidad de analizar sumas. De nuevo, usamos una definición poco intuitiva de suma”. Para nosotros, una suma puede ser un árbol con + en la ra³z, un producto en la izquierda y una suma en la derecha. O también, una suma puede ser simplemente un producto.

Si quiere seguir adelante con esta definición, tiene una bonita propiedad: podemos representar cualquier expresión (sin paréntesis) como una suma de productos. Esta propiedad es la base de nuestro algoritmo de analisis.

obtieneSuma intenta construir un árbol con un producto en la izquierda y una suma en la derecha. Pero si no encuentra un +, simplemente construye un producto.

   1: def obtieneSuma(listaToken):

   2:     a = obtieneProducto(listaToken)

   3:     if tomaToken(listaToken, '+'):

   4:         b = obtieneSuma(listaToken)

   5:         return Arbol ('+', a, b)

   6:     else:

   7:         return a

 

Vamos a probarla con 9 * 11 + 5 * 7:

   1: >>> listaToken = [9, '*', 11, '+', 5, '*', 7, 'fin']

   2: >>> arbol = obtieneSuma(listaToken)

   3: >>> imprimeArbolPostfijo(arbol)

   4: 9 11 * 5 7 * +

 

Ya casi hemos acabado, pero todavía tenemos que manejar los paréntesis.

En cualquier lugar de una expresión donde puede haber un numero, puede haber también una suma entera entre paréntesis. Solo necesitamos modificar
obtieneNumero para manejar subexpresiones:

 

   1: def obtieneNumero(listaToken):

   2:     if tomaToken(listaToken, '('):

   3:         x = obtieneSuma(listaToken) # obtiene la subexpresi¶on

   4:         tomaToken(listaToken, ')') # quita el cierre de par¶entesis

   5:         return x

   6:     else:

   7:         x = listaToken[0]

   8:     if type(x) != type(0): return None

   9:         listaToken[0:1] = []

  10:         return Arbol (x, None, None)

 

Vamos a probar este código con 9 * (11 + 5) * 7:

   1: >>> listaToken = [9, '*', '(', 11, '+', 5, ')', '*', 7, 'fin']

   2: >>> arbol = obtieneSuma(listaToken)

   3: >>> imprimeArbolPostfijo(arbol)

   4: 9 11 5 + 7 * *

El analizador manejo correctamente los paréntesis; la suma ocurre antes de la multiplicación.

En la versión final del programa, sería bueno dar a obtieneNumero un nombre mas descriptivo de su nueva función.

20.6. Manejar errores

En todo momento hemos supuesto que las expresiones estaban bien formadas.

Por ejemplo, cuando alcanzamos el final de una subexpresion, suponemos que el carácter siguiente es un paréntesis cerrado. Si hay un error y el siguiente carácter es cualquier otra cosa, deber³amos ocuparnos de el.

   1: def obtieneNumero(listaToken):

   2:     if tomaToken(listaToken, '('):

   3:         x = obtieneSuma(listaToken)

   4:         if not tomaToken(listaToken, ')'):

   5:             raise 'ExpresionErronea', 'falta un par¶entesis'

   6:         return x

   7:     else:

   8:     # omitido el resto de la función

La sentencia raise crea una excepción; en este caso creamos un nuevo tipo de excepción, llamado ExpresionErronea. Si la función que llamo a obtieneNumero, o alguna de las otras funciones de la pila de llamadas, maneja la expresión, el programa podrá continuar. En caso contrario, Python imprimirá un mensaje de error y saldrá.

Como ejercicio, encuentre otros lugares de estas funciones donde
puedan ocurrir errores y añada las sentencias raise adecuadas.
Pruebe su código con expresiones formadas incorrectamente.

20.7. El árbol de animales

En esta sección desarrollaremos un pequeño programa que usa un árbol para representar una base de conocimientos.

El programa interactúa con el usuario para crear un árbol de preguntas y nombres de animales. Aquí tenemos un ejemplo de ejecución:

Estas pensando en un animal? s
Es un pájaro? n
Como se llama el animal? perro
Que pregunta distinguir³a a un perro de un pájaro? Puede volar
Si el animal fuera un perro, cual sería la respuesta? n
Estas pensando en un animal? s
Puede volar? n
Es un perro? n
Como se llama el animal? gato
Que pregunta distinguir³a a un gato de un perro? Ladra
Si el animal fuera un gato, cual ser³a la respuesta? n
Estas pensando en un animal? s
Puede volar? n
Ladra? s
Es un perro? s
Soy el mas grande!

Estas pensando en un animal? n

Este es el árbol que construye este dialogo:

Sin título

Al principio de cada ronda, el programa empieza en lo alto del árbol y hace la primera pregunta. Dependiendo de la respuesta, se mueve al hijo de la izquierda o de la derecha y sigue hasta que llega a un nodo hoja. En ese momento, intenta adivinar. Si falla, pide al usuario el nombre del nuevo animal y una pregunta que distinga al intento fallido del nuevo animal. Entonces a~nade un nodo al árbol con la nueva pregunta y el nuevo animal.

Este es el código:

   1: def animal():

   2: # empezar con un nodo suelto

   3: raiz = Arbol("pajaro")

   4: # bucle hasta que el usuario salga

   5: while 1:

   6: print

   7: if not si("Estas pensando en un animal? "): break

   8: # recorrer el arbol

   9: arbol = raiz

  10: while arbol.tomaIzquierda() != None:

  11: indicador = arbol.tomaCarga() + "? "

  12: if si(indicador):

  13: arbol = arbol.tomaDerecha()

  14: else:

  15: arbol = arbol.tomaIzquierda()

  16: # intentar adivinar

  17: adivina = arbol.tomaCarga()

  18: indicador = "Es un " + adivina + "? "

  19: if si(indicador):

  20: print "Soy el mas grande!"

  21: continue

  22: # obtener informacion nueva

  23: indicador = "Como se llama el animal? "

  24: animal = raw_input(indicador)

  25: indicador = "Que pregunta distinguir³a a un %s de un %s? "

  26: pregunta = raw_input(indicador % (animal,adivina))

  27: # añadir informacion nueva al arbol

  28: arbol.ponCarga(pregunta)

  29: indicador = "Si el animal fuera un %s, cual ser³a la

  30: respuesta? "

  31: if si(indicador % animal):

  32: arbol.ponIzquierda(Arbol(adivina))

  33: arbol.ponDerecha(Arbol(animal))

  34: else:

  35: arbol.ponIzquierda(Arbol(animal))

  36: arbol.ponDerecha(Arbol(adivina))

 

La función si es un auxiliar; imprime un indicador y acepta una entrada del usuario. Si la respuesta comienza con s o S, la función devuelve verdadero:

   1: def si(preg):

   2: from string import lower

   3: resp = lower(raw_input(preg))

   4: return (resp[0] == 's')

La condición del bucle externo es 1, lo que significa que seguirá hasta que se ejecute la sentencia break cuando el usuario ya no piense en ningún animal.

El bucle while interno recorre el árbol de arriba a abajo, guiado por las respuestas del usuario.

Cuando se añade un nuevo nodo al árbol, la pregunta sustituye a la carga y los dos hijos son el animal nuevo y la carga original.

Una carencia del programa es que, al salir, <olvida todo lo que usted le había enseñado con tanto cuidado!

Como ejercicio, piense en varias formas en las que podría guardar el árbol de conocimiento en un archivo. Implemente la que piense que es mas fácil.

20.8. Glosario

árbol binario: Un árbol en el que cada nodo apunta a cero, uno, o dos nodos dependientes.

raíz: El nodo superior de un árbol, sin padre.

hoja: Un nodo del extremo inferior de un árbol, sin hijos.

padre: El nodo que apunta a un nodo dado.

hijo: Uno de los nodos a los que apunta un nodo.

hermanos: Nodos que tienen un padre común.

nivel: El conjunto de nodos equidistante de la raíz.

operador binario: Un operador que toma dos operandos.

subexpresión: Una expresión entre paréntesis que actúa como un operando simple dentro de otra expresión mayor.orden prefijo: Una forma de recorrer un árbol, visitando cada nodo antes que a sus hijos.

notación prefija: Una forma de escribir una expresión matemática en la que los operadores aparecen antes que sus operandos.

orden postfijo: Una forma de recorrer un árbol, visitando los hijos de cada nodo antes del propio nodo.

orden infijo: Una forma de recorrer un árbol, visitando el subárbol izquierdo, luego la raíz, y luego el subárbol derecho.

Apéndice A

Depuración

En un programa pueden suceder varios tipos de error, y resulta útil distinguirlos para localizarlos rápidamente:

  • Python presenta errores de sintaxis mientras traduce el código fuente en código binario. Normalmente indican que hay algo erróneo en la sintaxis del programa. Ejemplo: omitir los dos puntos al final de una sentencia def nos da el mensaje SyntaxError: invalid syntax, algo redundante.
  • El sistema de tiempo de ejecución presenta los errores en tiempo de ejecución si algo va mal mientras se ejecuta el programa. La mayoría de los mensajes de error en tiempo de ejecución incluyen información acerca de donde sucedió el error y que funciones se estaban ejecutando. Ejemplo: una recursión infinita termina por provocar un error en tiempo de ejecución del máximum recursión depth exceeded” (superada la profundidad máxima de recursión).
  • Los errores semánticos son problemas con un programa que compila y se ejecuta pero no hace lo que se espera de el. Ejemplo: una expresión puede no evaluarse en el orden esperado, dando un resultado inesperado.

El primer paso de la depuración es averiguar con que tipo de error se enfrenta.

Aunque las secciones que siguen están organizadas por tipos de error, algunas tecnicas son aplicables en mas de una situación.

A.1. Errores de sintaxis

Los errores de sintaxis suelen ser fáciles de arreglar una vez que averigua lo que son. Desgraciadamente, muchas veces los mensajes de error no son muy útiles. Los mensajes mas comunes son SyntaxError: invalid syntax y SyntaxError: invalid token, ninguno de los cuales es muy informativo.Por otra parte, el mensaje le dice en que lugar del programa sucedió el error.

En realidad, le dice donde noto el problema Python, que no es necesariamente donde esta el error. A veces el error esta antes de la localización del mensaje de error, muchas veces en la línea anterior.

Si esta haciendo el programa incrementalmente, deber³a tener casi localizado el error. Estará en la ultima línea que añadió.

Si esta usted copiando código de un libro, comience comparando con atención su código con el del libro. Compruebe cada carácter. Al mismo tiempo, recuerde que el libro podr³a estar equivocado, as³ que si ve algo que parezca un error de
sintaxis, podría serlo.

He aquí algunas formas de evitar los errores de sintaxis mas habituales:

  1. Asegúrese de que no utiliza una palabra clave de Python como nombre de variable.
  2. Compruebe que tiene los dos puntos al final de la cabecera de todas las sentencias compuestas, las for, while, if, y def.
  3. Compruebe que el sangrado es consistente. Puede usted sangrar tanto con espacios como con tabuladores, pero es mejor no mezclarlos. Todos los niveles deberían estar anidados por la misma cantidad.
  4. Asegúrese de que todas las cadenas del código tienen su par de comillas de apertura y cierre.
  5. Si tiene cadenas que ocupan varias l³neas con triples comillas (o triples apóstrofos), asegúrese de que ha terminado la cadena correctamente. Una cadena sin terminar puede provocar un error invalid token al final de su programa, o puede tratar la siguiente parte del programa como una cadena hasta que llegue a la siguiente cadena. <En el segundo caso, podría no presentar ningún mensaje de error!
  6. Un paréntesis sin cerrar|(, { o [|hace que Python continue con la línea siguiente como parte de la sentencia actual. Generalmente aparecerá un error casi inmediatamente en la l³nea siguiente.
  7. Compruebe el clásico = donde debería haber un == en los condicionales.

Si nada funciona, siga con la sección que sigue…

Página 127 de 143

Creado con WordPress & Tema de Anders Norén