Clase 4

Temas

  • Orientación a objetos
  • Repaso del paradigma
  • Implementación en Python
  • Excepciones
  • Aserciones

Terminología de la POO

  • Clase
  • Objeto
  • Instancia
  • Atributo
  • Método
  • Mensaje

¿Porque usar clases?

  • Nos permiten reflejar objetos reales del dominio de nuestro problema.
  • Podemos modelar estructuras y relaciones entre objetos como en el mundo real.
  • Tenemos herramientas poderosas como la Herencia para tener una jerarquia de Clases.
  • Tambien podemos componer muchos objetos creando nuevos, por composición.

Clases

  • La sentencia class crea un nuevo objeto clase y le asigna un nombre. Cuando se corre, genera un nuevo objeto Clase y le asigna el nombre que se encuentra en el header.
  • Las asignaciones dentro de la declaración de la clase, correponden a los atributos de clase: despues de correr la declaración de la clase, los atributos pueden ser accedidos de la siguiente manera: objeto.nombre
  • Los atributos de clase proveen al objeto estado y comportamiento. Los atributos de clase mantienen la información de su estado y es compartido a todas las instancias de esa clase.

Instancias

  • Llamando a una clase como una función crea una nueva instancia del objeto. Las instancias representan items concretos en el dominio del programa.
  • Cada instancia hereda los atributos de clase y tienen su propio namespace.
  • Asignaciones a atributos de self en métodos son atributos de instancia. Difieren de una a otra. Dentro de los metodos de la clase, el primer argumento por convención es self y referencia a la instancia del objeto que esta siendo procesado.

Ejemplo de sintaxis

Definir una clase Punto que modele un punto que tiene dos coordenadas x e y.

Ademas le podemos pedir que calcule su norma. La norma es la raíz cuadrada de la suma de sus componentes al cuadrado

In [15]:
from math import sqrt

class Punto:
    
    """ Clase que modela a un punto en el plano. """
    
    def __init__(self, x, y):
        
        """ Recibo las coordenadas del punto. """
            
        self.x = x
        self.y = y
            
    def obtener_norma(self):
        
        """Devuelve la distancia al origen."""
        
        return sqrt(self.x**2 + self.y**2)
    
In [16]:
help(Punto)
Help on class Punto in module __main__:

class Punto
 |  Clase que modela a un punto en el plano.
 |  
 |  Methods defined here:
 |  
 |  __init__(self, x, y)
 |      Recibo las coordenadas del punto.
 |  
 |  obtener_norma(self)
 |      Devuelve la distancia al origen.

Conceptos de orientación a objetos

  • Polimorfismo
  • Herencia
  • Delegación
  • Encapsulamiento

Polimorfismo: ¿necesario en Python?

Veamos un ejemplo en Java

public interface Coloreable {
     void colorear(Color color);
}

public class Cuadrado implements Coloreable {
     private Color color;

    public void colorear(Color color) { 
        this.color = color;
    } 
}

Recordamos: Python introduce duck-typing

Herencia

class Padre(object):
     ''' Clase base. '''
     pass

class Hijo(Padre):
    ''' Clase que hereda el comportamiento del padre.'''
    pass

Veamos un ejemplo

In [32]:
class Padre(object):
    
    def sobrescrito(self):
        print("Padre: método sobrescrito") 
    
    def heredado(self):
        print("Padre: método heredado.")
        
    def alterado(self):
        print("Padre: método alterado.")


class Hijo(Padre):
    
    def sobrescrito(self):
        print("Hijo: método sobrescrito.")
    
    def alterado(self):
        print("Hijo: método alterado antes de invocar al padre.")
        super(Hijo,self).alterado()
        print("Hijo: método alterado después de invocar al padre.")

        
In [38]:
padre = Padre()
hijo = Hijo()
In [39]:
padre.heredado()
Padre: método heredado.
In [40]:
hijo.heredado()
Padre: método heredado.
In [41]:
padre.sobrescrito()
Padre: método sobrescrito
In [42]:
hijo.sobrescrito()
Hijo: método sobrescrito.
In [43]:
padre.alterado()
Padre: método alterado.
In [44]:
hijo.alterado()
Hijo: método alterado antes de invocar al padre.
Padre: método alterado.
Hijo: método alterado después de invocar al padre.

¿Alternativas?

¿Encapsulamiento?

¡En Python todos los métodos y atributos son públicos!

  • Respetamos convenciones.
  • Usamos Properties.

Convenciones: en PEP8

In [57]:
CONSTANTE_A = 4
CONSTANTE_B = 2

class ClaseDeEjemplo:
        ''' Clase que sirve de ejemplo. '''
        def __init__(self, a = CONSTANTE_A, b = CONSTANTE_B):
                self.atributo_publico_a = a
                self.atributo_publico_b = b 
                self.__atributo_privado_c = a + b
                
        def metodo_publico(self):
                return self.atributo_publico_a == CONSTANTE_A and \
                self.atributo_publico_b == CONSTANTE_A
        
        def __metodo_privado(self):
                return self.atributo_publico_a == CONSTANTE_B and \
                self.atributo_publico_b == CONSTANTE_A
                

Una clase más sencilla...

In [46]:
CONSTANTE_A = 4
class OtraClaseDeEjemplo(object):
      
    def __init__(self, a = CONSTANTE_A):
        self.a = a
    
    def atributo_al_cuadrado(self): 
        return self.a ** 2
In [38]:
clase = OtraClaseDeEjemplo()
print "clase.atributo_al_cuadrado: ", clase.atributo_al_cuadrado()
print "Atributo a:", clase.a # Obtengo atributo a
clase.a = 11 # Seteo atributo a
print "clase.atributo_al_cuadrado: ", clase.atributo_al_cuadrado()
clase.atributo_al_cuadrado:  16
Atributo a: 4
clase.atributo_al_cuadrado:  121

¿Si quiero hacer que haya un máximo valor a almacenar?

Problema: al hacer clase.a = 11, si nuestro máximo es 10 no tenemos forma de verificarlo...

Solución 1: Agregar métodos get_a y set_a

In [6]:
CONSTANTE_A = 4
MAX_A = 10
class OtraClaseDeEjemplo(object):
    def __init__(self, a = CONSTANTE_A):
        self.a = a
        
    def get_a(self):
        return self.a
    
    def set_a(self, a):
        self.a = min(a, MAX_A)

    def atributo_al_cuadrado(self):
        return self.a ** 2

Problema:

  • Si se hace clase.a = 11, nuevamente tenemos el mismo problema.
  • Si tenemos código previo que accede con clase.a, hay que actualizarlo todo por clase.get_a.

Solución 2: Usar property

In [87]:
CONSTANTE_A = 4
MAX_A = 10
class OtraClaseDeEjemplo(object):
    
    def __init__(self, a = CONSTANTE_A):
        
        self.set_a(a) # _a la usamos como atributo privado 
        
        
    def get_a(self):
        return self._a
    
    def set_a(self, a):
        self._a = min(a, MAX_A)

    def atributo_al_cuadrado(self):
        return self._a ** 2
    
    a = property(get_a) # a lo usamos como atributo publico
    

Esto permite que al hacer:

  • clase.a se llame a la función fget() (en este caso get_a)
  • clase.a = x se llame a la función fset(x) (en este caso set_a)
In [10]:
clase = OtraClaseDeEjemplo()
print "clase.atributo_al_cuadrado: ", clase.atributo_al_cuadrado()
print "Atributo a:", clase.a # Obtengo atributo a
clase.a = 11 # Seteo atributo a
print "clase.atributo_al_cuadrado: ", clase.atributo_al_cuadrado()
clase.atributo_al_cuadrado:  16
Atributo a: 4
clase.atributo_al_cuadrado:  100

Solución 3: Usar decorators

In [1]:
CONSTANTE_A = 4
MAX_A = 10
class OtraClaseDeEjemplo(object):
    def __init__(self, a = CONSTANTE_A):
        self.a = a
        
    @property
#    @a.getter
    def a(self):
        return self._a
#    a = property(a)
    
    @a.setter
    def a(self, a):
        self._a = min(a, MAX_A)

    def atributo_al_cuadrado(self):
        return self.a ** 2
    
ej1 = OtraClaseDeEjemplo()
ej2 = OtraClaseDeEjemplo()
ej1.a = 30
print ej1.a
10

Es equivalente a la Solusión 3, no requiere definir una variable self._a
Nota: usa decorators, el concepto no entra dentro del scope del curso, si hay tiempo lo vemos al final

In [58]:
clase = OtraClaseDeEjemplo()
print "clase.atributo_al_cuadrado: ", clase.atributo_al_cuadrado()
print "Atributo a:", clase.a # Obtengo atributo a
clase.a = 11 # Seteo atributo a
print "clase.atributo_al_cuadrado: ", clase.atributo_al_cuadrado()
clase.atributo_al_cuadrado:  16
Atributo a: 4
clase.atributo_al_cuadrado:  100

Ejercicio 4.1

Implementar la clase Vector que reciba en el constructor los valores de las coordenadas e implemente los métodos:

  • sumar, que recibe por parámetro otro Vector y devuelve una nueva instancia con la suma.
  • producto_numero, que recibe por parámetro un número y devuelve una nueva instancia con las coordenadas multiplicadas por el número.

Solución 4.1

In [60]:
class Vector(object):
    ''' Clase que modela un vector. '''
    def __init__(self, *coordenadas, **lista_coordenadas): 
        self.coordenadas = \
        lista_coordenadas.get("coordenadas", list(coordenadas))

    def sumar(self, otro):
        ''' Suma dos vectores del mismo largo. '''
        coord_1 = self.coordenadas
        coord_2 = otro.coordenadas
        return Vector(coordenadas = \
               [ x + y for x, y in zip(coord_1, coord_2) ])
    
    def producto_numero(self, numero):
       ''' Multiplica al vector por un escalar. '''
       return Vector(coordenadas = \
                     [ x * numero for x in self.coordenadas])

Métodos Mágicos!

Se invocan automáticamente cuando usamos cierta sintaxis sobre algún tipo que los tenga definidos.

Uso Sintaxis Método Mágico
Inicializar instancias. v = Vector(1,2,3) __init__
Representación de impresión. str(v), print(v) __str__
Representación formal. repr(v) __repr__
Iterar una secuencia. iter(secuencia) __iter__
Obtener el siguiente de un iterador. next(secuencia) __next__
Obtener el largo. len(secuencia) __len__
Verificar pertenencia. elemento in secuencia __contains__
Verificar igualdad. x == y __eq__
Verificar desigualdad. x != y __ne__
Verificar por menor / menor o igual. x<y x / <= y __lt__ / __le__
Verificar por mayor / mayor o igual. x>y x / >= y __gt__ / __ge__
Verificar valor de verdad. if x: __bool__

Ejercicio 4.2

Implementar en la clase Vector los métodos de representación, representación en cadena e igualdad y no igualdad

__str__ 
__repr__
__eq__
__ne__

Solución 4.2

In [100]:
class Vector:
    ''' Clase que modela un vector. '''

    def __init__(self, a, b):
        self.x = a
        self.y = b
        
    def __add__(self, otro):
        ''' Suma dos vectores '''
        return Vector(self.x + otro.y, self.y + otro.y)
 
    def __mul__(self, escalar):
        ''' Multiplica al vector por un escalar. '''
        return Vector(self.x * escalar, self.y * escalar) 
    
    def __str__(self):
        return "("+str(self.x)+","+str(self.y)+")"
    
    def __repr__(self):
        return "("+str(self.x)+","+str(self.y)+")"
Uso Sintaxis Método Mágico
Sumar. x+y __add__
Restar. x–y __sub__
Multiplicar. x*y __mul__
Dividir. x/y __truediv__
Dividir truncando. x // y __floordiv__

Ejercicio 4.3

Sobrecargar los operadores de “+” y “*” de la clase Vector para que implementen la suma y multiplicación respectivamente.

v1 = Vector(1,2)
v2 = Vector(5,6)
print v1 + v2 #(6,8)
print v1 * v2 #(5,12)

Solución 4.3

In [113]:
class Vector:
    ''' Clase que modela un vector. '''

    def __init__(self, a, b):
        self.x = a
        self.y = b
        
    def __add__(self, otro):
        ''' Suma dos vectores '''
        return Vector(self.x + otro.x, self.y + otro.y)

    def __mul__(self, otro):
        ''' Multiplica al vector por un escalar. '''
        return Vector(self.x * otro.x, self.y * otro.y) 
    
    def __str__(self):
        return "("+str(self.x)+","+str(self.y)+")"
    
    def __repr__(self):
        return "("+str(self.x)+","+str(self.y)+")" 
    
v1 = Vector(1,4)
v2 = Vector(4,3)
print v1 + v2
(5,7)

Excepciones

  • Permiten manejar flujo excepcional en tiempo de ejecución.
  • Las excepciones en Python son objetos, instancias de alguna derivada de BaseException.

Sintaxis para lanzar una excepción

raise NombreException("mensaje error")
Ej: 
raise ValueError("Parametro invalido")

Sintaxis para una manejar el flujo

try:
    ...
except Ex1 as e1:
    ...
except (Ex2, Ex3):
     ...
else:
    ...
finally:
    ...

Tipos de excepciones definidas por Python:

BaseException
+-- SystemExit
+-- KeyboardInterrupt +-- GeneratorExit +-- Exception
...
+-- StopIteration +-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError +-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError

Ejercicio 4.4

Implementar en la suma de Vectores la validación de coordenadas, levantando una excepción de tipo ValueError si alguna de las coordenadas del segundo vector es mayor a la del primero.

(1,3)+(2,3) -> ValueError
(1,3)+(1,1) -> (2,4) OK!

Solución 4.4

In [121]:
import traceback
class Vector:
    ''' Clase que modela un vector. '''

    def __init__(self, a, b):
        self.x = a
        self.y = b
        
    def __add__(self, otro):
        ''' Suma dos vectores '''
        if(otro.x > self.x or otro.y > self.y):
            raise ValueError("No se puede sumar un vector mas grande")
        return Vector(self.x + otro.y, self.y + otro.y)

    def __mul__(self, escalar):
        ''' Multiplica al vector por un escalar. '''
        return Vector(self.x * escalar, self.y * escalar) 
    
    def __str__(self):
        return "("+str(self.x)+","+str(self.y)+")"
    
    def __repr__(self):
        return "("+str(self.x)+","+str(self.y)+")"

v1 = Vector(1,3)
v2 = Vector(2,3)
v3 = Vector(1,1)

Excepciones definidas por el usuario

class ExcepcionDeReglaDeNegocio(Exception):
    pass

Ejemplo de uso

CONSTANTE_A = 4
    MAX_A = 10
    class OtraClaseDeEjemplo:
        def __init__(self, a = CONSTANTE_A):
            if a > MAX_A:
                raise ExcepcionDeReglaDeNegocio("Valor de 'a' muy grande.")
        self._a = a

        def atributo_al_cuadrado(self):
            return self.a ** 2

Aserciones

Levantan una excepción del tipo AssertionError si no se cumple una condición.

Se usan para encontrar errores en la etapa de desarrollo de los cuales no se espera recuperar por ejemplo, ruptura de invariantes.

Sintaxis

assert expresion

Equivalente a:

if __debug__:
    if not expresion:
        raise AssertionError

Ejemplo de uso

In [123]:
def pedir_numero_positivo():

    ''' Programa que devuelve un número positivo ingresado por el usuario.'''

    numero = int(raw_input("Ingrese un número positivo"))
    assert int(numero) > 0
    return int(numero)
In [125]:
pedir_numero_positivo()
Ingrese un número positivo-32
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-125-96f2de201bc2> in <module>()
----> 1 pedir_numero_positivo()

<ipython-input-123-a5303b5da4e5> in pedir_numero_positivo()
      5 
      6     numero = int(raw_input("Ingrese un número positivo"))
----> 7     assert int(numero) > 0
      8     return int(numero)

AssertionError: 

Ejercicios Complementarios

Ejercicio 4.5

Fracciones

a) Crear una clase Fraccion, que cuente con dos atributos: dividendo y divisor, que se asignan en el constructor, y se imprimen como X/Y en el método __str__.

b) Crear un método sumar __add__ que recibe otra fracción y devuelve una nueva fracción con la suma de ambas. Ej: f1 + f2

c) Crear un método multiplicar __mul__ que recibe otra fracción y devuelve una nueva fracción con el producto de ambas. Ej: f1 * f2

d) Crear un método simplificar que modifica la fracción actual de forma que los valores del dividendo y divisor sean los menores posibles.

Ejercicio 4.6

Botella y Sacacorchos

a) Escribir una clase Corcho, que contenga un atributo bodega (cadena con el nombre de la bodega).

b) Escribir una clase Botella que contenga un atributo corcho con una referencia al corcho que la tapa, o None si está destapada.

c) Escribir una clase Sacacorchos que tenga un método destapar que le reciba una botella, le saque el corcho y se guarde una referencia al corcho sacado. Debe lanzar una excepción en el caso en que la botella ya esté destapada, o si el sacacorchos ya contiene un corcho.

d) Agregar un método limpiar, que saque el corcho del sacacorchos, o lance una excepción en el caso en el que no haya un corcho.

Ejercicio 4.7

Papel, Birome, Marcador

a) Escribir una clase Papel que contenga un texto, un método escribir, que reciba una cadena para agregar al texto, y el método str que imprima el contenido del texto.

b) Escribir una clase Birome que contenga una cantidad de tinta, y un método escribir, que reciba un texto y un papel sobre el cual escribir. Cada letra escrita debe reducir la cantidad de tinta contenida. Cuando la tinta se acabe, debe lanzar una excepción.

c) Escribir una clase Marcador que herede de Birome, y agregue el método recargar, que reciba la cantidad de tinta a agregar.

Próxima Clase

  • TDD: diseño guiado por pruebas.
  • Más sobre orientación a objetos.
  • ¡Práctica!