Apuntes_Python/01_curso/Modulo_3/README.md
2022-12-24 22:41:20 -03:00

47 KiB

Ir a: Repositorio, Modulo 2, Modulo 4

Modulo 3 - Python basico

Programación Orientada a Objetos

Indice


Intro POO

Caracteristicas:

  • Todo es un objeto
  • Tipado Dinamico
    • Menor Acoplamiento, facilita realizar cambios en los modelos
    • Permite mejor aprovechamiento del polimorfismo
  • Es meta-circular, permite hacer meta-programación

Programa (POO)

Objetos que colaboran enter si enviandose mensajes.
Los obejtos son creados mediante clases.
Las clases son objetos que representan conceptos o ideas del dominio del problema.
Las clases definen el msj q sabe el responder objeto.
Los obejtos son conocidos por los mensajes q saben responder.

Metodos

Son objetos que representan un conjunto de colaboraciones

En las clases se definen 2 tipos de metodos:

  • Metodos de instancia:
    Son metodos que implementan los mensajes que se envian a los objetos que
    son instancias de una clase.
  • Metodos de clase:
    Son metodos que implementan los mensajes que se envian a la clase.

Existen 2 implementaciones de POO:

  • Prototipos:
    Un objeto ejemplar que representa el comportamiento de un conjunto de obetos similares.
  • Clases:
    Una clases es un onjeto que representa un concepto o idea del dominio del problema.

La subclasificacion o herencia es una herramienta para organizar el conocimiento en ontologías

La clasificacion es:

  1. Una relacion estatica entre clases.
  2. Obliga a tener una clase y por lo tanto su nombre antes del objeto concreto, antinatural.
  3. Obliga a generealizar cuadno aun no se posee el conocimiento total de aquello que representa.

La subclasificacion (herencia):

  1. Debe ser especificada de manera inversa a como se obtiene el conociento
  2. Rompe el encapsulamiento, puesto que la subclase debe conocer la implementacion de la superclase

Polimorfismo

Dos o mas objeto son polimorficos entre si para un conjunto de mensajes, si responden a dicho
conjunto de mensajes semanticamente igual, es decir, que hacen lo mismo.

Recibir objetos polimorficos y devolver objetos polimorficos.
Un método es un objeto que representa un conjunto e colaboraciones.
La relación de conocimiento es la única relación que existe entre los objetos.
La definición de tipo en el POO es que es un conjunyo de mensajes.

Multiparadigma, funcional e imperativo además de POO.

Todo es un objeto, es dinamicamente tipado, se puede utlizar al máximo el polimorfismo.
Se puede hacer metaprogramación.

Ejemplificación de polimorfismo

Los enteros y los decimales son polimórficos respecto a la suma

print(  4 + 6,
        4.5 + 5.7,)
# 10 10.2

Los enteros y decimales son polimórficos respecto a la resta

print(  4 - 3,
        8.5 - 5.7,)
# 1 2.8

Los enteros y decimales son polimórficos respecto a la multiplicación

print(  4 * 3,
        8.5 * 5.7,)
# 12 48.45

Pero los enteros no son polimórficos respecto a la funcion factorial

import math

print( math.factorial(4) )       
# 24

print( math.factorial(4.5) )
# ValueError
class ClassA(object):
    def m1(self, a, b):
        return a + b

Clases

class MiClase:
    """Simple clase de ejemplo"""

    i = 12345

    def f(self):
        return 'hola mundo'

x = MiClase()

print(x.__init__())


class Complejo:
    def __init__(self, partereal, parteimaginaria):
        self.r = partereal
        self.i = parteimaginaria


x = Complejo(3.0, -4.5)
print(x.r, x.i)

# Objetos Instancia

x.contador = 1
while x.contador < 10:
    x.contador *= 2
print(x.contador)

del x.contador

# Objetos Metodo

x = MiClase()

xf = x.f

cont = 0
while cont < 10:
    print(xf())
    cont += 1

Variables de clase y de instancia

class Perro:
    tipo = 'canino'             # variable de clase compartida por todas las instancias
    def __init__(self, nombre):
        self.nombre = nombre    # variable de instancia única para la instancia


d = Perro('Fido')
e = Perro('Buddy')

print(d.tipo)                   # compartido por todos los perros 'canino'

print(e.tipo)                   # compartido por todos los perros 'canino'

print(d.nombre)                 # único para d 'Fido'

print(e.nombre)                 # único para e 'Buddy'

La lista trucos en el siguiente código no debería ser usada como variable
de clase porque una sola lista sería compartida por todos las instancias de Perro

Forma Incorrecta

class Perro:
    trucos = []                 # uso INCORRECTO de una variable de clase
    def __init__(self, nombre):
        self.nombre = nombre

    def agregar_truco(self, truco):
        self.trucos.append(truco)

d = Perro('Fido')
e = Perro('Buddy')
d.agregar_truco('girar')
e.agregar_truco('hacerse el muerto')
d.trucos                        # compartidos por todos los perros inesperadamente

Forma Correcta

class Perro:
    def __init__(self, nombre):
        self.nombre = nombre
        self.trucos = []
    # crea una nueva lista vacía para cada perro
    def agregar_truco(self, truco):
        self.trucos.append(truco)

d = Perro('Fido')
e = Perro('Buddy')
d.agregar_truco('girar')
e.agregar_truco('hacerse el muerto')
print(d.trucos)                 # ['girar']
print(e.trucos)                 # ['hacerse el muerto']

Una función definida fuera de la clase

def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1
    def g(self):
        return 'hola mundo'

    h = g

Los métodos pueden llamar a otros métodos de la instancia usando el argumento self

class Bolsa:
    def __init__(self):
        self.datos = []

    def agregar(self, x):
        self.datos.append(x)

    def dobleagregar(self, x):
        self.agregar(x)
        self.agregar(x)

Todo valor es un objeto, y por lo tanto tiene una clase (también llamado su tipo).
Esta se almacena como objeto.__class__.

Herencia

class ClaseDerivada(modulo.ClaseBase):

# Python tiene dos funciones integradas que funcionan con herencia
# isinstance(obj, int)

# ejm
issubclass(float, int)

Herencia multiple

class ClaseDerivada(Base1, Base2, Base3):
    ...

Python Method-resolution-order

Variables privadas

class Mapeo:
    def __init__(self, iterable):
        self.lista_de_items = []
        self.__actualizar(iterable)

    def actualizar(self, iterable):
        for item in iterable:
            self.lista_de_items.append(item)

    __actualizar = actualizar               # copia privada del actualizar() original


class SubClaseMapeo(Mapeo):
    def actualizar(self, keys, values):     # provee una nueva signatura para actualizar()
        for item in zip(keys, values):      # pero no rompe __init__()
            self.lista_de_items.append(item)

Cambalache

class Empleado:
    pass

juan = Empleado()                           # Crear un registro de empleado vacío

juan.nombre = 'Juan Flores'                 # Llenar los campos del registro
juan.depto = 'lab. de computación'
juan.salario = 1000

Algún código Python que espera un tipo abstracto de datos en particular, puede frecuentemente
recibir en cambio una clase que emula los métodos de aquel tipo de datos.

Por ejemplo, si tenés una función que formatea algunos datos a partir de un objeto archivo,
podés definir una clase con métodos read() y readline() que obtengan los datos de
alguna cadena en memoria intermedia, y pasarlo como argumento.

Los objetos método de instancia tienen atributos también:

  • m.self es el objeto instancia con el método m() , y
  • m.func es el objeto función correspondiente al método.

Ambitos y espacios de variables

Alcance de variables

def prueba_ambitos():

    def hacer_local():
        algo = "algo local"


    def hacer_nonlocal():
        nonlocal algo
        algo = "algo no local"


    def hacer_global():
        global algo
        algo = "algo global"

    algo = "algo de prueba"
    hacer_local()
    print("Luego de la asignación local:", algo)

    hacer_nonlocal()
    print("Luego de la asignación no local:", algo)

    hacer_global()
    print("Luego de la asignación global:", algo)



prueba_ambitos()
# Luego de la asignación local: algo de prueba
# Luego de la asignación no local: algo no local
# Luego de la asignación global: algo no local

print("In global scope:", algo)
In global scope: algo global

Buenas practicas POO

ELimar un if, utilizando polimorfismo:

  • en POO todo deberian ser objetos que colaboran enviandose mensajes.
  • el If en Python, no es un mensaje que se le manda un objeto,
    sino que es una sentencia del lenguaje

Isomorfismo

  • Si aparece algo nuevo en el domino deber aparecer algo nuevo en el modelo (no modificarlo).
  • Si se modifica algo del dominio, solo se debe modificar su representacion en el modelo.

En lenguajes de clasificación el if se implementa con polimorfismo.

Usar un if no implica no estamos usando polimorfismo. Esto implica que se tiene
diseños menos mantenibles y ademas diseños NO orientados a objetos.
ej.

llamada = ''

def calculadora_costo_de_llamada(llamada):
    costo = 0
    if llamada.es_local():
        costo = calcular_costo_local_de(llamada)
    elif llamada.es_nacional():
        costo = calcular_costo_internacional_de(llamada)
    elif llamada.es_internacional():
        costo = calcular_costo_internacional_de(llamada)
    return costo

Como sacar el if?

  1. Crear una jerarquia polimorfica con una abstracción por cada condición
  2. Usando el mismo nombre de mensaje repartir el cuerpo del if en cada abstraccion( polimorfismo )
  3. Nombrar el mensaje del paso anterior
  4. Nombrar las abstracciones
  5. Reemplazar if por envío de mensaje polimórfico
  6. Buscar objeto polimoefico si es necesario

Abstraccion

class CondicionSuperClase(object):
    def m(self):
        raise NotImplementedError("Responsabilida de subclase")

class CondicionLocal(CondicionSuperClase):
    def m(self):
        # código de calcular_costo_local_de

class CondicionNacional(CondicionSuperClase):
    def m(self):
        # código de calcular_costo_nacional_de

class CondicionInternacional(CondicionSuperClase):
    def m(self):
        # código de calcular_costo_internacional_de

Implementación puntos 1° 2° 3° 4°

class CalcularCostoLLamada(object):

    @classmethod
    def to_handle(clase, llamada):
        pass # Código q busca CalcularCostoLLamada correspondiente

    def calcular(self):
        raise NotImplementedError("Responsabilida de subclase")


class CalcularCostoLLamadaLocal(CalcularCostoLLamada):
    def calcular(self):
        pass # código de calcular_costo_local_de


class CalcularCostoLLamadaNacional(CalcularCostoLLamada):
    def calcular(self):
        pass # código de calcular_costo_nacional_de


class CalcularCostoLLamadaInternacional(CalcularCostoLLamada):
    def calcular(self):
        pass # código de calcular_costo_internacional_de

# 6to buscar obj polimorfico
# 5to reemplazo del if por envio de mensajes polimorficos
#calculadora_costo = CalcularCostoLLamada.to_handle(llamada)
#calculadora_costo.calcular()

Eliminar codigo repetido

  1. Copiar código repetido
  2. Parametrizar lo que cambia
  3. Poner nombre
def inicial_nombre_cliente(letra):
    "Selecion de clientes con nombres de inicial @letra"
    clientes_selec = []
    for cliente in clientes:
        if cliente.nombre.startswith(letra):
            clientes_selec.append(cliente)
    return clientes_selec


def sobrescribir_cuentas():
    "Selección de cuentas con giro descubierto"
    cuentas_selec = []
    for cuenta in cuentas:
        if cuenta.is_overdraw():
            cuentas_selec.append(cuenta)
    return cuentas_selec

Funcion que reifica las anteriores

def selecionar(objetos, condicion):
    "Selecciona obejtos que cumplen una condición"
    selecion = []
    for objeto in objetos:
        if condicion(objeto):
            selecion.append(objeto)
    return selecion

# selecionar(clientes, lambda cliente: cliente.nombre.startswith(letra))
# selecionar(cuentas, lambda cuenta: cuenta.is_overdraw())

Actividad

Cantidad de dinero

class Moneda():
    """Representa una Moneda"""
    def __init__(self, nombre, simbolo, factor):
        self.nombre = nombre
        self.simbolo = simbolo
        self.factor = factor

    def convert_cant_a_moneda_base(self, numero):
        return round(numero / self.factor, 3)

    def convert_monto_de_moneda_base(self, numero):
        return round(numero * self.factor, 3)

    # Se llama para mostrar objeto en patalla ej, consola
    def __repr__(self):
        return self.nombre
class Dinero(object):
    """Representa una cantidad de dinero"""
    def __init__(self, monto, divisa):
        self.cant = monto
        self.moneda = divisa

    def monto_moneda_base(self):
        return self.moneda.convert_cant_a_moneda_base(self.cant)

    def __add__(self, cantDinero):
        monto = self.monto_moneda_base() + cantDinero.monto_moneda_base()
        monto = self.moneda.convert_monto_de_moneda_base(monto)
        return Dinero(monto, self.moneda)

    def __sub__(self, cantDinero):
        monto = self.monto_moneda_base() - cantDinero.monto_moneda_base()
        monto = self.moneda.convert_monto_de_moneda_base(monto)
        return Dinero(monto, self.moneda)

    def __mul__(self, mult):
        return Dinero(self.cant * mult, self.moneda)

    def __truediv__(self, divi):
        return Dinero(self.cant / divi, self.moneda)

    def __repr__(self):
        return '{} {}'.format(self.moneda.simbolo, self.cant)
valorEuro = 912.35
valorDolar = 808.6
pesoDolar = 1 / valorDolar
pesorEuro = 1 / valorEuro
Peso = Moneda('Peso', '$', 1)
Dolar = Moneda('Dolar', 'U$', pesoDolar)
Euro = Moneda('Euro', '€', pesorEuro)

#DosPesos = Moneda('$', 2)
#CincoDolares = Moneda('U$', 5)

I_Dolar = Dinero(1, Dolar)
V_Dolar = Dinero(5, Dolar)
X_Dolar = Dinero(10, Dolar)
VX_Dolar = Dinero(50, Dolar)
C_Dolar = Dinero(100, Dolar)
VC_Dolar = Dinero(500, Dolar)
M_Dolar = Dinero(1000, Dolar)

I_Peso = Dinero(1, Peso)
X_Peso = Dinero(10, Peso)
VX_Peso = Dinero(50, Peso)
C_Peso = Dinero(100, Peso)
VC_Peso = Dinero(500, Peso)
M_Peso = Dinero(1000, Peso)
VM_Peso = Dinero(5000, Peso)
XM_Peso = Dinero(10000, Peso)
XXM_Peso = Dinero(20000, Peso)

I_Euro = Dinero(1, Euro)
V_Euro = Dinero(5, Euro)
X_Euro = Dinero(10, Euro)
VX_Euro = Dinero(50, Euro)
C_Euro = Dinero(100, Euro)
VC_Euro = Dinero(500, Euro)
M_Euro = Dinero(1000, Euro)
print(I_Dolar+I_Dolar+I_Dolar)
#  U$ 3.0

print(X_Dolar + XM_Peso + X_Euro)
#  U$ 33.65

print(X_Dolar-VC_Dolar)
#  U$ -490.0

print(V_Euro - C_Peso + X_Dolar)
#  € 13.753

print(C_Euro * 3)
#  € 300

print((XXM_Peso / 40))
#  500.0

Introduccion al diseño con objetos

Principios de diseño

  • Eje funcional:
    • Que tan buena es la representacion del dominio Se espera que pueda representar toda observación de aquello q modela. si aparece algo nuevo en el dominio, debe aparecer algo nuevo en el modelo. No modificarlo. Si se modifica algo del dominio, solo se debe modificar su representación en el modelo.

      La relación entre el dominio y el modelo debería ser de uno a uno, lo que se denomina isomorfismo. Este eje es la parte observacional del desarrollo.

  • Eje descriptivo:
    • Que tan bien está escrito el modelo, que tan entendible es un modelo es bueno cuando se le puede entender y, por lo tanto, cambiar.
      En este sentido, es muy importante usar buenos nombres, usar el mismo
      lenguaje que el del dominio de problema, y el código debe ser 'lindo'.
      Este eje es la parte artística del desarrollo.

  • Eje implementativo:
    • Como ejecuta en el aspecto técnico un modelo es bueno cuando ejecuta en el tiempo esperado con los recursos
      definidos como necesarios.
      En este eje se consideran los requerimientos no funcionales,
      por ejemplo performance, espacio, escalabilidad y seguridad.

Para conseguir buenos diseños existen unos principios básicos.

Entre otros, simplicidad, mantener el software simple;
consistencia, utilizar metáforas;
entendible, debe ser legible y tiene que haber un mapeo con el dominio del problema;
máxima cohesión, objetos bien funcionales;
mínimo acoplamiento, minimizar ripple effect o efecto dominó.

Para resumir todo esto, vimos los ejes por los cuales podemos evaluar nuestros
modelos y mencionamos los principales principios de diseño que nos permiten
que nuestros programas sean mantenibles, entendibles y preparados para el cambio.


Reglas de diseño

Mapeo con dominio de problema

  • Regla 1
    Cada ente del dominio del problema debe estar representado por un objeto
    • Las ideas son representadas con una sola clase.
    • Los entes deben tener una o mas representaciones en objetos,
      depedendiendo de la implementacion.

  • Regla 2
    Los objetos deben ser cohesivos representando respnsabilidades de un solo
    dominio de problema. Mietnra mas cohesivo un objeto mas reutilizable es.


  • Regla 3
    Se deben utilizar buenos nombres, que sinteticen correctamenten el conocimiento
    contenido por aquello que están nombrando.
    • Los nombres son el resultado de sintetizar el conocimiento
      que se tiene de aquello está nombrando.
    • Los nombres que se utilizan crean el vocabulario que se utiliza
      en el lenguaje del modelo que se está creando.

  • Regla 4
    Las clases deben representar conceptos del dominio del problema
    • Las clases no son módulos ni componentes de reuso de código.
    • Crear una clase por cada componente de conocimiento o informacion del dominio del problema.
    • La ausencia de clases implica ausencia de conocimiento y por lo tanto la imposibilidad
      del sistema de referirse a dicho conocimiento.

Subclasificacion

  • Regla 1
    Se deben utilizar clases abstractas para representar conceptos abstractos.
    No denominar las clases abstractas como abstractas.

  • Regla 2
    Las clases no-hojas del árbol de subclasificación deben ser clases abstractas

  • Regla 3
    Evitar definir variables de instancia en las clases abstractas, ya q esto impone una
    implementacíon en todas las subclases.

  • Regla 4
    El motivo de subclasificación debe pertener al domonmio del problema que se esta modelando.

  • Regla 5
    No se deben mezclar motivos de subclasificación al subclasificar una clase.

Polimorfismo, respaso, creacion de objetos

  • Regla 1
    Reemplazar el uso de if con polimorfismo

    • El if en el POO implementado usando polimorfismo
    • Cada if es un indicio de la falta de un objeto y del uso del polimorfismo
  • Regla 2
    El codigo repetido refleja la falta de algún objeto que represente el motivo
    de dicho codigo

    • Código repetido no significa 'texto repetido', sino que patrones de colaboraciones
      repetidas.
    • hay reedificar ese código repetido y darle un significado por medio de un nombre.
  • Regla 3
    Un objeto debe estar completo desde el momento de su creación

    • El no hacerlo abre la posibilidad a errores por estar imcompleto,
      habrá mensajes que no sabrá responder.
    • Si un objeto está completo desde su creación, siempre respondera los mensajes que definio.
  • Regla 4
    Un objeto debe ser válido desde el momento de su creación

    • Un objeto debe representar correctamente el ente desde su inicio
    • Junto a la regla anterior mantienen el modelo consistente constantemente

Evitar usar none, objetos inmutables, modelar la arquitectura del sistema.

  • Regla 1
    No utlitizar None.
    • None no es polimorfico con ningún objeto.
    • Por no ser polimórfico implica la necesidad de poner un if lo que abre a errores.
    • None es un objeto con muchos significados.

  • Regla 2
    Favorecer el uso de objetos inmutables
    • Un objeto debe ser inmutable si el ente que representa es inmutable.
    • La mayoría de los entes son inmutables.
    • Todo modelo mutable puede ser representado por un inmutable donde
      se modele los cambios de los objetos por medio de eventos temporales.

  • Regla 3
    Evitar el uso de setters
    • Para aquellos objetos mutables, ebvitar el uso de settets pq pueden generar
      objetos invalidos.
    • Utilizar un único mensaje de modificación.

  • Regla 4
    Modelar la arquitectura del sistema
    • Crear un modelo de la arquitectura del sistema (subsistemas, etc).
    • Otorgar a los subsistemas la responsabilidad de mantener la validez de todo el sistema.
      (la relación entre los objetos)
    • Otorgar la responsabilidad a los subsistemas de modificar un objeto por su impacto
      en el conjunto.

Actividad pila

class Pila(object):
    "ejercicio 3-2"
    
    def __init__(self, *args):
        self.base = []
        for arg in args:
            self.base.append(arg)

    def len(self):
        return len(self.base)

    def top(self):
        indice = self.len() - 1
        return self.base[indice]

    def push(self, args):
        self.base.append(args)

    def pop(self):
        try:
            return self.base.pop()
        except IndexError:
            return "Fuera del indice/vacío"

    def is_empty(self):
        return (lambda x: True if x == 0 else False)(len(self.base))
miPila = Pila('uno', 'dos', 'tres', 'cuatro')

print("Mi Pila1 :", miPila.base, '\n')
#  Mi Pila1 : ['uno', 'dos', 'tres', 'cuatro']

print("Pila1 top() :", miPila.top(), '\n')
#  Pila1 top() : cuatro

print("Pila1 post - push('dieciseis') :", end='')
#  Pila1 post - push('dieciseis')

miPila.push('dieciseis')
print(miPila.base, '\n')
#  ['uno', 'dos', 'tres', 'cuatro', 'dieciseis']

print("Pila1 pop() :", miPila.pop(), '\n')
#  Pila1 pop() : dieciseis

print("Largo Pila1 :", miPila.len(), '\n')
#  Largo Pila1 : 4

print("Esta la Pila1 vacía ? :", miPila.is_empty(), '\n')
#  Esta la Pila1 vacía ? : False

miPila2 = Pila()
print("Esta vacía la Pila2 ? :", miPila2.is_empty(),'\n')
#  Esta vacía la Pila2 ? : True

print("Pila2 pop() : ", miPila2.pop())
#  Pila2 pop() :  Fuera del indice/vacío

UML

Unified Modeling Languaje

Programar es representar conocimiento

Proceso, generalmente, de 3 etapas.

Analisis

Descripción en lenguaje natural. Informal - Inclompleto.

Diseño

Se diagrama el conocimiento de la etapa anterior.
Informal - Incompleto - Explícito

Programación

Codificación a lenguaje de programacion, de la etapa anterior.
Formal - Completo - Explícito

UML es lenguaje mas usado y conocido para modelar software

Provee diversos tipos de diagramas, es ampliamente utilizado para
diseñar y documentar software. Tiene la cualidad de ser compresible.

Diagramas Estructurales

Muestran la estructura estática de los objetos en un sistema

  • Diagramas:
    • Diagrama de clases
    • Diagrama de objetos
    • Diagrama de componentes
    • Diagrama de despliegue
    • Diagrama de paquetes
    • Diagrama de estructura compuesta

Diagramas de Comportamiento

Muestran el comportalmiento dinámico de los objetos en el sistema

  • Diagramas:
    • Diagrama de actividades
    • Diagrama de casos de uso
    • Diagrama de secuencia
    • Diagrama de comunicación
    • Diagrama de tiempos
    • Diagrama global de interacciones

Actividad UML

Semaforo

Enfrentar un problema acotado, incorporando los conceptos vistos, resolver el problema pensando en objetos.

Modelar un semáforo para automóviles

El semáforo debe ser capáz de cambiar la luz que está encendida, cada cierto tiempo,
ej. cada 40 segundos que cambie la luz encendida.
Para los diagramas usar app o web que provea un editor de UML.
Por ejemplo: draw

sequenceDiagram
    participant unSemaforo
    participant unaLuzVerde
    participant unaLuzAmarilla
    participant unaLuzRoja
    participant unTimer

    loop Timer
    unSemaforo->>unaLuzVerde: Encender
    unSemaforo->>unTimer: envia_mensj_a_en(msj_cambiar,40,self)
    unTimer->>unSemaforo: cambiar
    unSemaforo->>unaLuzVerde: Apagar
    
    unSemaforo->>unaLuzAmarilla: Encender
    unSemaforo->>unTimer: envia_mensj_a_en(msj_cambiar,40,self)
    unTimer->>unSemaforo: cambiar
    unSemaforo->>unaLuzAmarilla: Apagar
    
    unSemaforo->>unaLuzRoja: Encender
    unSemaforo->>unTimer: envia_mensj_a_en(msj_cambiar,40,self)
    unTimer->>unSemaforo: cambiar
    unSemaforo->>unaLuzRoja: Apagar
    end
    Note right of unSemaforo: mermaid <br/> sequenceDiagram

Refactorización de Código

Tecnica para reestructurar el código, cambiando la estructura interna
sin cambiar el comportamiento del programa.

Se utliza para mejorar el código o mantiendo la funcionalidad ya desarrollada.

La programación es un proceso de aprendizaje, iterativo e incremental. Y este conocimiento se
debe incorporar al código.

    Ej. aprendemos que cierto concepto en el dominio del problema
    se llama de una manera, pero fue nombrado de otra.

    Se renombra la clase para que tenga una relación directa 
    con el dominio del problema.

    El código queda mas comprensible y evita errores de interpretación. 

Ejemplos de Refactoring

  • Rename:
    Renombrar objeto y sus referencias (nombre de clase, metodo, variable, etc.

  • Extract method:
    Crear nuevo método con el código seleccionado y reemplaza con un envío de mensaje
    a ese método (algunas herramientas reemplazan todas las repeticiones de la selección).

  • Move method:
    Mueve el método seleccionado a una clase visible dentro del contexto en que se mueve.
    Modifica todas las referencias al método movido.

  • Convert local to field:
    Convierte una variable local, en una variable de instancia.

  • Extract to local:
    Extrae el código seleccionado en una variable local iniciada con este.

  • Changue method signature:
    Permite modificar los elementos que definen un método . ej. los parametros. Modificas todos los senders acorde a la nueva definición.

  • Inline:
    Copia el código representado por el método o variable a los lugares donde se referencia dicho método o variable.

  • Encapsulated field:
    Referencias a variables de instancia se reemplazan por getters y setters.

  • Pull up:
    Mueve variables de instancia y/o métodos a la superclase o declara el método como abstracto en la superclase.

  • Push down:
    mueve un conjunto de variables de instancia y/o métodos a las subclases.

Es recomendable tener bien testeado el programa antes de relizar un Refactor.

El software está en constante cambio, por ende, hay debe estar preparado para cambiar facilmente.

Por ello esta es una técnica muy utilizada porqué el proceso de desarrollo de software,
es iterativo e incremental.

Mantenimiento de Software

El mantenimiento de un programa no es solo correción de errores.

Es una etapa en el ciclo de desarrollo del software, como mejoras
de funcionalidad, nuevos features, optimización, etc.

El mantenimiento es un desarrollo evolutivo.


Intro a Test Driven Development

Desarrollo *Guiado por pruebas(tests). * orientado, dirigido
Técnica de aprendizaje iterativa e incremental, y constructivista.
Involucra escritura de pruebas y refactorización de código.

Generalmente para las pruebas se utilizan prue4bas unitarias (unit test).

Realizando TDD

  1. Escribir un test. Definicion del problema
    a) El mas sencillo
    b) Debe fallar al correrlo

  2. Correr todos los test. Resolución del problema
    a) Corregir errores y repetir.
    Resolver el problema hasta el punto donde esta definido

  3. Evaluar posibles mejoras. Mejora de Diseño
    a) Si hay mejoras posibles, refactorizar y volver al paso 2.
    Mejorando el diseño del programa
    b) Si no hay mejoras, volver al paso 1.

Caracteristicas TDD

Es incremental:
- Se concreta al hacer el test más sencillo
- Al fallar al correrlo

Recuerda lo aprendido, y Asegura q se siga cumpliendo:
- Se concreta al correr todos los test.

Feedbak inmediato:
- Se concreta cuando hay errores

Es iterativo:
- Se concreta al saltar al paso 1 o 2

Refelxion en el paso 3, meta-aprender

El TDD permite escribir el mínimo código necesario para alcanzar
la solución del problema, y resolver solo este.

Al Tener tests se logra que el programa sea mantenible, y se pueden
realizar cambios sabiendo que agún test advertirá si se rompe algna
funcionalidad.

No es TDD:

  • Cuando no hay feedback inmediato:
    Escribiendo o modificando código antes de escribir un test
    o escribiendo muchos test antes de escribir el código.

  • Cuando no se desarrolla de manera iterativa e incremental:
    Escribiendo la solución completa de entrada o escribiendo
    los test luego de tener el código (eso ya es testing).


Testing

Detalle de: Test Unitario Automático

Los test poseen una estructura interna común:

  • Anatomía de los Tests
    • Setup: Es donde se crean los objetos necesarios (el contexto) para el test.

    • Act: Acción a realizar o probar.

    • Assertions:
      Verificaciones sobre resultados obtenidos.
      Los assertions son afirmaciones que gerealmente chequean
      el estado del sistema, comparando los resultados obtenidos
      con los esperados.

      Algunos métodos de la clase TestCase:

      • assertEqual
      • assertNotEqual
      • assertTrue
      • assertFalse
      • assertRises

  • Caracteristicas deseables de los test

    • Ser de rápida ejecución.
    • Ser reducidos en tamaño.
    • Ser entendibles.
    • Debe tener 1 test por caso.
    • Debe tener control de todo.
    • Misma cantidad de lineas de código q las del sistema.
    • Nombres declarativos y resumir el: GIVEN / WHEN / THEN

  • Buenas practicas

    • Testear un caso por test (no implica tener solo un assert).
    • Empezar siempre por el test mas sencillo.
    • Comenzar por la aserción, ayuda a entender qué se quiere hacer.
    • Siempre debe haber un assert en el test (o fail, etc).
    • Recordar testear casos negativos (posibles fallas), no solo positivos.
    • Recordar que el test debe estar en control de todo:
      *En el caso de testeo sobre secuencias Verificar la longitud que debe tener
      y que estén unicamente los objetos que deben estar.

  • Clasificación de Test según funcionamiento

    • Test Insoportables: Tardan mucho, posible uso de algún recurso
      lento (bd, conex, etc).
    • Test Fragiles: Se "rompen" cuando se modifica la implementación
      interna de un objeto. Son test de "caja blanca".
    • Test Erraticos: Aveces funcionan, otras no. Hay dependencias
      de "pictures" entre test o usan recursos externos.

Framework Xunit

Frameworks de desarrollo guiado por por pruebas conocidos colectivamente como xUnit.

Disponibles para otros lenguajes y plataformas.

Librería Unittest

Framework de test unitarios unittest de la libreria estándar (inspirado en JUnit).

Similar a la mayoria de los frameworks de otros lengajes, soporta automatización de test,
código compartido para setup y shutdown de test, agregar test en colecciones
e independencia de tests respecto del framework de reporte.

Componentes Xunit

  • Test Fixture :
    Representa la preparación necesaria para correr uno o más test,
    y cualquier acción de limpieza asociada.

  • Test Case :
    Unidad de prueba individial. Ademas de definir los metodos de Assertions,
    define también:

    • setUp: Llamado antes de ejecutar cada test del TestCase.
      Aqui va el código para preparar el contexto del test.

    • tearDown: LLamado al finalizar cada ejecución de test del TestCase.
      Aquí va el código para limpiar el contexto que pudo haber modificado el test.

    • setUpClass:
      Ejecutado cuando se crea una instancia del TestCase.
      Utilizado para hacer un setup del contexto común entre todos los test del TestCase.

    • tearDownClass: Ejecutado cuando se destruye una instancia del TestCase.
      Utilizado para hacer limpieza del contexto. Evitando que afecte otros TestCase.

    • addCleanup:
      Agrega una función de limpieza que se ejecutará en el teardown del test.
      Al agregar muchas opciones, estas se ejecutarán en orden LIFO.

  • Test Suite :
    Colección de test cases, test suites o ambos.

  • Test Runner:
    Coordina la ejecución de test y provee el Output al usuario.

Ejemplos de test

Test Factorial

Ejemplo de implementacion de test y pruebas de la funcion factorial

        factorial(0) = 1
        factorial(1) = 1
        factorial(2) = 2
        factorial(3) = 6
        factorial(4) = 24
        factorial(5) = 120

        n! = n*(n-1)*(n-2)*...*1
import unittest

class ErrorValorNegativo(Exception):
    pass

# Esta función se puede definir de forma recursiva o iterativa
# Para este caso se define usando recursión.
def factorial(n):
    if n < 0:
        raise ErrorValorNegativo("Número Negativo")
    elif n == 0:
        return 1
    elif n == 1:
        return 1
    else:
        return n * factorial(n - 1)

class FactoriaTestCase(unittest.TestCase):

    def test_factorial_numero_negativo(self):
        self.assertRaises(ErrorValorNegativo, factorial, -1)

    def test_factorial_0(self):
        self.assertEqual(1, factorial(0))

    def test_factorial_1(self):
        self.assertEqual(1, factorial(1))

    def test_factorial_2(self):
        self.assertEqual(2, factorial(2))

    def test_factorial_3(self):
        self.assertEqual(6, factorial(3))

    def test_factorial_4(self):
        self.assertEqual(24, factorial(4))

    def test_factorial_5(self):
        self.assertEqual(120, factorial(5))

    def test_factorial_6(self):
        self.assertEqual(720, factorial(6))

# if __name__ == '__main__':
#     unittest.main()
for i in range(10):
    print(factorial(i))

Resultados

........
----------------------------------------------------------------------
Ran 8 tests in 0.000s

OK 

Test Primos

Obtener los factores primos de un número entero

    2 ---> 2
    4 ---> 2,2
    6 ---> 2,3
    12---> 2,2,3
    91---> 7,13
import unittest


def es_primo(n):
    if n <= 1:
        result = False
    else:
        result = True
    for div in range(2, n):
        if n % div == 0:
            result = False
            break;
    return result


def primos_hasta(n):
    primos = []
    for x in range(2, n+1):
        if es_primo(x):
            primos.append(x)
    return primos


def factores_primos(n):
    result = []
    for div in primos_hasta(n):
        while n % div == 0:
            result.append(div)
            n /= div
    return result


class EsPrimoTestCase(unittest.TestCase):

    def test_es_primo_1(self):
        result = es_primo(1)
        self.assertFalse(result)

    def test_es_primo_2(self):
        result = es_primo(2)
        self.assertTrue(result)

    def test_es_primo_3(self):
        result = es_primo(3)
        self.assertTrue(result)

    def test_es_primo_4(self):
        result = es_primo(4)
        self.assertFalse(result)

    @unittest.skip("test malo, saltado")
    def test_es_primo_5(self):
        result = es_primo(5)
        #self.assertFalse(result)
        self.assertFalse(result)

    def test_es_primo_6(self):
        result = es_primo(6)
        self.assertFalse(result)

    def test_es_primo_7(self):
        result = es_primo(7)
        self.assertTrue(result)


class PrimosHastaTestCase(unittest.TestCase):

    def test_primos_hasta_1(self):
        result = primos_hasta(1)
        self.assertEqual([], result)

    def test_primos_hasta_2(self):
        result = primos_hasta(2)
        self.assertEqual([2], result)

    def test_primos_hasta_3(self):
        result = primos_hasta(3)
        self.assertEqual([2, 3], result)

    def test_primos_hasta_4(self):
        result = primos_hasta(4)
        self.assertEqual([2, 3], result)

    def test_primos_hasta_5(self):
        result = primos_hasta(5)
        self.assertEqual([2, 3, 5], result)

    def test_primos_hasta_6(self):
        result = primos_hasta(6)
        self.assertEqual([2, 3, 5], result)

    def test_primos_hasta_7(self):
        result = primos_hasta(7)
        self.assertEqual([2, 3, 5, 7], result)


class FactoresPrimosTestCase(unittest.TestCase):
    def test_factores_primos_de_1(self):
        result = factores_primos(1)
        self.assertEqual([], result)

    def test_factores_primos_de_2(self):
        result = factores_primos(2)
        self.assertEqual([2], result)

    def test_factores_primos_de_4(self):
        result = factores_primos(4)
        self.assertEqual([2, 2], result)

    def test_factores_primos_de_6(self):
        result = factores_primos(6)
        self.assertEqual([2, 3], result)

    def test_factores_primos_de_12(self):
        result = factores_primos(12)
        self.assertEqual([2, 2, 3], result)

    def test_factores_primos_de_91(self):
        result = factores_primos(91)
        self.assertEqual([7, 13], result)
        self.assertEqual(2, len(result))
        self.assertIn(7, result)
        self.assertIn(13, result)


if __name__ == '__main__':
    unittest.main()

Resultados

....s...............
----------------------------------------------------------------------
Ran 20 tests in 0.001s

OK (skipped=1)

Actividades

Actividad números romanos

Aplicar técnica de programación Test Driven Development (TDD)

Implementar de manera iterativa e incremental la conversión de números
enteros a números romanos.

  • Se debe poder convertir cualquier número entero desde el 1 hasta el 1000.
  • Escribe el código y los tests en un archivo de Python.

Relación números y los símbolos:

Número entero Número romano
1 I
5 V
10 X
50 L
100 C
500 D
1000 M
import unittest

class NumeroRomano(object):

    valores = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
    simbolos = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']

    def conv_a_romano(self, n):
        restante = n
        nro_romano = ''
        for i in range(len(self.valores)):
            nro_romano, restante = self.agregar_nro_romano(restante,
                                                           self.valores[i],
                                                           self.simbolos[i],
                                                           nro_romano)
        return nro_romano

    def agregar_nro_romano(self, n, numero, valor_romano, num_romano):
        restante = n
        while restante >= numero:
            num_romano = num_romano + valor_romano
            restante -= numero
        return num_romano, restante


class NumeroRomanoTest(unittest.TestCase):

    def setUp(self):
        self.nro_romano = NumeroRomano()

    def test_1_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(1)
        self.assertEqual('I', nro_romano)

    def test_2_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(2)
        self.assertEqual('II', nro_romano)

    def test_3_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(3)
        self.assertEqual('III', nro_romano)

    def test_4_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(4)
        self.assertEqual('IV', nro_romano)

    def test_5_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(5)
        self.assertEqual('V', nro_romano)

    def test_9_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(9)
        self.assertEqual('IX', nro_romano)

    def test_10_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(10)
        self.assertEqual('X', nro_romano)

    def test_40_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(40)
        self.assertEqual('XL', nro_romano)

    def test_50_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(50)
        self.assertEqual('L', nro_romano)

    def test_90_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(90)
        self.assertEqual('XC', nro_romano)

    def test_100_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(100)
        self.assertEqual('C', nro_romano)

    def test_400_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(400)
        self.assertEqual('CD', nro_romano)

    def test_500_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(500)
        self.assertEqual('D', nro_romano)

    def test_900_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(900)
        self.assertEqual('CM', nro_romano)

    def test_989_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(989)
        self.assertEqual('CMLXXXIX', nro_romano)

    def test_1000_romano(self):
        nro_romano = self.nro_romano.conv_a_romano(1000)
        self.assertEqual('M', nro_romano)


if __name__ == '__main__':
    unittest.main()

Resultados

................
----------------------------------------------------------------------
Ran 16 tests in 0.000s

OK

Actividad Caja Registradora

Para este proyecto, deberás programar una caja registradora para una almacén.

  • El sistema debe poder escanear un producto (el cajero puede tipear el código del producto),

  • y agregarlo a la lista de productos comprados para ese cliente.

  • Además debe mostrar el subtotal.

  • El cajero cuando lo desee puede finalizar la compra y

  • el sistema deberá aplicar los descuentos correspondientes a los productos.

  • Luego, el cajero indica con cuánto paga el cliente y el sistema

  • debe mostrar el cambio que debe devolver al cliente.

Se pide hacer los modelos y las pruebas de las funcionalidades.

No es necesario hacer una interfaz gráfica (o de consola), sino que puede estar
todo el funcionamiento validado con las pruebas unitarias.

@author devfzn@gmail.com

import unittest


class CajaRegistradora(object):

    def __init__(self):
        self.catalogo = {'001': ['Leche', 1050, 5.0],
                         '002': ['Azúcar', 950, 3.0],
                         '003': ['Café', 2000, 7.0]}
        self.pedido = []

    def escanear_producto(self, codigo):
        if codigo in self.catalogo:
            return self.catalogo[codigo]
        else:
            return 'null', 'null', 'null'

    def agregar_a_pedido(self, codigo):
        self.pedido.append(self.escanear_producto(codigo))

    def ver_pedido(self):
        return self.pedido + [['TOTAL ', self.subtotal()]]

    def subtotal(self):
        subtotal = 0
        for item in self.pedido:
            subtotal += item[1] - (item[1]*item[2]/100)
        return subtotal

    def finalizar_pedido(self, pago):
        if pago > self.subtotal():
            return ['Vuelto', pago - self.subtotal()]


class CajaRegistradoraScanTestCase(unittest.TestCase):

    def setUp(self):
        self.caja_registradora = CajaRegistradora()

    def test_escanear_producto_1(self):
        resultado = self.caja_registradora.escanear_producto('001')
        self.assertEqual(['Leche', 1050, 5.0], resultado)

    def test_escanear_producto_2(self):
        resultado = self.caja_registradora.escanear_producto('002')
        self.assertEqual(['Azúcar', 950, 3.0], resultado)

    def test_escanear_producto_3(self):
        resultado = self.caja_registradora.escanear_producto('003')
        self.assertEqual(['Café', 2000, 7.0], resultado)


class CajaRegistradoraListaPedidoSimpleTestCase(unittest.TestCase):

    def setUp(self):
        self.caja_registradora = CajaRegistradora()

    def test_agregar_a_pedido_001(self):
        self.caja_registradora.agregar_a_pedido('001')
        resultado = self.caja_registradora.pedido
        self.assertEqual([['Leche', 1050, 5.0]], resultado)

    def test_agregar_a_pedido_002(self):
        self.caja_registradora.agregar_a_pedido('002')
        resultado = self.caja_registradora.pedido
        self.assertEqual([['Azúcar', 950, 3.0]], resultado)

    def test_agregar_a_pedido_003(self):
        self.caja_registradora.agregar_a_pedido('003')
        resultado = self.caja_registradora.pedido
        self.assertEqual([['Café', 2000, 7.0]], resultado)


class CajaRegistradoraListaPedidoMultipleTestCase(unittest.TestCase):

    def setUp(self):
        self.caja_registradora = CajaRegistradora()
        self.caja_registradora.agregar_a_pedido('003')
        self.caja_registradora.agregar_a_pedido('003')
        self.caja_registradora.agregar_a_pedido('001')
        self.caja_registradora.agregar_a_pedido('002')

    def test_ver_pedido(self):
        resultado = self.caja_registradora.ver_pedido()
        self.assertEqual([['Café', 2000, 7.0],
                          ['Café', 2000, 7.0],
                          ['Leche', 1050, 5.0],
                          ['Azúcar', 950, 3.0],
                          ['TOTAL ', 5639]], resultado)

    def test_subtotal(self):
        resultado = self.caja_registradora.subtotal()
        self.assertEqual(5639, resultado)

    def test_finalizar_pedido10000(self):
        resultado = self.caja_registradora.finalizar_pedido(10000)
        self.assertEqual(['Vuelto', 4361], resultado)

if __name__ == '__main__':
    unittest.main()
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK