Next Up Previous Hi Index

Chapter 18

\newcommand{\tab{\hspace{5mm}} \chapter{Pilas} \section{Tipos abstractos de datos} \index{tipo abstracto de datos {\textbar}ver{TAD}} \index{TAD} \index{encapsulación} Los tipos de datos vistos hasta ahora han sido todos concretos, en el sentido que se ha especificado su implementación completamente. Por ejemplo, la clase {\tt Carta} representa un naipe por medio de dos enteros. Como dijimos, esa no es la única manera de representar una carta; existen muchas implementaciones alternativas. Un {\bf tipo abstracto de datos}, o TAD, especifica un conjunto de operaciones (o métodos) y la semántica de las operaciones (lo que hacen), pero no especifica la implementación de las operaciones. Esto es lo hace lo abstracto. ¿Para qué sirve? \begin{itemize} \item Simplifica la tarea de especificar un algoritmo si se pueden indicar las operaciones necesarias sin preocuparse de cómo se ejecutarán dichas operaciones. \item Como suelen existir muchas maneras de implementar un TAD, puede ser útil desarrollar un algoritmo que se pueda usar con cualquiera de las implementaciones posibles. \item Los TADs mas conocidos, como el TAD Pila de este capítulo, se implementan a menudo en bibliotecas estándares de manera que se puedan escribir una vez y usarlas luego muchos programadores. \item Las operaciones ejecutadas con TADs proporcionan un lenguaje de alto nivel común para desarrollar y hablar sobre algoritmos. \end{itemize} Cuando hablamos de TADs a menudo se hace la distinción entre el código que usa el TAD, el código {\bf cliente}, y el código que implementa el TAD, el código {\bf proveedor}. \index{cliente} \index{proveedor} \section{El TAD Pila} \index{pila} \index{colección} \index{TAD!Pila} En este capítulo se presentará un TAD común, la {\bf pila}. Una pila es una colección, lo que significa que es una estructura de datos que contiene elementos múltiples. Otras colecciones que se han visto son los diccionarios y las listas. \index{interfaz} Un TAD se definido por medio de las operaciones que se pueden ejecutar sobre él, lo que se llama un {\bf interfaz}. La interfaz para una pila consta de estas operaciones\footnote{Mantenemos los nombres ingleses porque son estándar en otros TADs en Python y otros lenguajes.}: \begin{description} \item[{\tt \_\_init\_\_}:] Inicializar una pila nueva y vacía. \item[{\tt push}:] Añadir un elemento a la pila. \item[{\tt pop}:] Extraer un elemento de la pila. El elemento devuelto siempre es el último que se añadió. \item[{\tt isEmpty}:] Probar si la pila está vacía. \end{description} A veces a una pila se la llama una estructura ``último en entrar primero en salir'' (``last in, first out'' en inglés), o LIFO, porque el elemento añadido en último lugar es el primero que extraemos. \section {Cómo implementar pilas con listas de Python} \index{Pila} \index{clase!Pila} \index{estructura genérica de datos} \index{estructura de datos!genérica} Las operaciones sobre listas que posee Python son parecidas a las operaciones que definen a una pila. La interfaz no es exactamente la que debería de ser, pero se pueden desarrollar programas para convertir el TAD Pila a las operaciones predefinidas. A este programa se lo llama {\bf implementación} del TAD Pila. En general, una implementación es un conjunto de métodos que satisfacen los prerrequisitos sintácticos y semánticos de la interfaz. He aquí una implementación de el TAD Pila que utiliza una lista Python: \beforeverb \begin{verbatim} class Pila : def __init__(self) : self.elementos = [] def push(self, elemento) : self.elementos.append(elemento) def pop(self) : return self.elementos.pop() def isEmpty(self) : return (self.elementos == []) \end{verbatim} \afterverb % Un objeto {\tt Pila} contiene un atributo llamado {\tt elementos} que es una lista de elementos en la pila. El método de inicialización pone una lista vacía en {\tt elementos}. Para meter un elemento nuevo en la pila, {\tt push} lo apila en {\tt elementos}. Para quitar un elemento de la pila, {\tt pop} utiliza el método de lista homónimo\footnote{del mismo nombre} para quitar y devolver el último elemento de la lista. Finalmente, para probar si la pila esta vacía, {\tt isEmpty} (está vacía) compara {\tt elementos} con la lista vacía. \index{enchapado} Una implementación como esta, en la cual los métodos consisten de llamadas a métodos existentes, se llama {\bf enchapado}. En la vida real, un enchapado es una capa fina de madera de alta calidad que se utiliza en la fabricación de muebles para ocultar madera de baja calidad. Los científicos informáticos utilizan esta metáfora para describir una parte de un programa que esconde los detalles de una implementación y que provee una interfaz mas simple o mas estándar. \section{Uso de {\tt push} y {\tt pop}} \index{push} \index{pop} \index{estructura genérica de datos} \index{estructura de datos!genérica} Una pila es una {\bf estructura genérica de datos}, lo significa que se puede añadir cualquier tipo de elementos a ella. El ejemplo mete en la pila dos enteros y una cadena: \beforeverb \begin{verbatim} >>> s = Stack() >>> s.push(54) >>> s.push(45) >>> s.push("+") \end{verbatim} \afterverb % Se pueden utilizar {\tt isEmpty} y {\tt pop} para quitar e imprimir todos los elementos en la pila: \beforeverb \begin{verbatim} while not s.isEmpty() : print s.pop(), \end{verbatim} \afterverb % La salida es {\tt + 45 54}. En otras palabras, ¡se ha utilizado una pila para imprimir los elementos en orden inverso! Reconocemos que no es el formato estándar de impresión de listas, pero fue muy fácil usar una pila para lograrlo. Compare estas líneas con la implementación de {\tt imprimeAlReves} que vimos en la Sección~\ref{listrecursion}. Existe un paralelo natural entre la versión recurrente de {\tt imprimeAlReves} y el algoritmo de pila que acabamos de ver. La diferencia es que {\tt imprimeAlReves} utiliza la pila de tiempo de ejecución para mantenerse al tanto de los nodos mientras recorre la lista y luego los imprime cuando regresa de la recursión. El algoritmo de pila hace lo mismo, pero utiliza un objeto {\tt Pila} en vez de la pila de tiempo de ejecución. \section {Usar una pila para evaluar postfijo} \index{postfijo} \index{infijo} \index{expresión} Las expresiones matemáticas en la mayoría de lenguajes de programación se escriben con el operador entre los dos operandos, de esta manera: {\tt 1+2}. A este formato se le llama {\bf infijo}. Algunas calculadoras utilizan un formato alternativo llamado {\bf postfijo}. Con postfijo, el operador va después de los operandos, así: {\tt 1 2 +}. La razón por la que el formato postfijo es útil es que existe una forma natural de evaluar una expresión en formato postfijo utilizando una pila: \begin{itemize} \item Desde el principio de la expresión, evalúe los operadores y operandos uno por uno. \begin{itemize} \item Si el término es un operando, utilice push para colocarlo en la pila. \item Si el término es un operador, utilice pop con dos operandos de la pila, ejecute la operación sobre ellos, y coloque el resultado en la pila con push. \end{itemize} \item Cuando llegue al final de la expresión habrá un operando en la pila. Ese operando es el resultado. \end{itemize} \begin{quote} {\em Para practicar, aplique este algoritmo a la expresión {\tt 1 2 + 3 *}.} \end{quote} Este ejemplo demuestra una de las ventajas de el formato postfijo---no hay necesidad de usar paréntesis para controlar el orden de operaciones. Para obtener el mismo resultado con el formato infijo, se tendría que escribir {\tt (1 + 2) * 3)}. \begin{quote} {\em Para practicar, escriba una expresión en formato postfijo que sea equivalente a {\tt 1 + 2 * 3}.} \end{quote} \section {Análisis sintáctico} \index{analizar} \index{token} \index{delimitador} \index{expresión regular} Para implementar el algoritmo anterior, necesitamos ser capaces de recorrer una cadena y separarla en operandos y operadores. Este proceso es un ejemplo del {\bf análisis sintáctico}, y los resultados---la piezas individuales de la cadena---son {\bf tokens}\footnote{Podríamos traducir ``token'' como ``cospel'' o ``pieza'', en ocasiones también como ``símbolo'' pero la expresión inglesa está tan introducida en el vocabulario informático que sólo añadiría confusión.}. Quizás se acuerde de esas palabras del Capítulo 1. Python posee un método llamado {\tt split} (partir) en el módulo {\tt string} (cadena) y en el módulo {\tt re} (expresiones regulares). La función {\tt string.split} parte una cadena y la convierte en una lista utilizando un sólo carácter como {\bf delimitador}. Por ejemplo: \beforeverb \begin{verbatim} >>> import string >>> string.split("La hora ha llegado"," ") ['La', 'hora', 'ha', 'llegado'] \end{verbatim} \afterverb % En este caso, el delimitador es el carácter espacio, y se parte la cadena en cada espacio. La función {\tt re.split} tiene más potente, y nos permite utilizar una expresión regular en vez de un delimitador. Una expresión regular es una manera de especificar un conjunto de cadenas. Por ejemplo, \verb+[A-z]+ es el conjunto de todas las letras y \verb+[0-9]+ es el conjunto de todas las cifras. El operador \verb+^+ niega un conjunto, y \verb+[^0-9]+ es el conjunto de todo lo que no es un número, lo cual es exactamente lo que queremos utilizar para partir expresiones en el formato postfijo: \beforeverb \begin{verbatim} >>> import re >>> re.split("([^0-9])", "123+456*/") ['123', '+', '456', '*', '', '/', ''] \end{verbatim} \afterverb % Fíjese que el orden de los argumentos es diferente del de {\tt string.split}; el delimitador viene antes de la cadena. La lista resultante incluye los operandos {\tt 123} y {\tt 456} y los operadores {\tt *} y {\tt /}. También incluye dos cadenas vacías que se insertan después de los operandos. \section {Evaluar un postfijo} Para evaluar una expresión en formato postfijo usaremos el analizador sintáctico de la sección anterior y el algoritmo de la sección previa a ésa. Para no complicar las cosas, comenzaremos con un evaluador que solo implementa los operadores {\tt +} y {\tt *}: \adjustpage{-3} \beforeverb \begin{verbatim} def evalPostfijo(expr): import re listaTokens = re.split("([^0-9])", expr) pila = Pila() for token in listaTokens: if token == '' or token == ' ': continue if token == '+': suma = pila.pop() + pila.pop() pila.push(suma) elif token == '*': producto = pila.pop() * pila.pop() pila.push(producto) else: pila.push(int(token)) return pila.pop() \end{verbatim} \afterverb % La primera condición se encarga de espacios y cadenas vacías. Las dos condiciones siguientes controlan los operadores. Asumimos, por ahora, que todo lo demás es un operando. Por supuesto, sería mejor verificar si la entrada tiene errores y mostrar un mensaje con el error, pero eso se hará después. Comprobemos con una evaluación de la forma postfijo {\tt (56+47)*2)}: \beforeverb \begin{verbatim} >>> print evalPostfijo ("56 47 + 2 *") 206 \end{verbatim} \afterverb % Esto es suficiente. \section {Clientes y proveedores} \index{encapsulación} \index{TAD} Uno de los objetivos fundamentales de un TAD es el de separar los intereses del proveedor, quien escribe el código de programa que implementa el TAD, y los del cliente, quien utiliza el TAD. El proveedor sólo se preocupa de la implementación y de si es correcta o no---de acuerdo a las especificaciones del TAD---y no de cómo se va a utilizar. A la inversa, el cliente {\em supone} que la implementación del TAD es correcta y no se preocupa de los detalles. Cuando se usa uno de los tipos predefinidos de Python, uno se puede dar el lujo de pensar exclusivamente como un cliente. Por supuesto, cuando usted implemente un TAD, también debe desarrollar código de cliente para probarlo. En ese case, uno toma ambos papeles, lo cual puede causar confusión. Tiene que fijarse bien en del papel que está tomando en todo momento. \section{Glosario} \index{TAD} \index{cliente} \index{proveedor} \index{infijo} \index{postfijo} \index{analizar sintácticamente} \index{token} \index{delimitador} \begin{description} \item[tipo abstracto de datos (TAD):] Un tipo de datos (a menudo una colección de objetos) que se define por un conjunto de operaciones pero que se puede implementar de varias maneras. \item[interfaz:] El conjunto de operaciones que definen un TAD. \item[implementación:] El código de programa que satisface los prerrequisitos sintácticos y semánticos de un interfaz. \item[cliente:] Un programa (o la persona que lo escribió) que utiliza un TAD. \item[proveedor:] Un programa (o la persona que lo escribió) que implementa un TAD. \item[enchapado:] La definición de clase que implementa un TAD con definiciones de métodos que son las invocaciones de otros métodos, a veces con transformaciones simples. El enchapado no ejecuta nada de gran valor, pero mejora la interfaz vista por el cliente o la hace mas estándar. \item[estructura de datos genérica:] Un tipo de estructura de datos que puede contener datos de cualquier tipo. \item[infijo:] Un método de escribir expresiones matemáticas con los operadores entre los operandos. \item[postfijo:] Un método de escribir expresiones matemáticas con los operadores después de los operandos. \item[analizar sintácticamente:] Examinar una cadena de caracteres o tokens y analizar su estructura gramátical. \item[token:] Un conjunto de caracteres que se tratan como una unidad y son analizados sintácticamente, como las palabras de un lenguaje natural. \item[delimitador:] Un carácter utilizado para separar tokens, como la puntuación en un lenguaje natural. \end{description}


Next Up Previous Hi Index