Ver índice de contenidos del libro

14.5. Creando clases más complejas

Nos contratan para diseñar una clase para evaluar la relación calidad-precio de diversos hoteles. Nos dicen que los atributos que se cargarán de los hoteles son: nombre, ubicación, puntaje obtenido por votación, y precio, y que además de guardar hoteles y mostrarlos, debemos poder compararlos en términos de sus valores de relación calidad-precio, de modo tal que x < y signifique que el hotel x es peor en cuanto a la relación calidad-precio que el hotel y, y que dos hoteles son iguales si tienen la misma relación calidad-precio. La relación calidad-precio de un hotel la definen nuestros clientes como (puntaje^2) ∗ 10 / precio.

Además, y como resultado de todo esto, tendremos que ser capaces de ordenar de menor a mayor una lista de hoteles, usando el orden que nos acaban de definir.

Averiguamos un poco más respecto de los atributos de los hoteles:

  • El nombre y la ubicación deben ser cadenas no vacías.
  • El puntaje debe ser un número (sin restricciones sobre su valor)
  • El precio debe ser un número distinto de cero.

Empezamos diseñar a la clase:

El método __init__:

  • Creará objetos de la clase Hotel con los atributos que se indicaron (nombre, ubicación, puntaje, precio).
  • Los valores por omisión para la construcción son: puntaje en 0, precio en float("inf") (infinito), nombre y ubicación en * (el precio muy alto sirve para que si no se informa el precio de un hotel, se asuma el mayor valor posible.
  • Necesitamos validar que puntaje y precio sean números (utilizaremos la función es_numero que ya se usó en el caso de los puntos). Cuando un precio viene en cero se reemplaza su valor por float("inf") (de modo de asegurar que el precio nunca quede en cero).
  • Necesitamos validar que nombre y ubicación sean cadenas no vacías (para lo cual tenemos que construir una función es_cadena_no_vacia).
  • Cuando los datos no satisfagan los requisitos se levantará una excepción TypeError.

Contará con un método __str__ para mostrar a los hoteles mediante una cadena del estilo:

"Hotel City de Mercedes - Puntaje: 3.25 - Precio: 78 pesos".

Respecto a la relación de orden entre hoteles, la clase deberá poder contar con los métodos necesarios para realizar esas comparaciones y para ordenar una lista de hoteles.

Casi todas las tareas, podemos realizarlas con los temas vistos para la creación de la clase Punto. Para el último ítem deberemos introducir nuevos métodos especiales.

Ejercicio 14.1. Escribir la función es_cadena_no_vacia(valor) que decide si un valor cualquiera es una cadena no vacía o no, e incluirla en el módulo validaciones.

El fragmento inicial de la clase programada en Python queda así:

class Hotel(object):
    """ Hotel: sus atributos son: nombre, ubicacion, puntaje y precio."""
 
    def __init__(self, nombre = '*', ubicacion = '*',
                 puntaje = 0, precio = float("inf")):
        """ nombre y ubicacion deben ser cadenas no vacías,
            puntaje y precio son números.
            Si el precio es 0 se reemplaza por infinito. """
 
        if es_cadena_no_vacia (nombre):
            self.nombre = nombre
        else:
            raise TypeError ("El nombre debe ser una cadena no vacía")
 
        if es_cadena_no_vacia (ubicacion):
            self.ubicacion = ubicacion
        else:
            raise TypeError ("La ubicación debe ser una cadena no vacía")
 
        if es_numero(puntaje):
            self.puntaje = puntaje
        else:
            raise TypeError ("El puntaje debe ser un número")
 
        if es_numero(precio):
            if precio != 0:
                self.precio = precio
            else:
                self.precio = float("inf")
        else:
            raise TypeError("El precio debe ser un número")
 
    def __str__(self):
        """ Muestra el hotel según lo requerido. """
        return self.nombre + " de "+ self.ubicacion+ \
                " - Puntaje: "+ str(self.puntaje) + " - Precio: "+ \
                str(self.precio)+ " pesos."

Con este código tenemos ya la posibilidad de construir hoteles, con los atributos de los tipos correspondientes, y de mostrar los hoteles según nos lo han solicitado.

>>> h = Hotel("Hotel City", "Mercedes", 3.25, 78)
>>> print h
Hotel City de Mercedes - Puntaje: 3.25 - Precio: 78 pesos.

14.5.1. Métodos para comparar objetos

Para resolver las comparaciones entre hoteles, será necesario definir algunos métodos especiales que permiten comparar objetos.

En particular, cuando se quiere que los objetos puedan ser ordenados, el método que se debe definir es __cmp__, que debe devolver:

  • Un valor entero menor a cero, si el primer parámetro es menor al segundo.
  • Un valor entero mayor a cero, si el primer parámetro es mayor que el segundo.
  • Cero, si ambos parámetros son iguales.

Para crear el método __cmp__, definiremos primero un método auxiliar ratio(self) que calcula la relación calidad-precio de una instancia de Hotel según la fórmula indicada:

def ratio(self):
    """ Calcula la relación calidad-precio de un hotel de acuerdo
        a la fórmula que nos dio el cliente. """
    return ((self.puntaje**2)*10.)/self.precio

A partir de este método es muy fácil crear un método __cmp__ que cumpla con la especificación previa.

def __cmp__(self, otro):
    diferencia = self.ratio() - otro.ratio()
    if diferencia < 0:
        return -1
    elif diferencia > 0:
        return 1
    else:
        return 0

Una vez que está definida esta función podremos realizar todo tipo de comparaciones entre los hoteles:

>>> h = Hotel("Hotel City", "Mercedes", 3.25, 78)
>>> i = Hotel("Hotel Mascardi", "Bariloche", 6, 150)
>>> i < h
False
>>> i == h
False
>>> i > h
True

14.5.2. Ordenar de menor a mayor listas de hoteles

En una unidad anterior vimos que se puede ordenar una lista usando el método sort:

>>> l1 = [10, -5, 8, 12, 0]
>>> l1.sort()
>>> l1
[-5, 0, 8, 10, 12]

De la misma forma, una vez que hemos definido el método __cmp__, podemos ordenar listas de hoteles, ya que internamente el método sort comparará los hoteles mediante el método de comparación que hemos definido:

>>> h1=Hotel("Hotel 1* normal", "MDQ", 1, 10)
>>> h2=Hotel("Hotel 2* normal", "MDQ", 2, 40)
>>> h3=Hotel("Hotel 3* carisimo", "MDQ", 3, 130)
>>> h4=Hotel("Hotel vale la pena" ,"MDQ", 4, 130)
>>> lista = [ h1, h2, h3, h4 ]
>>> lista.sort()
>>> for hotel in lista:
...     print hotel
...
Hotel 3* carisimo de MDQ - Puntaje: 3 - Precio: 130 pesos.
Hotel 1* normal de MDQ - Puntaje: 1 - Precio: 10 pesos.
Hotel 2* normal de MDQ - Puntaje: 2 - Precio: 40 pesos.
Hotel vale la pena de MDQ - Puntaje: 4 - Precio: 130 pesos.

Podemos verificar cuál fue el criterio de ordenamiento invocando al método ratio en cada caso:

>>> h1.ratio()
1.0
>>> h2.ratio()
1.0
>>> h3.ratio()
0.69230769230769229
>>> h4.ratio()
1.2307692307692308

Y vemos que efectivamente:

  • "Hotel 3* carisimo", con la menor relación calidad-precio aparece primero.
  • "Hotel 1* normal" y "Hotel 2* normal" con la misma relación calidad-precio (igual a 1.0 en ambos casos) aparecen en segundo y tercer lugar en la lista.
  • "Hotel vale la pena" con la mayor relación calidad-precio aparece en cuarto lugar en la lista.

Hemos por lo tanto ordenado la lista de acuerdo al criterio solicitado.

14.5.3. Otras formas de comparación

Si además de querer listar los hoteles por su relación calidad-precio también se quiere poder listarlos según su puntaje, o según su precio, no se lo puede hacer mediante el método __cmp__.

Para situaciones como esta, sort puede recibir, opcionalmente, otro parámetro que es la función de comparación a utilizar. Esta función deberá cumplir con el mismo formato que el método __cmp__, pero puede ser una función cualquiera, ya sea un método de una clase o una función externa.

Además, para simplificar la escritura de este tipo de funciones, podemos utilizar la función de Python cmp, que si le pasamos dos númeroes, devuelve los valores de la forma que necesitamos.

def cmpPrecio(self, otro):
    """ Compara dos hoteles por su precio. """
    return cmp(self.precio, otro.precio)
 
def cmpPuntaje(self, otro):
    """ Compara dos hoteles por su puntaje. """
    return cmp(self.puntaje, otro.puntaje)

Así, para ordenar según el precio, deberemos hacerlo de la siguiente forma:

>>> h1 = Hotel("Hotel Guadalajara", "Pinamar", 2, 55)
>>> h2 = Hotel("Hostería París", "Rosario", 1, 35)
>>> h3 = Hotel("Apart-Hotel Estocolmo", "Esquel", 3, 105)
>>> h4 = Hotel("Posada El Cairo", "Salta", 2.5, 15)
>>> lista = [ h1, h2, h3, h4 ]
>>> lista.sort(cmp=Hotel.cmpPrecio)
>>> for hotel in lista:
...     print hotel
...
Posada El Cairo de Salta - Puntaje: 2.5 - Precio: 15 pesos.
Hostería París de Rosario - Puntaje: 1 - Precio: 35 pesos.
Hotel Guadalajara de Pinamar - Puntaje: 2 - Precio: 55 pesos.
Apart-Hotel Estocolmo de Esquel - Puntaje: 3 - Precio: 105 pesos.

14.5.4. Comparación sólo por igualdad o desigualdad

Existen clases, como la clase Punto vista anteriormente, que no se pueden ordenar, ya que no se puede decir si dos puntos son menores o mayores, con lo cual no se puede implementar un método __cmp__.

Pero en estas clases, en general, será posible comparar si dos objetos son o no iguales, es decir si tienen o no el mismo valor, aún si se trata de objetos distintos.

>>> p = Punto(3,4)
>>> q = Punto(3,4)
>>> p == q
False

En este caso, por más que los puntos tengan el mismo valor, al no estar definido ningún método de comparación Python no sabe cómo comparar los valores, y lo que compara son las variables. p y q son variables distintas, por más que tengan los mismos valores.

Para obtener el comportamiento esperado en estos casos, se redefinen los métodos __eq__ (correspondiente al operador ==) y __ne__ (correspondiente a != o <>).

De forma que para poder comparar si dos puntos son o no iguales, deberemos agregar los siguientes dos métodos a la clase Punto:

def __eq (self, otro):
    """ Devuelve si dos puntos son iguales. """
    return self.x == otro.x and self.y == otro.y
 
def __ne__(self, otro):
    """ Devuelve si dos puntos son distintos. """
    return not self == otro

Una vez agregados estos métodos ya se puede comparar los puntos por su igualdad o desigualdad:

>>> p = Punto(3,4)
>>> q = Punto(3,4)
>>> p == q
True
>>> p != q
False
>>> r = Punto(2,3)
>>> p == r
False
>>> p != r
True
Copyright (c) 2011-2014 Rosita Wachenchauzer, Margarita Manterola, Maximiliano Curia, Marcos Medrano, Nicolás Paez. La copia y redistribución de esta página se permite bajo los términos de la licencia Creative Commons Atribución - Compartir Obras Derivadas Igual 3.0 siempre que se conserve esta nota de copyright.