You are on page 1of 93
MANUAL DE PRÁCTICAS PROGRAMACIÓN ORIENTADA A OBJETOS MTI MARIO SALVADOR CASTRO ZENIL ACADEMIA DE INGENIERÍA
MANUAL DE PRÁCTICAS PROGRAMACIÓN ORIENTADA A OBJETOS MTI MARIO SALVADOR CASTRO ZENIL ACADEMIA DE INGENIERÍA

MANUAL DE PRÁCTICAS PROGRAMACIÓN ORIENTADA A OBJETOS

MTI MARIO SALVADOR CASTRO ZENIL ACADEMIA DE INGENIERÍA BIOMÉDICA
MTI MARIO SALVADOR
CASTRO ZENIL
ACADEMIA DE INGENIERÍA
BIOMÉDICA

1.

PRESENTACIÓN

Estimado Estudiante:

El aprendizaje y la generación de conocimiento de las ciencias de la computación siempre han estado ligados a la experimentación y la repetición. La programación que debe de conocer un ingeniero biomédico está ligada a la interacción con hardware y lenguajes enfocados al trabajo en ingeniería.

Las prácticas de este manual proceden de diferentes fuentes adaptados a los objetivos que se propone alcanzar dentro de la asignatura de Programación Orientada a Objetos y a los recursos didácticos disponibles en el laboratorio del ITSPA.

Las instrucciones para la realización de cada práctica están bien detalladas, las ideas conceptuales de cada experiencia vienen acompañadas de una breve introducción y se agregan los retos que deberán de cumplirse.

Al estudiante se le recomienda que antes de comenzar una práctica lea las instrucciones generales del manual, así también la exposición teórica para que alcance una comprensión clara de lo que va a hacer. Se le recomienda, además que conserve un registro de la experiencia y de las medidas para la elaboración de su respectivo reporte.

2. OBJETIVO GENERAL

El objetivo del Laboratorio de Programación Orientada a Objetos es familiarizarse con el paradigma de objetos en programación empleando para esto la codificación en lenguaje de programación Python y LabView.

Este manual tiene la intención de servir como una guía para el desarrollo de prácticas y para la escritura de reportes de laboratorio.

Esperamos que la experiencia en este curso despierte tu curiosidad y te permita orientar tu entusiasmo hacia la resolución de problemas relacionados con la biomédica.

3. PRÁCTICAS

3.1 Unidad 1

PRÁCTICA

#1.1

Clases

De acuerdo a lo aprendido en cátedras cree una clase en Python que imprima los datos básicos de un equipo medico de uso hospitalario en un inventario llamado equipoMedico.

3.2 Unidad 2

Una clase Fraccion

Un ejemplo muy común para mostrar los detalles de la implementación de una clase definida por el usuario es construir una clase para implementar el tipo abstracto de datos Fraccion. Ya hemos visto que Python proporciona una serie de clases numéricas para nuestro uso. Hay ocasiones en las que, sin embargo, sería más apropiado ser capaz de crear objetos de datos que ―luzcan‖ como fracciones.

Una fracción como 3/5 consta de dos partes. El valor de arriba, conocido como el numerador, puede ser cualquier entero. El valor de abajo, llamado el denominador, puede ser cualquier entero mayor que 0 (las fracciones negativas tienen un numerador negativo). Aunque es posible crear una aproximación de punto flotante para cualquier fracción, en este caso nos gustaría representar la fracción como un valor exacto.

Las operaciones para el tipo Fraccion permitirán que un objeto de datos Fraccion se comporte como cualquier otro valor numérico. Necesitamos ser capaces de sumar, restar, multiplicar y dividir fracciones. También queremos ser capaces de mostrar fracciones usando la forma estándar de ―barra‖, por ejemplo 3/5. Además, todos los métodos de fracciones deben devolver resultados en sus términos menores de modo que, sin importar el cálculo que se realice, siempre terminemos con la forma más simplificada.

En Python, definimos una nueva clase proporcionando un nombre y un conjunto de definiciones de métodos que son sintácticamente similares a las definiciones de funciones. Para este ejemplo,

class Fraccion:

#los métodos van aquí

Proporciona el esqueleto para definir los métodos. El primer método que todas las clases deben proporcionar es el constructor. El constructor define la forma en que se crean los objetos de datos. Para crear un objeto Fraccion, tendremos que proporcionar dos piezas de

datos, el numerador y el denominador. En Python, el método constructor siempre se llama

init

(dos subrayados antes y después de init) y se muestra en el Programa 2.

Programa 2

class Fraccion:

def

init

(self,arriba,abajo):

self.num = arriba

self.den = abajo

Observe que la lista de parámetros formales contiene tres elementos (self, arriba, abajo). self es un parámetro especial que siempre se utilizará como una referencia al objeto mismo. Debe ser siempre el primer parámetro formal; no obstante, nunca se le dará un valor de parámetro real en la invocación.

Como se describió anteriormente, las fracciones requieren dos piezas de datos de estado, el numerador y el denominador. La notación self.num en el constructor define que el objeto fraccion tenga un objeto de datos interno llamado num como parte de su estado. Del mismo modo, self.den crea el denominador. Los valores de los dos parámetros formales se asignan inicialmente al estado, permitiendo que el nuevo objeto fraccion conozca su valor inicial.

Observe que la lista de parámetros formales contiene tres elementos (self, arriba, abajo). self es un parámetro especial que siempre se utilizará como una referencia al objeto mismo. Debe ser siempre el primer parámetro formal; no obstante, nunca se le dará un valor de parámetro real en la invocación. Como se describió anteriormente, las fracciones requieren dos piezas de datos de estado, el numerador y el denominador. La notación self.num en el constructor define que el objeto fraccion tenga un objeto de datos interno llamado num como parte de su estado. Del mismo modo, self.den crea el denominador. Los valores de los dos parámetros formales se asignan inicialmente al estado, permitiendo que el nuevo objeto fraccion conozca su valor inicial.

Para crear una instancia de la clase Fraccion, debemos invocar al constructor. Esto ocurre usando el nombre de la clase y pasando los valores reales para el estado necesario (note que

nunca invocamos directamente a

init

). Por ejemplo,

miFraccion = Fraccion(3,5)

Crea un objeto llamado miFraccion que representa la fracción 3/5 (tres quintos). La Figura muestra este objeto tal como está implementado ahora.

Lo siguiente que debemos hacer es implementar el comportamiento que requiere el tipo abstracto de

Lo siguiente que debemos hacer es implementar el comportamiento que requiere el tipo abstracto de datos. Para comenzar, considere lo que sucede cuando tratamos de imprimir un objeto Fraccion.

>>> miF = Fraccion(3,5) >>> print(miF)

<

main

Fraction

instance at 0x409b1acc>

El objeto fraccion, miF, no sabe cómo responder a esta solicitud de impresión. La función print requiere que el objeto sea convertido en una cadena para que se pueda escribir en la salida. La única opción que miF tiene es mostrar la referencia real que se almacena en la variable (la dirección en sí misma). Esto no es lo que queremos.

Hay dos maneras de resolver este problema. Una de ellas es definir un método llamado mostrar que permitirá que el objeto Fraccion se imprima como una cadena. Podemos implementar este método como se muestra en el Programa 3. Si como antes creamos un objeto Fraccion, podemos pedirle que se muestre, en otras palabras, que se imprima en el formato apropiado. Desafortunadamente, esto no funciona en general. Para que la impresión funcione correctamente, necesitamos decirle a la clase Fraccion cómo puede convertirse en una cadena. Esto es lo que necesita la función print para hacer su trabajo.

Programa 3

def mostrar(self):

print(self.num,"/",self.den) >>> miF = Fraccion(3,5) >>> miF.mostrar() 3 / 5 >>> print(miF)

<

>>>

main

Fraction

instance at 0x40bce9ac>

En Python, todas las clases tienen un conjunto de métodos estándar que se proporcionan pero

, objeto en una cadena. La implementación predeterminada para este método es devolver la cadena de la dirección de la instancia como ya hemos visto. Lo que necesitamos hacer es

es el método para convertir un

que podrían no funcionar correctamente. Uno de ellos,

str

proporcionar una ―mejor‖ implementación para este método. Diremos que esta implementación reescribe a la anterior, o que redefine el comportamiento del método.

y le damos una nueva

implementación como se muestra en el Programa 4. Esta definición no necesita ninguna otra información excepto el parámetro especial self. A su vez, el método construirá una representación de cadena convirtiendo cada pieza de datos de estado internos en una cadena y luego colocando un caracter / entre las cadenas usando la concatenación de cadenas. La cadena resultante se devolverá cada vez que se solicite a un objeto Fraccion que se convierta

Para ello, simplemente definimos un método con el nombre

str

en una cadena. Observe las diversas formas en que se utiliza esta función.

Programa 4

def

str

(self):

return str(self.num)+"/"+str(self.den)

>>> miF = Fraccion(3,5) >>> print(miF)

3/5

>>> print("Comí", miF, "de la pizza") Comí 3/5 de la pizza

>>> miF

str

()

'3/5'

>>> str(miF)

 

'3/5'

>>>

Podemos redefinir muchos otros métodos para nuestra nueva clase Fraccion. Algunas de los más importantes son las operaciones aritméticas básicas. Nos gustaría poder crear dos objetos Fraccion y luego sumarlos usando la notación estándar ―+‖. En este punto, si intentamos sumar dos fracciones, obtendremos lo siguiente:

>>> f1 = Fraccion(1,4) >>> f2 = Fraccion(1,2) >>> f1+f2

Traceback (most recent call last):

File "<pyshell#173>", line 1, in -toplevel-

f1+f2

TypeError: unsupported operand type(s) for +:

'instance' and 'instance'

>>>

Si nos fijamos atentamente en el error, veremos que el problema es que el operador ―+‖ no entiende los operandos para Fraccion.

Podemos corregir este error agregándole a la clase Fraccion un método que redefina el método

y requiere dos parámetros. El

primero, self, siempre es necesario, y el segundo representa el otro operando en la expresión. Por ejemplo,

asociado a la adición. En Python, este método se llama

add

f1 add (f2) pedirá al objeto Fraccion f1 que sume el objeto Fraccion f2 a sí mismo. Esto se puede escribir en la notación estándar, f1 + f2.

Dos fracciones deben tener el mismo denominador para poder ser sumadas. La forma más fácil de asegurarse de que tienen el mismo denominador es simplemente utilizar el producto de los dos denominadores como un denominador común de modo que a/b+c/d=ad/bd+cb/bd=ad+cb/bd. La implementación se muestra en el Programa 5. La función de adición devuelve un nuevo objeto Fraccion con el numerador y el denominador de la suma. Podemos usar este método escribiendo una expresión aritmética estándar que involucre fracciones, asignando el resultado de la adición e imprimiendo nuestro resultado.

Programa 5

def

add

(self,otraFraccion):

nuevoNum = self.num*otraFraccion.den + self.den*otraFraccion.num nuevoDen = self.den * otraFraccion.den

return Fraccion(nuevoNum,nuevoDen) >>> f1=Fraccion(1,4) >>> f2=Fraccion(1,2) >>> f3=f1+f2 >>> print(f3)

6/8

>>>

El método de adición ya funciona como queremos, pero una cosa podría ser mejor. Note que 6/8 es el resultado correcto (14+12) pero no está en la representación de ―términos menores‖. La mejor representación sería 3/4. Con el fin de estar seguros de que nuestros resultados estén siempre en los términos menores, necesitamos una función auxiliar que sepa cómo simplificar las fracciones. Esta función tendrá que buscar el máximo común divisor, o MCD. Podemos entonces dividir el numerador y el denominador por el MCD y el resultado se simplificará a los términos menores.

El algoritmo más conocido para encontrar un máximo común divisor es el Algoritmo de Euclides. El Algoritmo de Euclides establece que el máximo común divisor de dos enteros m y n es n si n divide de forma exacta a m. No obstante, si n no divide exactamente a m, entonces la respuesta es el máximo común divisor de n y el residuo de m dividido entre n. Aquí simplemente proporcionaremos una implementación iterativa. Tenga en cuenta que esta implementación del algoritmo del MCD sólo funciona cuando el denominador es positivo. Esto es aceptable para nuestra clase Fraccion porque hemos dicho que una fracción negativa estará representada por un numerador negativo.

def mcd(m,n):

while m%n != 0:

mViejo = m nViejo = n

m = nViejo n = mViejo%nViejo return n

print(mcd(20,10))

Ahora podemos utilizar esta función para ayudar a simplificar cualquier fracción. Para poner una fracción en los términos menores, dividiremos el numerador y el denominador por su máximo común divisor. Por lo tanto, para la fracción 6/8, el máximo común divisor es 2. Dividiendo arriba y abajo por 2 se crea una nueva fracción, ¾ (ver el Programa 6).

Programa 6

def

nuevoNum = self.num*otraFraccion.den + self.den*otraFraccion.num nuevoDen = self.den * otraFraccion.den comun = mcd(nuevoNum,nuevoDen) return Fraccion(nuevoNum//comun,nuevoDen//comun) >>> f1=Fraccion(1,4) >>> f2=Fraccion(1,2) >>> f3=f1+f2 >>> print(f3)

3/4

>>>

add

(self,otraFraccion):

PRÁCTICA

#2.1

Clase Fracción

Tomando en cuenta los ejercicios anteriores, cree los métodos necesarios para realizar las operaciones de resta, multiplicación y división de fracciones.

PRÁCTICA

#2.2

Clase Equipo Médico

De acuerdo a lo planteado en las cátedras construye una clase llamada equipoMedico, dentro de esta clase deberás de generar las funciones propias para dar de alta y de baja equipo Médico , así como planear sus mantenimiento preventivos y poder generar un mantenimiento correctivo, y la salida de almacén para su uso, o en su caso la cita para estudio, validando si el quipo esta funcional o en reparación.

3.3 Unidad 3

Herencia: Compuertas lógicas y circuitos

Nuestra sección final presentará otro aspecto importante de la programación orientada a objetos. La herencia es la habilidad para que una clase esté relacionada con otra clase de la misma manera que las personas pueden estar relacionadas entre sí. Los hijos heredan características de sus padres. Del mismo modo, las clases hija en Python pueden heredar datos

y comportamientos característicos de una clase madre. Estas clases se denominan a menudo subclases y superclases, respectivamente.

La Figura muestra las colecciones incorporadas de Python y sus relaciones entre sí. Llamamos

a una estructura de relación como ésta una jerarquía de herencias. Por ejemplo, la lista es un

hija de la colección secuencial. En este caso, llamamos hija a la lista y madre a la secuencia (o la subclase lista y la superclase secuencia). Esto a menudo se denomina Relación ES-UNA (la lista ES-UNA colección secuencial). Esto implica que las listas heredan características importantes de las secuencias, a saber, el ordenamiento de los datos y operaciones, tales como

la

concatenación, la repetición y la indización.

como la concatenación, la repetición y la indización. Las listas, las tuplas y las cadenas son

Las listas, las tuplas y las cadenas son todas tipos de colecciones secuenciales. Todas heredan organización de datos y operaciones comunes. Sin embargo, cada una de ellas es distinta según los datos sean o no homogéneos y si la colección es inmutable. Los hijos se parecen a sus padres pero se distinguen agregando características adicionales.

Al organizar las clases de esta manera jerárquica, los lenguajes de programación orientados a objetos permiten que el código previamente escrito se extienda para satisfacer las necesidades de una nueva situación. Además, al organizar los datos de esta manera jerárquica, podemos comprender mejor las relaciones que existen entre ellos. Podemos ser más eficientes en la construcción de nuestras representaciones abstractas.

Para explorar esta idea más a fondo, construiremos una simulación, una aplicación para simular circuitos digitales. El bloque constructivo básico para esta simulación será la compuerta lógica. Estos conmutadores electrónicos representan relaciones de álgebra booleana entre su entrada y su salida. En general, las compuertas tienen una sola línea de salida. El valor de la salida depende de los valores dados en las líneas de entrada.

Las compuertas AND tienen dos líneas de entrada, cada una de las cuales puede ser 0 ó 1 (representando False o True, repectivamente). Si ambas líneas de entrada tienen valor 1, la salida resultante es 1. Sin embargo, si una o ambas líneas de entrada son 0, el resultado es 0. Las compuertas OR también tienen dos líneas de entrada y producen un 1 si uno o ambos valores de entrada son 1. En el caso en que ambas líneas de entrada sean 0, el resultado es 0.

Las compuertas NOT se diferencian de las otras dos compuertas porque sólo tienen una única línea de entrada. El valor de salida es simplemente el opuesto al valor de entrada. Si aparece 0 en la entrada, se produce 1 en la salida. Similarmente, un 1 produce un 0. La Figura muestra cómo se representa típicamente cada una de estas compuertas. Cada compuerta tiene también

una tabla de verdad de valores que muestran el mapeo de entrada a salida que es llevado a cabo por la compuerta.

de entrada a salida que es llevado a cabo por la compuerta. Podemos construir circuitos que

Podemos construir circuitos que tengan funciones lógicas al combinar estas compuertas en varios patrones y luego aplicarles un conjunto de valores de entrada. La Figura muestra un circuito que consta de dos compuertas AND, una compuerta OR y una única compuerta NOT. Las líneas de salida de las dos compuertas AND se conectan directamente en la compuerta OR y la salida resultante de la compuerta OR es suministrada a la compuerta NOT. Si aplicamos un conjunto de valores de entrada a las cuatro líneas de entrada (dos por cada puerta AND), los valores se procesan y aparece un resultado en la salida de la compuerta NOT. La Figura 10 también muestra un ejemplo con valores.

NOT. La Figura 10 también muestra un ejemplo con valores. Para implementar un circuito, primero construiremos

Para implementar un circuito, primero construiremos una representación para compuertas lógicas. Las compuertas lógicas se organizan fácilmente en una jerarquía de herencias de clase como se muestra en la Figura. En la parte superior de la jerarquía, la clase CompuertaLogica representa las características más generales de las compuertas lógicas: a saber, una etiqueta para la compuerta y una línea de salida. El siguiente nivel de subclases divide las compuertas lógicas en dos familias, las que tienen una línea de entrada y las que tienen dos. Debajo de ellas, aparecen las funciones lógicas específicas de cada una.

Ahora podemos comenzar a implementar las clases empezando con la más general, CompuertaLogica. Como se

Ahora podemos comenzar a implementar las clases empezando con la más general, CompuertaLogica. Como se ha indicado anteriormente, cada compuerta tiene una etiqueta para la identificación y una sola línea de salida. Además, necesitamos métodos para permitir que un usuario de una compuerta le pida la etiqueta a la compuerta.

El otro comportamiento que necesita toda compuerta lógica es la capacidad de conocer su valor de salida. Esto requerirá que la compuerta lleve a cabo la lógica apropiada con base en la entrada actual. Con el fin de producir la salida, la compuerta tiene que saber específicamente cuál es esa lógica. Esto implica invocar a un método para realizar el cálculo lógico. La clase completa se muestra en el Programa 8.

Programa 8

class CompuertaLogica:

def

init

(self,n):

self.etiqueta = n self.salida = None

def obtenerEtiqueta(self):

return self.etiqueta

def obtenerSalida(self):

self.salida = self.ejecutarLogicaDeCompuerta() return self.salida

En este punto, no implementaremos la función ejecutarLogicaDeCompuerta. La razón de esto es que no sabemos cómo llevará a cabo cada compuerta su propia operación lógica. Estos detalles serán incluidos por cada compuerta individual que se añada a la jerarquía. Esta es una idea muy poderosa en la programación orientada a objetos.

Estamos escribiendo un método que usará código que aún no existe. El parámetro self es una referencia al verdadero objeto compuerta que invoca el método. Cualquier compuerta lógica nueva que se agregue a la jerarquía simplemente tendrá que implementar la función ejecutarLogicaDeCompuerta y se utilizará en el momento apropiado. Una vez se haya usado, la compuerta puede proporcionar su valor de salida. Esta capacidad de extender una jerarquía que existe actualmente y proporcionar las funciones específicas que la jerarquía necesita para usar la nueva clase es extremadamente importante para reutilizar el código ya existente.

Categorizamos las compuertas lógicas en función del número de líneas de entrada. La compuerta AND tiene dos líneas de entrada. La compuerta OR también tiene dos líneas de entrada. Las compuertas NOT tienen una línea de entrada. La clase CompuertaBinaria será una subclase de CompuertaLogica y agregará dos líneas de entrada. La clase CompuertaUnaria también será subclase de CompuertaLogica pero sólo contará con una única línea de entrada. En el diseño de circuitos asistido por computador, estas líneas a veces se llaman ―pines‖ por lo que vamos a utilizar esa terminología en nuestra implementación.

Programa 9

class CompuertaBinaria(CompuertaLogica):

def

init

(self,n):

CompuertaLogica

self.pinA = None self.pinB = None

init

(self,n)

def obtenerPinA(self):

return int(input("Ingrese la entrada del Pin A para la compuerta "+ self.obtenerEtiqueta()+"-->"))

def obtenerPinB(self):

return int(input("Ingrese la entrada del Pin B para la compuerta "+ self.obtenerEtiqueta()+"-->"))

Programa 10

class CompuertaUnaria(CompuertaLogica):

def

CompuertaLogica

self.pin = None

def obtenerPin(self):

init

(self,n):

init

(self,n)

return int(input("Ingrese la entrada del Pin para la compuerta "+ self.obtenerEtiqueta()+"-->"))

El Programa 9 y el Programa 10 implementan estas dos clases. Los constructores en ambas clases comienzan con una llamada explícita al constructor de la clase madre utilizando el

método

Al crear una instancia de la clase CompuertaBinaria, primero queremos inicializar cualesquiera ítems de datos heredados de CompuertaLogica. En este caso, eso significa la etiqueta para la compuerta. A continuación, el constructor agrega las dos líneas de entrada (pinA y pinB). Éste es un patrón muy común que debe usarse siempre al crear jerarquías de clases. Los constructores de las clases hija deben llamar a los constructores de las clases madre y luego ocuparse de sus propios datos distintivos.

Python también tiene una función llamada super que se puede usar en lugar de nombrar explícitamente la clase madre. Éste es un mecanismo más general, y es ampliamente utilizado especialmente cuando una clase tiene más de una clase madre. Sin embargo, esa opción no se discutirá en esta introducción.

Por ejemplo, en nuestro ejemplo anterior,

init

de la madre.

podría reemplazarse por

CompuertaLogica

init

super(CompuertaUnaria,self)

(self,n)

init

(n).

El único comportamiento que añade la clase CompuertaBinaria es la capacidad de obtener los valores de las dos líneas de entrada. Dado que estos valores vienen de algún lugar externo, simplemente le pediremos al usuario a través de una instrucción input que los proporcione. La misma implementación se usa para la clase CompuertaUnaria excepto que sólo hay una línea de entrada.

Ahora que tenemos una clase general para las compuertas dependiendo del número de líneas de entrada, podemos construir compuertas específicas que tengan un comportamiento único. Por ejemplo, la clase CompuertaAND será una subclase de CompuertaBinaria, ya que las compuertas AND tienen dos líneas de entrada. Como antes, la primera línea del constructor invoca al constructor de la clase madre (CompuertaBinaria), que a su vez llama al constructor de su clase madre (CompuertaLogica). Note que la clase CompuertaAND no proporciona ningún dato nuevo, ya que hereda dos líneas de entrada, una línea de salida y una etiqueta.

Programa 11

class CompuertaAND(CompuertaBinaria):

def

init

(self,n):

CompuertaBinaria

init

(self,n)

def ejecutarLogicaDeCompuerta(self):

a = self.obtenerPinA()

b = self.obtenerPinB() if a==1 and b==1:

return 1

else:

return 0

Lo único que CompuertaAND necesita agregar es el comportamiento específico que realiza la operación booleana que se describió anteriormente. Éste es el lugar donde podemos proporcionar el método ejecutarLogicaDeCompuerta. Para una compuerta AND, este método debe obtener primero los dos valores de entrada y luego devuelve 1 sólo si ambos valores de entrada son 1. La clase completa se muestra en el Programa 11.

Podemos mostrar la clase CompuertaAND en acción creando una instancia y pidiéndole que calcule su salida. La sesión siguiente muestra un objeto CompuertaAND, c1, que tiene una etiqueta interna "C1". Cuando invocamos el método obtenerSalida, el objeto debe llamar primero a su método ejecutarLogicaDeCompuerta que a su vez consulta las dos líneas de entrada. Una vez que se proporcionan los valores, se muestra la salida correcta.

>>> c1 = CompuertaAND("C1") >>> c1.obtenerSalida()

Ingrese la entrada del Pin A para la compuerta C1-->1 Ingrese la entrada del Pin B para la compuerta C1-->0

0

El mismo desarrollo se puede hacer para las compuertas OR y las compuertas NOT. La clase CompuertaOR también será una subclase de CompuertaBinaria y la clase CompuertaNOT extenderá la clase CompuertaUnaria. Ambas clases tendrán que proporcionar sus propias funciones ejecutarLogicaDeCompuerta, ya que ése será su comportamiento específico.

PRÁCTICA

#3.1

Compuertas OR y NOT

De acuerdo a lo visto en los códigos anteriores, se solicita se creen las clases CompuertaOR y CompurtaNOT, a través del proceso de herencia que se ha establecido en el transcurso de este capito del manual de prácticas.

Ahora que tenemos las compuertas básicas funcionando, podemos centrar nuestra atención en la construcción de circuitos. Para crear un circuito, necesitamos conectar las compuertas juntas, la salida de una fluirá hacia la entrada de otra. Para ello, implementaremos una nueva clase llamada Conector.

La clase Conector no residirá en la jerarquía de las compuertas. Sin embargo, sí usará la jerarquía de ellas por el hecho que cada conector tendrá dos compuertas, una en cada extremo (ver la Figura ).

Esta relación es muy importante en la programación orientada a objetos. Se llama la Relación TIENE-UN(A). Recuerde que antes usamos la frase ―Relación ES-UN(A)‖ para decir que una clase hija está relacionada con una clase madre, por ejemplo CompuertaUnaria ES-UNA CompuertaLogica.

madre, por ejemplo CompuertaUnaria ES-UNA CompuertaLogica. Ahora, con la clase Conector, decimos que un Conector

Ahora, con la clase Conector, decimos que un Conector TIENE-UNA CompuertaLogica lo cual significa que los conectores tendrán instancias de la clase CompuertaLogica dentro de ellos, pero no forman parte de la jerarquía. Al diseñar clases, es muy importante distinguir entre aquéllas que tienen la relación ES-UN(A) (lo cual requiere herencia) y aquéllas que tienen relaciones TIENE-UN(A) (sin herencia).

El Programa 12 muestra la clase Conector. Las dos instancias de compuertas dentro de cada objeto conector se referirán como deCompuerta y aCompuerta, reconociendo que los valores de los datos ―fluirán‖ desde la salida de una compuerta a una línea de entrada de la siguiente. El llamado a asignarProximoPin es muy importante para realizar conexiones (ver el Programa 13). Necesitamos agregar este método a nuestras clases de compuertas para que cada aCompuerta pueda elegir la línea de entrada adecuada para la conexión.

Programa 12

class Conector:

def

init

(self,

deComp, aComp):

self.deCompuerta = deComp self.aCompuerta = aComp

aComp.asignarProximoPin(self)

def obtenerFuente(self):

return self.deCompuerta

def obtenerDestino(self):

return self.aCompuerta

En la clase CompuertaBinaria, para compuertas con dos posibles líneas de entrada, el conector debe conectarse a una sola línea. Si ambas están disponibles, elegiremos pinA de forma predeterminada. Si pinA ya está conectado, entonces elegiremos pinB. No es posible conectarse a una compuerta sin líneas de entrada disponibles.

Programa 13

def asignarProximoPin(self,fuente):

if self.pinA == None:

self.pinA = fuente else:

if self.pinB == None:

self.pinB = fuente else:

raise RuntimeError("Error: NO HAY PINES DISPONIBLES")

Ahora es posible obtener entradas desde dos lugares: externamente, como antes, y desde la salida de una compuerta que está conectada a esa línea de entrada. Esto requiere un cambio en los métodos obtenerPinA y obtenerPinB (ver el Programa 14). Si la línea de entrada no está conectada a nada (None), entonces se pide al usuario que ingrese el valor externamente como antes. Sin embargo, si hay una conexión, se accede a ella y se consulta el valor de salida de deCompuerta. Esto, a su vez, hace que esa compuerta procese su lógica. Se continúa este proceso hasta que todas las entradas estén disponibles y el valor de salida final se convierta en la entrada requerida para la compuerta en cuestión. En cierto sentido, el circuito opera hacia atrás para encontrar la entrada necesaria para finalmente producir la salida.

Programa 14

def obtenerPinA(self):

if self.pinA == None:

->")

return input("Ingrese la entrada del Pin A para la compuerta " + self.obtenerNombre()+"-

else:

return self.pinA.obtenerFuente().obtenerSalida()

El siguiente fragmento construye el circuito mostrado anteriormente en esta sección:

el circuito mostrado anteriormente en esta sección: >>> c1 = CompuertaAND("C1")

>>> c1 = CompuertaAND("C1") >>> c2 = CompuertaAND("C2") >>> c3 = CompuertaOR("C3") >>> c4 = CompuertaNOT("C4") >>> c1 = Conector(c1,c3)

>>> c2 = Conector(c2,c3) >>> c3 = Conector(c3,c4)

Las salidas de las dos compuertas AND (c1 y c2) están conectadas a la compuerta OR (c3) y la salida de esta última está conectada a la compuerta NOT (c4). La salida de la compuerta NOT es la salida de todo el circuito. Por ejemplo:

>>> c4.obtenerSalida() Ingrese la entrada del Pin A para la compuerta C1-->0 Ingrese la entrada del Pin B para la compuerta C1-->1 Ingrese la entrada del Pin A para la compuerta C2-->1 Ingrese la entrada del Pin B para la compuerta C2-->1

0

PRÁCTICA

#3.2

Compuertas NAND, NOR, XOR y XNOR

Compuertas NAND, NOR, XOR y XNOR

Generar las clases necesarias para poder trabajar con todas las compuertas lógicas binarias y unarias, deberán de funcionar de acuerdo a la lógica establecida a través del desarrollo de este manual.

PRÁCTICA

#3.2

Compuertas cuaternarias

Existen otro tipo de familia de compuertas además de las unarias y las binarias, son conocidas como compuertas cuaternarias, es decir de cuatro entradas, y estas son construidas en su interior por compuertas binarias, cree las clases y herencias necesarias para poder usar este tipo de compuertas en la construcción de circuitos.

PRÁCTICA

#3.3

Circuito

Genera con lo aprendido en el curso da la solución para el siguiente circuito, deberás de construir la representación física del circuito creado para cumplir la tabla de verdad de la función propuesta y el código Python.

Tabla de Verdad

A

B

C

D

F

0

0

0

0

1

0

0

0

1

0

0

0

1

0

0

0

0

1

1

1

0

1

0

0

0

0

1

0

1

1

0

1

1

0

1

0

1

1

1

0

1

0

0

0

0

1

0

0

1

1

1

0

1

0

0

1

0

1

1

1

1

1

0

0

1

1

1

0

1

1

1

1

1

0

1

1

1

1

1

1

F=a´b´c´ + ab +ad +bc´d + b´cd + bcd´

3.4 Unidad 4

Listas Enlazadas

En esta unidad, nos dedicaremos a construir nuestras propias listas, que consistirán de cadenas de objetos enlazadas mediante referencias.

Si bien Python ya cuenta con sus propias listas, las listas enlazadas que implementaremos en esta unidad nos resultarán también útiles.

En primer lugar, definiremos una clase muy simple,

tendrá sólo dos atributos:

servirá para poner una referencia al siguiente vagón. Además, como siempre, implementaremos el constructor y el método imprimir el contenido del nodo.

, que

para poder

Nodo
Nodo

, que se comportará como un vagón:

prox str
prox
str
dato
dato

, que servirá para almacenar cualquier información, y

class Nodo(object):

def

init

(self,

dato=None, prox = None):

self.dato = dato

self.prox = prox

def

str

(self):

return str(self.dato)

Ejecutamos este código:

>>> v3=Nodo("Bananas")

>>> v2=Nodo("Peras", v3)

>>> v1=Nodo("Manzanas", v2)

>>> print v1

Manzanas

>>> print v2

Peras

>>> print v3

Bananas

Con esto hemos generado la estructura de la Figura 4.1.

El atributo prox de v3 nuestra estructura.
El atributo
prox
de
v3
nuestra estructura.

tiene una referencia nula, lo que indica que

v3
v3

es el último vagón de

Hemos creado una lista en forma manual. Si nos interesa recorrerla, podemos hacer lo siguiente:

def verLista ( nodo ) : """ Recorre todos los nodos a través de sus

def verLista(nodo):

""" Recorre todos los nodos a través de sus enlaces,

mostrando sus contenidos. """

# cicla mientras nodo no es None

while nodo:

# muestra el dato

print nodo

# ahora nodo apunta a nodo.prox

nodo = nodo.prox

>>> verLista(v1)

Manzanas

Peras

Bananas

Es interesante notar que la estructura del recorrido de la lista es el siguiente:

Se le pasa a la función sólo la referencia al primer nodo.

El resto del recorrido se consigue siguiendo las cadena de referencias dentro de los nodos.

Si se desea desenganchar un vagón del medio de la lista, alcanza con cambiar el enganche:

>>> v1.prox=v3

>>> verLista(v1)

Manzanas

Bananas

>>> v1.prox = None

>>> verLista(v1)

Manzanas

De esta manera también se pueden generar estructuras impensables.

? La representación es finita y sin embargo en este

caso

Hemos creado una lista infinita, también llamada lista circular.

Caminos

En una lista cualquiera, como las vistas antes, si seguimos las flechas dadas por las referencias, obtenemos un camino en la lista.

Los caminos cerrados se denominan ciclos. Son ciclos, por ejemplo, la autorreferencia

de

ADVERTENCIA Las listas circulares no tienen nada de malo en sí mismas, mientras su representación sea finita. El problema, en cambio, es que debemos tener mucho cuidado al escribir programas para recorrerlas, ya que el recorrido debe ser acotado (por ejemplo no habría problema en ejecutar un programa que liste los 20 primeros nodos de una lista circular).

Cuando una función recibe una lista y el recorrido no está acotado por programa, se debe aclarar en su precondición que la ejecución de la misma terminará sólo si la lista no contiene ciclos. Ése es el caso de la función verLista(v1).

Referenciando el principio de la lista

Una cuestión no contemplada hasta el momento es la de mantener una referencia a la lista

completa. Por ahora para nosotros la lista es la colección de

no nos hace

de

lista como la colección de nodos a tratar (en las listas de Python,

perder la referencia a

Para ello lo que haremos es asociar una referencia al principio de la lista, que

llamaremos

principio de

, y que mantendremos independientemente de cuál sea el nodo que está al

¿Qué sucede si escribimos

verLista(v1)

v1.prox = v1

no termina más.

v1
v1

a

v1
v1

, como así también una flecha de

v1
v1

a

v2
v2

seguida de una flecha de

v2
v2

a

v1 .
v1
.

nodos que se enlazan a partir

continuar con el resto de la

v1
v1

. Sin embargo puede suceder que querramos borrar a

lista
lista

la lista:

lista
lista

).

v1 y del lista[0]
v1
y
del lista[0]

>>> v3=Nodo("Bananas")

>>> v2=Nodo("Peras", v3)

>>> v1=Nodo("Manzanas", v2)

>>> lista=v1

>>> verLista(lista)

Manzanas

Peras

Bananas

Ahora sí estamos en condiciones de borrar el primer elemento de la lista sin perder la identidad de la misma:

Peras

Bananas

Vamos a ver ahora una nueva manera de definir datos: por las operaciones que tienen y por lo que tienen que hacer esas operaciones (cuál es el resultado esperado de esas operaciones). Esa manera de definir datos se conoce como tipos abstractos de datos o TADs.

Lo novedoso de este enfoque respecto del anterior es que en general se puede encontrar más de una representación mediante tipos concretos para representar el mismo TAD, y que se puede elegir la representación más conveniente en cada caso, según el contexto de uso.

Los programas que los usan hacen referencia a las operaciones que tienen, no a la representación, y por lo tanto ese programa sigue funcionando si se cambia la representación.

Dentro del ciclo de vida de un TAD hay dos fases: la programación del TAD y la construcción de los programas que lo usan.

Durante la fase de programación del TAD, habrá que elegir una representación, y luego programar cada uno de los métodos sobre esa representación.

Durante la fase de construcción de los programas, no será relevante para el programador que utiliza el TAD cómo está implementado, sino únicamente los métodos que posee.

La Clase ListaEnlazada

Basándonos en los nodos implementados anteriormente, pero buscando deslindar al programador que desea usar la lista de la responsabilidad de manipular las referencias, definiremos ahora la clase ListaEnlazada, de modo tal que no haya que operar mediante las referencias internas de los nodos, sino que se lo pueda hacer a través de operaciones de lista.

Más allá de la implementación en particular, se podrá notar que implementaremos los mismos métodos de las listas de Python, de modo que más allá del funcionamiento interno, ambas serán listas.

clase

ListaEnlazada.

Definimos

a

continuación

las

operaciones

que

inicialmente

deberá

cumplir

la

str ,
str
,

para mostrar la lista.

len ,
len
,

para calcular la longitud de la lista.

append(x)

,

para agregar un elemento al final de la lista.

insert(i, x)

, para agregar el elemento x en la posición i (levanta una excepción si la

posición i es inválida).

remove(x)

,

para eliminar la primera aparición de x en la lista (levanta una excepción

si x no está).

pop([i])

, para borrar el elemento que está en la posición i y devolver su valor. Si no se

especifica el valor de i, pop() elimina y devuelve el elemento que está en el último lugar de la lista (levanta una excepción si se hace referencia a una posición no válida de la lista).

index(x)

, devuelve la posición de la primera aparición de x en la lista (levanta una

excepción si xno está).

Más adelante podrán agregarse a la lista otros métodos que también están implementados por las listas de Python.

Valen ahora algunas consideraciones más antes de empezar a implementar la clase:

Por lo dicho anteriormente, es claro que la lista deberá tener como atributo la referencia al primer nodo que la compone.

Como vamos a incluir un método

cada vez que se lo llame, para contar cuántos elementos tiene, alcanza con agregar un atributo más (la longitud de la lista), que se inicializa en 0 cuando se crea la lista vacía, se incrementa en 1 cada vez que se agrega un elemento y se decrementa en 1 cada vez que se borra un elemento.

Por otro lado, como vamos a incluir todas las operaciones de listas que sean necesarias para operar con ellas, no es necesario que la clase Nodo esté disponible para que otros programadores puedan modificar (y romper) las listas a voluntad usando operaciones de nodos. Para eso incluiremos la clase Nodo de manera privada (es decir oculta), de modo que la podamos usar nosotros como dueños (fabricantes) de la clase, pero no cualquier programador que utilice la lista.

, consideramos que no tiene sentido recorrer la lista

len
len

Python tiene una convención para hacer que atributos, métodos o clases dentro de una clase dada no puedan ser usados por los usuarios, y sólo tengan acceso a ellos quienes programan la clase: su nombre tiene que empezar con un guión bajo y terminar sin guión bajo. Así que para hacer que los nodos sean privados, nombraremos a esa clase como _Nodo, y la dejaremos tal como hasta ahora.

Empezaremos escribiendo la clase con su constructor

class ListaEnlazada(object):

" Modela una lista enlazada, compuesta de Nodos. "

def

init

(self):

""" Crea una lista enlazada vacía. """

# prim: apuntará al primer nodo -None con la lista vacía

self.prim = None

# len: longitud de la lista - 0 con la lista vacía

self.len = 0

Nuestra estructura ahora será como la representada por la Figura

de la lista - 0 con la lista vacía self . len = 0 Nuestra estructura

PRÁCTICA

#4.1

Lista enlazada

Deberás de realizar el código necesario para los métodos

str

y

len

para la lista.

Eliminar un elemento de una posición

Analizaremos a continuación pop([i]), que borra el elemento que está en la posición i y devuelve su valor. Si no se especifica el valor de i, pop() elimina y devuelve el elemento que está en el último lugar de la lista. Por otro lado, levanta una excepción si se hace referencia a una posición no válida de la lista.

Dado que se trata de una función con cierta complejidad, separaremos el código en las diversas consideraciones a tener en cuenta.

Si la posición es inválida (i menor que 0 o mayor o igual a la longitud de la lista), se considera error y se levanta la excepción ValueError.

Esto se resuelve con este fragmento de código:

# Verificación de los límites

if (i < 0) or (i >= self.len):

raise IndexError("Índice fuera de rango")

i toma la última posición de la lista. Esto se resuelve con este

i

toma la última posición de la lista. Esto se resuelve con este

Si no se indica posición, fragmento de código:

# Si no se recibió i, se devuelve el último.

if i == None:

i = self.len - 1

Cuando la posición es 0 se trata de un caso particular, ya que en ese caso, además de borrar el nodo, hay que cambiar la referencia de self.prim para que apunte al nodo siguiente. Es decir, pasar de

self.prim → nodo0 → nodo1 a self.prim → nodo1.

Esto se resuelve con este fragmento de código:

# Caso particular, si es el primero,

# hay que saltear la cabecera de la lista

if i == 0:

dato = self.prim.dato

self.prim = self.prim.prox

Vemos ahora el caso general:

Mediante un ciclo, se deben ubicar los nodos npi - 1 y npi que están en las posiciones i − 1 e i de la lista, respectivamente, de modo de poder ubicar no sólo el nodo que se borrará, sino también estar en condiciones de saltear el nodo borrado en los enlaces de la lista. La lista debe pasar de contener el camino npi - 1 → npi → npi.prox a contener el camino npi-1 → npi.prox.

Nos basaremos un esquema muy simple (y útil) que se denomina máquina de parejas:

Si nuestra secuencia tiene la forma ABCDE, se itera sobre ella de modo de tener las parejas AB, BC, CD, DE. En la pareja XY, llamaremos a X el elemento anterior y a Y el elemento actual. En general estos ciclos terminan o bien cuando no hay más parejas que formar, o bien cuando el elemento actual cumple con una determinada condición.

En nuestro problema, tenemos la siguiente situación:

Las parejas son parejas de nodos.

Para avanzar en la secuencia se usa la referencia al próximo nodo de la lista.

La condición de terminación es siempre que la posición del nodo en la lista sea igual al valor buscado. En este caso particular no debemos preocuparnos por la terminación de la lista porque la validez del índice buscado ya fue verificada más arriba.

Esta es la porción de código correspondiente a la búsqueda:

n_ant = elf.prim

n_act = n_ant.prox

for pos in xrange(1, i):

n_ant = n_act

n_act = n_ant.prox

Al finalizar el ciclo, n_ant será una referencia al nodo i − 1 y n_act una referencia al nodo i.

Una vez obtenidas las referencias, se obtiene el dato y se cambia el camino según era necesario:

# Guarda el dato y elimina el nodo a borrar

dato = n_act.dato

n_ant.prox = n_act.prox

Finalmente, en todos los casos de éxito, se debe devolver el dato que contenía el nodo eliminado y decrementar la longitud en 1:

# hay que restar 1 de len

self.len -= 1

# y devolver el valor borrado

return dato

Finalmente, en el código 16.1 se incluye el código completo del método pop.

Eliminar un elemento por su valor

Análogamente se resuelve remove(self,x), que debe eliminar la primera aparición de x en la lista, o bien levantar una excepción si x no se encuentra en la lista.

Nuevamente, dado que se trata de un método de cierta complejidad, lo resolveremos por partes, teniendo en cuenta los casos particulares y el caso general.

def pop(self, i = None):

""" Elimina el nodo de la posición i, y devuelve el dato contenido.

Si i está fuera de rango, se levanta la excepción IndexError.

Si no se recibe la posición, devuelve el último elemento. """

# Si no se recibió i, se devuelve el último.

if i is None:

i = self.len - 1

# Verificación de los límites

if not (0 <= i < self.len):

raise IndexError("Índice fuera de rango")

# Caso particular, si es el primero,

# hay que saltear la cabecera de la lista

if i == 0:

dato = self.prim.dato

self.prim = self.prim.prox

# Para todos los demás elementos, busca la posición

else:

n_ant = self.prim

n_act = n_ant.prox

for pos in xrange(1, i):

n_ant = n_act

n_act = n_ant.prox

# Guarda el dato y elimina el nodo a borrar

dato = n_act.dato

n_ant.prox = n_act.prox

# hay que restar 1 de len

self.len -= 1

# y devolver el valor borrado

return dato

Los casos particulares son: la lista vacía, que es un error y hay que levantar una excepción; y el caso en el que x está en el primer nodo, en este caso hay que saltear el primer nodo desde la cabecera de la lista.

El fragmento de código que resuelve estos casos es:

if self.len == 0:

# Si la lista está vacía, no hay nada que borrar.

raise ValueError("Lista vacía")

# Caso particular, x esta en el primer nodo

elif self.prim.dato == x:

# Se descarta la cabecera de la lista

self.prim = self.prim.prox

El caso general también implica un recorrido con máquina de parejas, sólo que esta vez la condición de terminación es: o bien la lista se terminó o bien encontramos un nodo con el

valor (

x
x

) buscado.

# Obtiene el nodo anterior al que contiene a x (n_ant)

n_ant = self.prim

n_act = n_ant.prox

while n_act != None and n_act.dato != x:

n_ant = n_act

n_act = n_ant.prox

En este caso, al terminarse el ciclo será necesario corroborar si se terminó porque llegó al final de la lista, y de ser así levantar una excepción; o si se terminó porque encontró el dato, y de ser así eliminarlo.

# Si no se encontró a x en la lista, levanta la excepción

if n_act == None:

raise ValueError("El valor no ester en la lista.")

# Si encontró a x, debe pasar de n_ant -> n_x -> n_x.prox

# a n_ant -> n_x.prox

else:

n_ant.prox = n_act.prox

Finalmente, en todos los casos de éxito debemos decrementar en

el código 16.2 se incluye el código completo del método

Insertar nodos

1 remove .
1
remove
.

el valor de

self.len

. En

Debemos posición i
Debemos
posición
i

ahora

insert(i, x)

,

programar

(y levantar una excepción si la posición

que

i
i

debe

agregar

es inválida).

el

elemento

x
x

en

la

Veamos qué debemos tener en cuenta para programar esta función.

Si se intenta insertar en una posición menor que cero o mayor que la longitud de la lista debe levantarse una excepción.

def remove(self, x):

""" Borra la primera aparición del valor x en la lista.

Si x no está en la lista, levanta ValueError """

if self.len == 0:

# Si la lista está vacía, no hay nada que borrar.

raise ValueError("Lista vacía")

# Caso particular, x esta en el primer nodo

elif self.prim.dato == x:

# Se descarta la cabecera de la lista

self.prim = self.prim.prox

# En cualquier otro caso, hay que buscar a x

else:

# Obtiene el nodo anterior al que contiene a x (n_ant)

n_ant = self.prim

n_act = n_ant.prox

while n_act != None and n_act.dato != x:

n_ant = n_act

n_act = n_ant.prox

# Si no se encontró a x en la lista, levanta la excepción

if n_act == None:

raise ValueError("El valor no ester en la lista.")

# Si encontró a x, debe pasar de n_ant -> n_x -> n_x.prox

# a n_ant -> n_x.prox

else:

n_ant.prox = n_act.prox

# Si no levantó excepción, hay que restar 1 del largo

self.len -= 1

if (i > self.len) or (i < 0):

# error

raise IndexError("Posición inválida")

Para los demás casos, hay que crear un nodo, que será el

corresponda. Construimos un nodo

nuevo
nuevo

cuyo dato sea

x .
x
.

que se insertará en la posición que

#

Crea nuevo nodo, con x como dato:

nuevo = _Nodo(x)

Si se quiere insertar en la posición

0
0

, hay que cambiar la referencia de

self.prim

.

# Insertar al principio (caso particular)

if i == 0:

# el siguiente del nuevo pasa a ser el que era primero

nuevo.prox = self.prim

# el nuevo pasa a ser el primero de la lista

self.prim = nuevo

Para los demás casos, nuevamente será necesaria la máquina de parejas. Obtenemos el nodo anterior a la posición en la que queremos insertar.

# Insertar en cualquier lugar > 0

else:

# Recorre la lista hasta llegar a la posición deseada

n_ant = self.prim

for pos in xrange(1,i):

n_ant = n_ant.prox

# Intercala nuevo y obtiene n_ant -> nuevo -> n_ant.prox

nuevo.prox = n_ant.prox

n_ant.prox = nuevo

En todos los casos de éxito se debe incrementar en 1 la longitud de la lista.

# En cualquier caso, incrementar en 1 la longitud

# self.len += 1

En el Código se incluye el código resultante del método

insert .
insert
.

Si la posición es inválida, levanta IndexError """

if (i > self.len) or (i < 0):

0 # error

raise IndexError("Posición inválida")

# Crea nuevo nodo, con x como dato:

nuevo = _Nodo(x)

# Insertar al principio (caso particular)

if i == 0:

# el siguiente del nuevo pasa a ser el que era primero

nuevo.prox = self.prim

# el nuevo pasa a ser el primero de la lista

self.prim = nuevo

# Insertar en cualquier lugar > 0

else:

# Recorre la lista hasta llegar a la posición deseada

n_ant = self.prim

for pos in xrange(1,i):

n_ant = n_ant.prox

# Intercala nuevo y obtiene n_ant -> nuevo -> n_ant.prox

nuevo.prox = n_ant.prox

n_ant.prox = nuevo

# En cualquier caso, incrementar en 1 la longitud

PRÁCTICA

#4.2

Lista enlazada: append e index

Completar la clase

con los métodos que faltan:

e

ListaEnlazada

append

index

.

Otras listas enlazadas

Las listas presentadas hasta aquí son las listas simplemente enlazadas, que son sencillas y útiles cuando se quiere poder insertar o eliminar nodos de una lista en tiempo constante. Existen otros tipos de listas enlazadas, cada uno con sus ventajas y desventajas.

Listas doblemente enlazadas

Las listas doblemente enlazadas son aquellas en que los nodos cuentan no sólo con una referencia al siguiente, sino también con una referencia al anterior. Esto permite que la lista pueda ser recorrida en ambas direcciones.

En una lista doblemente enlazada, es posible, por ejemplo, eliminar un nodo, teniendo únicamente ese nodo, sin necesidad de saber también cuál es el anterior.

Entre las desventajas podemos mencionar que al tener que mantener dos referencias el código se vuelve más complejo, y también que ocupa más espacio en memoria.

16.5.2. Listas circulares

Las listas circulares, que ya fueron mencionadas al comienzo de esta unidad, son aquellas en las que el último nodo contiene una referencia al primero. Pueden ser tanto simplemente como doblemente enlazadas. Se las utiliza para modelar situaciones en las cuales los elementos no tienen un primero o un último, sino que forman una cadena infinita, que se recorre una y otra vez.

NOTA: Un ejemplo de uso de las listas circulares es dentro del kernel Linux. La mayoría de las listas utilizadas por este kernel son circulares, ya que la mayoría de los datos a los que se quiere acceder son datos que no tienen un orden en particular.

Por ejemplo, la lista de tareas que se están ejecutando es una lista circular. El scheduler del kernel permite que cada tarea utilice el procesador durante una porción de tiempo y luego pasa a la siguiente; y al llegar a la última vuelve a la primera, ya que la ejecución de tareas no se termina.

Iteradores

En la unidad anterior se hizo referencia a que todas las secuencias pueden ser recorridas mediante una misma estructura (for variable in secuencia), ya que todas implementan el

método especial

secuencia como corresponda.

Este método debe devolver un iterador capaz de recorrer la

iter

NOTA: Un iterador es un objeto que permite recorrer uno a uno los elementos almacenados en una estructura de datos, y operar con ellos.

En particular, en Python, los iteradores tienen que implementar un método next que debe devolver los elementos, de a uno por vez, comenzando por el primero. Y al llegar al final de la estructura, debe levantar una excepción de tipo StopIteration.

Es decir que las siguientes estructuras son equivalentes

for elemento in secuencia:

# hacer algo con elemento

iterador = iter(secuencia)

while True:

try:

elemento = iterador.next()

except StopIteration:

break

# hacer algo con elemento

En particular, si queremos implementar un iterador para la lista enlazada, la mejor solución

implica

método

, que implemente el

_IteradorListaEnlazada

crear

método , que implemente el _IteradorListaEnlazada crear una nueva clase, de la forma apropiada. ADVERTENCIA :

una

nueva

clase,

de la forma apropiada.

ADVERTENCIA: Utilizamos la notación de clase privada, utilizada también para la clase _Nodo, ya que si bien se devolverá el iterador cuando sea necesario, un programador externo no debería construir el iterador sin pasar a través de la lista enlazada.

Para inicializar la clase, lo único que se necesita es una referencia al primer elemento de la lista.

class _IteradorListaEnlazada(object):

" Iterador para la clase ListaEnlazada "

def

init

(self, prim):

""" Constructor del iterador.

prim es el primer elemento de la lista. """

self.actual = prim

A partir de allí, el iterador irá avanzando a través de los elementos de la lista mediante el

. Para verificar que no se haya llegado al final de la lista, se corroborará que la

referencia

método

next

self.actual

sea distinta de

None .
None
.

if self.actual == None:

raise StopIteration("No hay más elementos en la lista")

Una vez que se pasó la verificación, la primera llamada a next debe devolver el primer elemento, pero también debe avanzar, para que la siguiente llamada devuelva el siguiente elemento. Por ello, se utiliza la estructura guardar, avanzar, devolver.

# Guarda el dato

dato = self.actual.dato

# Avanza en la lista

self.actual = self.actual.prox

# Devuelve el dato

return dato

# Código 16.4: _IteradorListaEnlazada: Un iterador para la lista enl azada

class _IteradorListaEnlazada(object):

" Iterador para la clase ListaEnlazada "

def

init

(self, prim):

""" Constructor del iterador.

prim es el primer elemento de la lista. """

self.actual = prim

def next(self):

""" Devuelve uno a uno los elementos de la lista. """

if self.actual == None:

raise StopIteration("No hay más elementos en la lista")

# Guarda el dato

dato = self.actual.dato

# Avanza en la lista

self.actual = self.actual.prox

# Devuelve el dato

return dato

Finalmente, una vez que se tiene el iterador implementado, es necesario

modificar

la

clase

ListaEnlazada

para que devuelva el iterador cuando se llama al método

iter

.

def

iter

(self):

" Devuelve el iterador de la lista. "

return _IteradorListaEnlazada(self.prim)

Con todo esto será posible recorrer nuestra lista con la estructura a la que estamos acostumbrados.

>>> l = ListaEnlazada()

>>> l.append(1)

>>> l.append(3)

>>> l.append(5)

>>> for valor in l:

print valor

1

3

5

Pilas

Una pila es un TAD que tiene las siguientes operaciones (se describe también la acción que lleva adelante cada operación:

apilar: agrega un nuevo elemento a la pila.

desapilar: elimina el tope de la pila y lo devuelve. El elemento que se devuelve es siempre el último que se agrego.

es_vacia: devuelve True o False según si la pila está vacía o no.

init

:

inicializa una pila nueva, vacía.

El comportamiento de una pila se puede describir mediante la frase "Lo último que se apiló es lo primero que se usa", que es exactamente lo que uno hace con una pila (de platos por ejemplo): en una pila de platos uno sólo puede ver la apariencia completa del plato de arriba, y sólo puede tomar el plato de arriba (si se intenta tomar un plato del medio de la pila lo más probable es que alguno de sus vecinos, o él mismo, se arruine).

Como ya se dijo, al crear un tipo abstracto de datos, es importante decidir cuál será la representación a utilizar. En el caso de la pila, si bien puede haber más de una representación, por ahora veremos la más sencilla: representaremos una pila mediante una lista de Python.

Sin embargo, para los que construyen programas que usan un TAD vale el siguiente llamado de atención:

Pilas representadas por listas

Definiremos una clase Pila con un atributo, items, de tipo lista, que contendrá los ele-mentos de la pila. El tope de la pila se encontrará en la última posición de la lista, y cada vez que se apile un nuevo elemento, se lo agregará al final.

El método

(que representaremos por una lista vacía):

no recibirá parámetros adicionales, ya que deberá crear una pila vacía

init

class Pila:

""" Representa una pila con operaciones de apilar, desapilar y

verificar si está vacía. """

def

init

(self):

""" Crea una pila vacía. """

# La pila vacía se representa con una lista vacía

self.items=[]

El método apilar se implementará agregando el nuevo elemento al final de la lista:

def apilar(self, x):

""" Agrega el elemento x a la pila. """

# Apilar es agregar al final de la lista.

self.items.append(x)

desapilar , se usará el método pop de lista que hace exactamente lo

desapilar

,

se

usará el

método

pop

de lista que hace exactamente lo

Para implementar requerido: elimina

el último elemento de la lista y devuelve el valor del elemento eliminado.

Si la lista está vacía levanta una excepción, haremos lo mismo, pero cambiaremos el tipo de excepción, para no revelar la implementación.

def desapilar(self):

""" Devuelve el elemento tope y lo elimina de la pila.

Si la pila está vacía levanta una excepción. """

try:

return self.items.pop()

except IndexError:

raise ValueError("La pila está vacía")

Finalmente, el método para indicar si se trata de una pila vacía.

def es_vacia(self):

""" Devuelve True si la lista está vacía, False si no. """

return self.items == []

Construimos algunas pilas y operamos con ellas:

>>> from clasePila import Pila

>>> p = Pila()

>>> p.es_vacia()

True

>>> p.apilar(1)

>>> p.es_vacia()

False

>>> p.apilar(5)

>>> p.apilar("+")

>>> p.apilar(22)

>>> p.desapilar()

22

>>> p

<clasePila.Pila instance at 0xb7523f4c>

>>> q=Pila()

>>> q.desapilar()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "clasePila.py", line 24, in desapilar

raise ValueError("La pila está vacía")

ValueError: La pila está vacía

Uso de pila: calculadora científica

La famosa calculadora portátil HP-35 (de 1972) popularizó la notación polaca inversa (o

notación prefijo) para hacer cálculos sin necesidad de usar paréntesis. Esa notación, inventada por el lógico polaco Jan Lukasiewicz en 1920, se basa en el principio de que un operador

se escribirá

siempre se escribe a continuación de sus operandos. La operación

como ``5 3 - 8 +, que se interpretará como: "restar 3 de 5, y al resultado sumarle 8".

Es posible implementar esta notación de manera sencilla usando una pila de la siguiente manera, a partir de una cadena de entrada de valores separados por blancos:

(5 − 3) + 8

Mientras se lean números, se apilan.

En el momento en el que se detecta una operación binaria últimos números apilados, se ejecuta la operación indicada, apila.

Si la expresión está bien formada, tiene que quedar al final un único número en la pila (el resultado).

se desapilan los dos

resultado de esa operación se

+ , - , y el
+
,
-
,
y el
* ,
*
,
/
/

o

%
%

Los posibles errores son:

Queda más de un número al final (por ejemplo si la cadena de entrada fue

Ingresa algún caracter que no se puede interpretar ni como número ni como

"5 3" "5 3 &" )
"5 3"
"5 3 &"
)

),

una de las

cinco operaciones válidas (por ejemplo si la cadena de entrada fue

No hay suficientes operandos para realizar la operación (por ejemplo si la

entrada fue

"5 3 - +"

).

cadena de

La siguiente es la estrategia de resolución:

con la expresión a evaluar, podemos separar sus componentes utilizando el . Recorreremos luego la lista de componentes realizando las acciones

indicadas en el párrafo anterior, utilizando una pila auxiliar para operar. Si la expresión está

Dada una cadena

método

bien formada

(devolveremos

devolveremos el resultado, de lo contrario levantaremos una excepción

split()
split()
None
None

).

Veamos algunos casos de prueba:

El caso de una expresión que es sólo un número (es correcta):

>>> calculadora_polaca.main()

Ingrese la expresion a evaluar: 5

DEBUG: 5

DEBUG: apila 5.0

5.0

El caso en el que sobran operandos:

>>> calculadora_polaca.main()

Ingrese la expresion a evaluar: 4 5

DEBUG: 4

DEBUG: apila 4.0

DEBUG: 5

DEBUG: apila 5.0

DEBUG: error pila sobran operandos

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "calculadora_polaca.py", line 64, in main

print calculadora_polaca(elementos)

File "calculadora_polaca.py", line 59, in calculadora_polaca

raise ValueError("Sobran operandos")

ValueError: Sobran operandos

El caso en el que faltan operandos:

>>> calculadora_polaca.main()

Ingrese la expresion a evaluar: 4 %

DEBUG: 4

DEBUG: apila 4.0

DEBUG: %

DEBUG: desapila 4.0

DEBUG: error pila faltan operandos

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "calculadora_polaca.py", line 64, in main

print calculadora_polaca(elementos)

File "calculadora_polaca.py", line 37, in calculadora_polaca

raise ValueError("Faltan operandos")

ValueError: Faltan operandos

# calculadora_polaca.py: Una calculadora polaca inversa

#!/usr/bin/env python

#encoding: latin1

from clasePila import Pila

def calculadora_polaca(elementos):

""" Dada una lista de elementos que representan las componentes de

una expresión en notacion polaca inversa, evalúa dicha expresión.

Si la expresion está mal formada, levanta ValueError. """

p = Pila()

for elemento in elementos:

print "DEBUG:", elemento

# Intenta convertirlo a número

try:

numero = float(elemento)

p.apilar(numero)

print "DEBUG: apila ", numero

# Si no se puede convertir a número, debería ser un operando

except ValueError:

# Si no es un operando válido, levanta ValueError

if elemento not in "+-*/ %" or len(elemento) != 1:

raise ValueError("Operando inválido")

# Si es un operando válido, intenta desapilar y operar

try:

a1 = p.desapilar()

print "DEBUG: desapila ",a1

a2 = p.desapilar()

print "DEBUG: desapila ",a2

# Si hubo problemas al desapilar

except ValueError:

print "DEBUG: error pila faltan operandos"

raise ValueError("Faltan operandos")

if elemento == "+":

resultado = a2 + a1

elif elemento == "-":

resultado = a2 - a1

elif elemento == "*":

resultado = a2 * a1

elif elemento == "/":

resultado = a2 / a1

elif elemento == " %":

resultado = a2 % a1

print "DEBUG: apila ", resultado

p.apilar(resultado)

# Al final, el resultado debe ser lo único en la Pila

res = p.desapilar()

if p.esPilaVacia():

return res

else:

print "DEBUG: error pila sobran operandos"

raise ValueError("Sobran operandos")

def main():

expresion = raw_input("Ingrese la expresion a evaluar: ")

elementos = expresion.split()

print calculadora_polaca(elementos)

El caso de un operador inválido:

DEBUG: apila 4.0

DEBUG: 5

DEBUG: apila 5.0

DEBUG: &

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "calculadora_polaca.py", line 64, in main

print calculadora_polaca(elementos)

File "calculadora_polaca.py", line 26, in calculadora_polaca

raise ValueError("Operando inválido")

ValueError: Operando inválido

El caso de

4 % 5 :
4 % 5
:

>>> calculadora_polaca.main()

Ingrese la expresion a evaluar: 4 5 %

DEBUG: 4

DEBUG: apila 4.0

DEBUG: 5

DEBUG: apila 5.0

DEBUG: %

DEBUG: desapila 5.0

DEBUG: desapila 4.0

DEBUG: apila 4.0

4.0

El caso de

(4 + 5) * 6

:

DEBUG: 5

DEBUG: apila 5.0

DEBUG: +

DEBUG: desapila 5.0

DEBUG: desapila 4.0

DEBUG: apila 9.0

DEBUG: 6

DEBUG: apila 6.0

DEBUG: *

DEBUG: desapila 6.0

DEBUG: desapila 9.0

DEBUG: apila 54.0

54.0

El caso de

4 * (5 + 6)

:

>>> calculadora_polaca.main()

Ingrese la expresion a evaluar: 4 5 6 + *

DEBUG: 4

DEBUG: apila 4.0

DEBUG: 5

DEBUG: apila 5.0

DEBUG: 6

DEBUG: apila 6.0

DEBUG: +

DEBUG: desapila 6.0

DEBUG: desapila 5.0

DEBUG: apila 11.0

DEBUG: *

DEBUG: desapila 11.0

DEBUG: desapila 4.0

DEBUG: apila 44.0

44.0

El caso de (4 + 5) * (3 + 8):

>>> calculadora_polaca.main()

Ingrese la expresion a evaluar: 4 5 + 3 8 + *

DEBUG: 4

DEBUG: apila 4.0

DEBUG: 5

DEBUG: apila 5.0

DEBUG: +

DEBUG: desapila 5.0

DEBUG: desapila 4.0

DEBUG: apila 9.0

DEBUG: 3

DEBUG: apila 3.0

DEBUG: 8

DEBUG: apila 8.0

DEBUG: +

DEBUG: desapila 8.0

DEBUG: desapila 3.0

DEBUG: apila 11.0

DEBUG: *

DEBUG: desapila 11.0

DEBUG: desapila 9.0

DEBUG: apila 99.0

99.0

PRÁCTICA

#4.3

Pilas

Realizar un programa que de una expresión regular la transforme a una expresión de tipo polaca para introducirla al programa trabajo en clases.

Colas

Todos sabemos lo que es una cola. Más aún, ¡estamos hartos de hacer colas!

El TAD cola modela precisamente ese comportamiento: el primero que llega es el primero en ser atendido, los demás se van encolando hasta que les toque su turno.

Sus operaciones son:  init : inicializa una cola nueva, vacía.  encolar : agrega
Sus operaciones son:
init
:
inicializa una cola nueva, vacía.
encolar
: agrega un nuevo elemento al final de la cola.
desencolar
: elimina el primero de
la cola y lo devuelve.
es_vacia
:
devuelve
True
o False
según si la cola está vacía o no.

Colas implementadas sobre listas

Al momento de realizar una implementación de una representamos a las colas? Veamos, en primer lugar, listas de Python, como hicimos con la Pila.

Definiremos una clase elementos de la cola. El

cada vez que encole un nuevo elemento, se lo agregará al final.

El método

(que representaremos por una lista vacía):

Cola
Cola

, deberemos preguntarnos ¿C6mo

si podemos implementar colas usando

Cola
Cola

los

primero de la cola se encontrará en la primera posición de la lista, y

de

atributo,

items ,
items
,

con

un

tipo

lista,

que

contendrá

init

no recibirá parámetros adicionales, ya que deberá crear una cola vacía

class Cola:

""" Representa a una cola, con operaciones de encolar y desencolar.

El primero en ser encolado es también el primero en ser desencolado.

"""

def

init

(self):

""" Crea una cola vacía. """

# La cola vacía se representa por una lista vacía

El método

self.items=[]

se implementará agregando el nuevo elemento al final de la lista:representa por una lista vacía El método self .items = [] def encolar ( self ,

def encolar(self, x):

""" Agrega el elemento x como último de la cola. """

self.items.append(x)

Para implementar

valor del elemento eliminado, utilizaremos nuevamente el método

desencolar

, se eliminará el primer elemento de la lista y se devolverá el

, pero en este caso le

pop
pop

pasaremos la posición

vacía se levantará una excepción.

0
0

, para que elimine el primer elemento, no el último. Si la cola está

def desencolar(self):

""" Elimina el primer elemento de la cola y devuelve su

valor. Si la cola está vacía, levanta ValueError. """

try:

return self.items.pop(0)

except:

raise ValueError("La cola está vacía")

Por último, el método

es_vacia

, que indicará si la cola está o no vacía.

def es_vacia(self):

""" Devuelve True si la cola esta vacía, False si no."""

return self.items == []

Veamos una ejecución de este código:

>>> from claseCola import Cola

>>> q = Cola()

>>> q.es_vacia()

True

>>> q.encolar(1)

>>> q.encolar(2)

>>> q.encolar(5)

>>> q.es_vacia()

False

>>> q.desencolar()

1

>>> q.desencolar()

2

>>> q.encolar(8)

>>> q.desencolar()

5

>>> q.desencolar()

8

>>> q.es_vacia()

True

>>> q.desencolar()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "claseCola.py", line 24, in desencolar

raise ValueError("La cola está vacía")

ValueError: La cola está vacía

Colas y listas enlazadas

En la unidad anterior vimos la clase

inserción en la primera posición en tiempo constante, pero el líneal.

Sin embargo, como ejercicio, se propuso mejorar el

que apunte al último nodo, de modo de poder agregar elementos en tiempo constante.

ListaEnlazada

.

La

clase presentada ejecutaba la

se había convertido en

append
append
append
append

, agregando un nuevo atributo

Si

esas

mejoras

estuvieran

hechas,

cambiar

nuestra

clase

Cola
Cola

para

que

utilice

 

la

ListaEnlazada

sería tan simple como cambiar el constructor, para que en lugar de

construir una lista de Python construyera una lista enlazada.

 

class Cola:

""" Cola implementada sobre lista enlazada"""

def

init

(self):

""" Crea una cola vacía. """

# La cola se representa por una lista enlazada vacía.

self.items = claseListaEnlazadaConUlt.ListaEnlazada()

Sin embargo, una

que también podemos implementar una clase

Cola
Cola

es bastante más sencilla que una

Cola
Cola

por lo

utilizando las técnicas de referencias, que

ListaEnlazadaConUlt

,

se vieron en las listas enlazadas.

Planteamos otra solución posible para obtener una cola que sea eficiente tanto al encolar como al desencolar, utilizando los nodos de las listas enlazadas, y solamente implementaremos insertar al final y remover al principio.

Para ello, la cola deberá tener dos atributos,

momento deberán apuntar al primer y último nodo de la invariantes de esta cola.

En primer lugar los crearemos vacíos, ambos referenciando a

self.primero y self.ultimo
self.primero
y
self.ultimo
None .
None
.

, que en todo

cola, es decir que serán los

def

init

(self):

""" Crea una cola vacía. """

# En el primer momento, tanto el primero como el último son None

self.primero = None

self.ultimo = None

Al momento de encolar, hay dos situaciones a tener en cuenta.

Si

la

cola

está

vacía (es

decir, self.ultimo es None),

tanto self.primero como self.ultimo deben pasar a referenciar al nuevo nodo, ya que este nodo será a la vez el primero y el último. Si ya había nodos en la cola, simplemente hay que agregar el nuevo a continuación del último y actualizar la referencia de self.ultimo.

El código resultante es el siguiente.

def encolar(self, x):

""" Agrega el elemento x como último de la cola. """

nuevo = Nodo(x)

# Si ya hay un último, agrega el nuevo y cambia la referencia.

if self.ultimo:

self.ultimo.prox = nuevo

self.ultimo = nuevo

# Si la cola estaba vacía, el primero es también el último.

else:

self.primero = nuevo

self.ultimo = nuevo

Al momento de desencolar, será necesario verificar que la cola no esté vacía, y de ser así

almacena el valor del primer nodo de la al siguiente elemento.

Nuevamente hay un caso particular a tener en cuenta y es el que sucede cuando luego de

eliminar el primer nodo de la

referencia de

cola, la cola queda vacía. En este caso, además de actualizar la

cola y luego se avanza la referencia

levantar una excepción. Si la cola no está vacía, se

self.primero

self.primero

, también hay que actualizar la referencia de

self.ultimo

.

def desencolar(self):

""" Elimina el primer elemento de la cola y devuelve su

valor. Si la cola está vacía, levanta ValueError. """

# Si hay un nodo para desencolar

if self.primero:

valor = self.primero.dato

self.primero = self.primero.prox

# Si después de avanzar no quedó nada, también hay que

# eliminar la referencia del último.

if not self.primero:

self.ultimo = None

return valor

else:

raise ValueError("La cola está vacía")

Finalmente,

si

saber

vacía,

self.primero o self.ultimo
self.primero
o self.ultimo

para

si

la

cola

está

referencian a

None .
None
.

es

posible

verificar

tanto

def es_vacia(self):

""" Devuelve True si la cola esta vacía, False si no."""

return self.items == []

Una vez implementada toda la interfaz de la cola, podemos probar el TAD resultante:

>>> from claseColaEnlazada import Cola

>>> q = Cola()

>>> q.es_vacia()

True

>>> q.encolar("Manzanas")

>>> q.encolar("Peras")

>>> q.encolar("Bananas")

>>> q.es_vacia()

False

>>> q.desencolar()

'Manzanas'

>>> q.desencolar()

'Peras'

>>> q.encolar("Guaraná")

>>> q.desencolar()

'Bananas'

>>> q.desencolar()

'Guaraná'

>>> q.desencolar()

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

File "claseColaEnlazada.py", line 42, in desencolar

raise ValueError("La cola está vacía")

ValueError: La cola está vacía

PRÁCTICA

#4.4

Colas

Hace un montón de años había una viejísma sucursal del correo en la vereda impar de Av. de Mayo al 800 que tenía un cartel que decía "No se recibirán más de 5 cartas por persona". O sea que la gente entregaba sus cartas (hasta la cantidad permitida) y luego tenía que volver a hacer la cola si tenía más cartas para despachar.

Modelar una cola de correo generalizada, donde en la inicialización se indica la cantidad (no necesariamente 5) de cartas que se reciben por persona.

Arboles

Árboles Binarios

Los árboles binarios tienen sólo dos ramas por cada nodo. Son particularmente interesantes porque se pueden utilizar para mantener un orden entre los elementos: Cualquier elemento menor está en la rama izquierda, mientras que cualquier elemento mayor está en la rama derecha

que cualquier elemento mayor está en la rama derecha Este tipo de estructura presenta una ventaja

Este tipo de estructura presenta una ventaja con respecto a las listas enlazadas: En el peor de los casos, un elemento en el árbol se encuentra en una cantidad de pasos igual a la profundidad del mismo, mientras que en una lista ordenada el peor de los casos se encuentra en una cantidad de pasos igual a la cantidad de elementos.

Árbol binario de búsqueda en python

Para generar un árbol binario, además del elemento vamos a utilizar una función que permita determinar el orden de los elementos,

class ArbolBinario:

def

init

(

self, elemento, esMenorFunction = lambda x,y: x < y ):

self.derecha = None self.izquierda =