Guía Python

Curso ultra-rapido de Python — Para niños de 11 años o mayores

Ver capítulos de Guía Python

Herencia, relación entre dos o más clases

Y ya hemos llegado al Capítulo VI de la Guía Python!
En el capítulo de hoy, agregaremos nueva funcionalidad a nuestro programa generador de presupuestos, introduciendo con ello, un nuevo concepto de la programación orientada a objetos: herencia.

Herencia

En Programación Orientada a Objetos, la herencia es la relación existente entre dos o más clases. La herencia marca una relación de jerarquía entre objetos, en la cual, una clase principal (madre) puede ser heredada por otras
secundarias (hijas), las cuales adquieren “por herencia” los métodos y atributos de la primera (clase principal).

El objetivo de hoy, es utilizar esta característica de la POO (la herencia), para lograr dos tipos de presupuesto:

  1. El presupuesto tradicional que venimos generando.
  2. Un presupuesto extendido, que emita los recibos de pagos correspondientes, calculando el monto de adelanto y su resto.

Archivos Necesarios

Utilizaremos todos los archivos disponibles en la carpeta “capitulo6“. En esta carpeta, encontraremos las siguientes novedades:

Un nuevo sub-directorio llamado recibos

En caso que decidamos ejecutar el generador de presupuestos en modo avanzado, éste, automáticamente creará y guardará los nuevos recibos en esta carpeta.

Nuevo template recibo.txt

Este archivo es un nuevo template, el cual emplearemos como plantilla para generar los recibos.

================================================================================
                                                 $titulo Nº $numero/1
================================================================================

    En $ciudad a los ___ días del mes de ___________ de 20__
    RECIBÍ de $cliente
    La cantidad de $moneda $pago_1
    En concepto de pago adelanto presupuesto Nº $numero.-


                                            ________________________
                                             $nombre
                                             $web
================================================================================

-------------------------- >>>> cortar aquí <<<< ------------------------------

================================================================================
                                                 $titulo Nº $numero/2
================================================================================

    En $ciudad a los ___ días del mes de ___________ de 20__
    RECIBÍ de $cliente
    La cantidad de $moneda $pago_2
    En concepto de pago finalización de obra según presupuesto Nº $numero.-


                                            ________________________
                                             $nombre
                                             $web
================================================================================

Un nuevo módulo: recibo.py

Este será el archivo en el que más nos concentraremos hoy. Un modelo que hereda de Presupuesto, generando un presupuesto extendido que incluye formulario de recibo de pago.

# -*- coding: utf-8 *-*
from string import Template

from presupuesto import Presupuesto
from constantes import TEMPLATE_RECIBO, CIUDAD, MONEDA_DENOMINACION
from helpers import leer_archivo


class PresupuestoConRecibo(Presupuesto):

    def __init__(self):
        Presupuesto.__init__(self)
        self.adelanto = 40
        self.titulo = "RECIBO"
        self.generar_recibo()

    def calcular_pagos(self):
        """Calcula el monto correspondiente al adelanto y resto del trabajo"""

        total = self.neto
        self.pago_1 = total * self.adelanto / 100
        self.pago_2 = total - self.pago_1

    def generar_recibo(self):
        """Genera los recibos para entregar al cliente"""

        self.calcular_pagos()
        txt = leer_archivo(TEMPLATE_RECIBO)
        diccionario = dict(titulo=self.titulo,
                           numero=self.numero_presupuesto,
                           ciudad=CIUDAD,
                           cliente=self.cliente,
                           moneda=MONEDA_DENOMINACION,
                           pago_1=self.pago_1,
                           nombre=Presupuesto.encabezado_nombre,
                           web=Presupuesto.encabezado_web,
                           pago_2=self.pago_2)

        txt = Template(txt).safe_substitute(diccionario)
        self.guardar_recibo(txt)

    def guardar_recibo(self, contenido):
        """Guarda el recibo generado en la carpeta recibos

        Argumentos:
        contenido -- template renderizado

        """

        filename = 'recibos/' + str(self.numero_presupuesto) + '.txt'
        recibo = open(filename, 'w')
        recibo.write(contenido)
        recibo.close()

run.py

Este archivo, será el que desde ahora en más ejecutemos de la línea de comandos en lugar de presupuesto.py. Veremos como, pasándole un parámetro determinado por línea de comando, se encargará de ejecutar uno u otro módulo (Presupuesto o PresupuestoConRecibo).

# -*- coding: utf-8 *-*
import sys

from presupuesto import Presupuesto
from recibo import PresupuestoConRecibo

modelo = sys.argv[1]

if modelo == 'basico':
    presupuesto = Presupuesto()
elif modelo == 'recibo':
    presupuesto = PresupuestoConRecibo()
else:
    print "Argumentos no válidos"

Ese extraño archivo con extensión .nja

El Capítulo VI de la Guía Python, lo he desarrollado con el IDE Open Source, NINJA-IDE.

.

El archivo Guia_Python.nja será opcionalmente necesario, si deseas utilizar Ninja-IDE para esta etapa del proyecto. El archivo .nja te evitará configurar el proyecto, pero es opcional su descarga.

Archivos modificados

presupuesto.py
- Código estandarizado según PEP 8
- Se elimina además, la instancia a Presupuesto incorporándola en run.py
- Se limpia el método __init__()
- Ahora, Presupuesto hereda de object (ver explicación más adelante)

constantes.py
- Incorpora nuevas constantes

Razonamiento lógico de la herencia

Como bien se comentó al principio, la herencia es una de las características que define al paradigma de la Programación Orientada a Objetos (POO), estableciendo la forma en la cual, dos o más clases se relacionan entre sí.

Cuando una clase hereda de otra, ésta, adquiere de forma automática, los métodos y atributos de la clase de la cual hereda.

Existe una lógica relacional en la herencia de clases. Una clase no debe heredar al azar de otra, sino que debe existir una relación real.

Llevado a un ejemplo de la vida diaria, podríamos tener una clase principal llamada Persona, que sea heredada por la clase Hombre y por la clase Mujer. Hombre y Mujer, tendrían los mismos atributos que Persona (extremidades superiores, inferiores, órganos vitales), pero cada una tendría atributos propios que las distinguen entre sí y a la vez extienden a Persona (órganos reproductores, genes). De la misma manera, compartirían los mismas métodos que Persona (caminar, correr, comer), pero cada una, tendrían sus propios métodos distintivos (no, no me pidan ejemplos, usen la imaginación!!!!).

Sin embargo, no sería relacionalmente lógico, que Perro herede de persona. Si bien puede tener atributos y métodos que a simple vista resultan similares (correr, comer, caminar) no es una clase de Persona sino de Animal.

La Herencia en Python

En Python, para indicar que una clase hereda de otra, se utiliza la siguiente sintaxis:

class NombreDeLaClaseHija(NombreDeLaClaseMadre):

Cuando una clase es principal (una clase madre), debe heredar de object:

class Presupuesto(object):

Nuestro nuevo módulo recibo.py, hereda todos los atributos y métodos de presupuesto:

class PresupuestoConRecibo(Presupuesto):

Además, de poder definir métodos y atributos propios que extenderán las características de Presupuesto.

Accediendo a métodos y atributos

Para acceder a las propiedades de clase, es decir, aquellos atributos definidos en la propia clase ANTES de ser instanciada, se utiliza:

NombreDeLaClase.nombre_del_atributo

Sin embargo, si se desea acceder a propiedades del objeto, es decir, a aquellos atributos definidos LUEGO de crear una instancia de la clase, se utiliza:

self.nombre_del_atributo

Es decir, que dado el siguiente código:

class ClaseMadre(object):

    atributo_de_clase = 'valor'

    def __init__(self):
        self.metodo()

    def metodo(self):
        self.atributo_del_objeto = 'otro valor'

Si heredo esta clase, por otra:
class ClaseHija(ClaseMadre):

Para acceder a “atributo_de_la_clase” dentro de ClaseHija, tendré que hacerlo mediante:

print ClaseMadre.atributo_de_la_clase

Aunque también es posible, acceder mediante self:

print self.atributo_de_la_clase

Mientras que para acceder a “atributo_del_objeto”, primero se debe haber ejecutado el método que define dicha propiedad, es decir metodo():

class ClaseHija(ClaseMadre):

    def __init__(self):
        ClaseMadre.__init__(self)

Para luego acceder a dicho atributo, mediante self:

print self.atributo_del_objeto

Sin embargo, podré acceder a cualquier método heredado, utilizando self directamente:

class ClaseMadre(object):

    atributo_de_clase = 'valor'

    def __init__(self):
        self.metodo()

    def metodo(self):
        self.atributo_del_objeto = 'otro valor'

    def segundo_metodo(self):
        print 'Hola Mundo'


class ClaseHija(ClaseMadre):

    def __init__(self):
        ClaseMadre.__init__(self)
        self.otro_metodo()

    def otro_metodo(self):
        print ClaseMadre.atributo_de_clase
        print self.atributo_del_objeto
        self.segundo_metodo()

Pasando parámetros a un archivo .py por línea de comandos

Desde ahora, para ejecutar nuestro programa, ya no tendremos que hacer python presupuesto.py, sino:
python run.py argumento
Donde argumento podrá ser: basico, quien ejecutará el módulo Presupuesto de la misma forma que en el Capítulo V o recibo, el cual ejecutará el Presupuesto extendido con la generación de recibos de pago.

Si ejecutas por línea de comandos:
presupuesto run.py recibo

Al finalizar, en la nueva carpeta “recibos” se habrá generado un TXT con el mismo número que el presupuesto creado, conteniendo dos recibos de pago para imprimir.

En cambios, si ejecutas:
presupuesto run.py basico

No habrá diferencia con lo que hemos hecho hasta el capítulo anterior.

Capturando argumentos enviados por línea de comandos (en run.py)

import sys
modelo = sys.argv[1]

sys es un módulo estándar de Python que provee de funciones específicas del sistema (ampliar información).

argv recoge una lista de parámetros que son pasados por línea de comandos cuanso se ejecuta mediante python archivo.py argumentos.

El primer elemento de la lista argv, es decir argv[0] es el nombre del archivo. En nuestro código, accedemos directamente al segundo elemento de la lista: sys.argv[1] el cuál nos dirá qué opción hemos elegido. Si optamos por básico, crearemos una instancia de Presupuesto()

if modelo == 'basico':
    presupuesto = Presupuesto()

En cambio, si hemos indicado “recibo” obtendremos una instancia de PresupuestoConRecibo()

elif modelo == 'recibo':
    presupuesto = PresupuestoConRecibo()

De lo contrario, se imprimirá un mensaje de error:

else:
    print "Argumentos no válidos"

Ver más información sobre paso y captura de argumentos por línea de comando.

El desafío de hoy

Nos estamos poniendo cada vez más exigentes con nuestro código. En el capítulo anterior, nos tocó hacer un refactoring para estandarizar el código con la normativa de la PEP 8.

En el capítulo de hoy, el reto es doble.

Desafío #1:

Prueba a ejecutar el módulo run.py sin pasar ningún argumento:
python run.py
¿Te animas a solucionar el inconveniente con la ayuda del tutorial oficial de Python?

Desafío #2

El nuevo módulo recibo.py posee un método para guardar el recibo generado, muy similar al método que utiliza el módulo Presupuesto para guardar el presupuesto en un archivo HTML. Muchas líneas de código se repiten, lo cual, incurre en una redundancia innecesaria que puede ser evitada. ¿Qué ideas se te ocurren para solucionar este código redundante? No es necesario que escribas código. El objetivo es que entre todos razonemos y hagamos una lluvia de ideas que nos ponga en práctica.

Siguiente capítulo: Testeando código con doctest en los comentarios

Eugenia BahitEugenia Bahit para Maestros del Web.
Agrega tu comentario | Enlace permanente al artículo

Síguenos en: Twitter @maestros | Facebook Fan page

23 comentarios

Comentarios

  1. Gume

    Xelente….En cuanto salga de la escuela me pongo a escudriñar lo nuevo de este capitulo….Gracias a STHEPHANIEFALLA por atender el llamado en el twitter. Y a Eugenia por el tiempo de las 14 horas dedicado a este capitulo….

  2. Pablo

    Lastima que ahora no lo pueda leer entero, pero igualmente esta re bueno!!

    reee graciassss :D , llego tarde.. pero mereció la pena la espera :P

    Un Saludo!

    1. umm pq no lo puedes leer entero???

  3. elchezer

    vi la guia duarante clases y es ta muy interesante tanto asi que no prese atencion al profesor jeje… al == que Gume saliendo de clases practicare para no perder la costumbre. Gracias Eugenia.

  4. muy excelente el tutorial de ahora, sigue asi Eugenia, y exito en todo.
    Saludos.

    1. Gracias Christian!

  5. Willy

    Se ve bonito el codigo, me dare un tiempo este fin de semana para meterle mano al codigo awwwnnnn :P …. De igual manera gracias a las señorita Eugenia por el tiempo dedicado, que por cierto esta muy guapa jejeje :D … Saludos !!! :)

  6. lfjaimesb

    Hola buenas tardes, vengo siguiente este tutorial desde hace un par de días y me parece muy bien el esfuerzo que esta realizando Eugenia. Felicidades por eso.

    Soy nuevo en python y he aprendido muchisimo en estas 6 entregas, aunque en esta ultima aun no la he estudiado a fondo como con las otras 5.

    Con respecto al Primer desafio creo que se podria solucionar preguntando primero la longitud de ARGV ya siempre tendrá como valor minimo 1 elemento.

    Para el Segundo Desafio, propongo realizar una clase externa en donde se pueda procesar todo lo relacionado a lectura, escritura de archivos, así se puede usar también para el contador y para guardar los datos.

    Como comente esta entrega aun no la he estudiado del todo bien, por si algo que comento ya esta implementado, una disculpa.

    Saludos

    1. Hola lfjaimesb!

      Desafío #1: genial! ¿Te animás a codearlo?

      Desafío #2: sobre la propuesta de realizar una clase…
      Teniendo en cuenta que una clase es la abstracción de un objeto (un “molde” para crear objetos con los mismos atributos y funciones):
      ¿La abstracción de que objetos sería esa clase?
      ¿Cuáles serían sus atributos y sus métodos?

  7. DRICK

    Hola Eugenia, que gusto saludarte y claro a todos mis compañeros de este seminario de Python.. cada vez se pone más emocionante esto. Te dejo el link de Run.py ya corregido para no mande el error al correr sin ningun parámetro. (–espero este bien….)-:
    run.py
    http://pastebin.com/aJcun50E
    Con el segundo reto se me ocurre crear un modulo (“o procedimiento”) por aparte y llamarlo cada vez que sea necesario — así no repetiremos las líneas de codigo cada vez que queramos hacer la misma tarea. – - Bueno lo voy a poner prueba a ver que tal me va..

    1. Hola drick!!

      Desafío #1: excelente!
      Aprovecho esto para comentar una pequeña cuestión técnica:
      Si bien sabemos que sys.argv traerá consigo al menos 1 parámetro, en la programación, todo razonamiento requiere de abstracción. Al momento de razonar la forma de solucionar un enunciado, debemos lograr “abstraernos” por completo de aquello que es tangible.
      Debemos olvidarnos que CONOCEMOS a sys.argv y solo debemos concentrarnos, en su tipo: una lista.
      Si quiero acceder al segundo elemento de una lista (index 1) ¿que debo validar realmente?
      Imaginemos la siguiente lista:
      mi_lista = []
      Supongamos que esa lista es sys.argv

      Al validar que len(mi_lista) sea igual a uno, no estoy considerando la posibilidad de que len(mi_lista) sea igual a cero (o que también sea igual a 7!)

      Es entonces cuando hay que llevar la abstracción a un nivel más abajo, pues las posibilidades de len(mi_lista) serán infinitas. So… si tengo que acceder al índice 1 (segundo elemento de la lista), la única posibilidad cierta que puedo validar, es que len(mi_lista) sea igual a 2 o distinta. Y con eso, estaré contemplando el infinito de posibilidades:
      si len(mi_lista) == 2: ejecuto el código
      sino: contemplo las infinitas posibilidades anunciando que “no es lo que se esperaba”.

      Si bien preguntar en este caso, si len(sys.argv) es igual a 1, funciona, es más propicio formularlo de manera abstracta olvidando que conocemos su contenido. Bajar un nivel de abstracción, hasta llegar a su tipo. Y luego, bajar otro nivel de abstracción, calculando la cantidad de posibilidades sobre lo que necesito.

      ¿Se entendió o te dejé mareado? Reconozco que es más simple explicarlo con una pizarra!!

      Desafío #2: ¿módulo o procedimiento? Y ya que a vos “te tengo visto” :D de los otros capítulos, te pregunto ¿descartarías convertir el método en un helper?

      PD: si algo de lo que dije sobre el desafío #1 no se entendió, a postear las dudas, ya que me parece sumamente importante que se comprenda :)

      Saludetes!!

    2. Drick

      Hola Eugenia.. gracias por contestarme, tienes razón con el desafío # 1, practicamente deje un vacío total que podría hacer que el el sistema fuera algo inestable pues si el usuairo ingresa más de 1 argumento sistema siempre lo tomará como bueno, es decir si ingresa 3 argumentos estaría bien para el sistema, pero para nosotros no…..(jijij.. que error…jijij).. ahora con lo del desafío dos.. disculpa confundi los lenguales de programación tienes razón al hablar de modulo o procedimiento quería referirme a un helper para luego llamarlo .. aunque creo que me falto explicación.. (disculpa por esto)…. GRACIAS POR TODO he aprendido bastante…!!!

    3. Drick

      Te dejo el link del run… ya corregido… (espero este bien..jijij)
      http://pastebin.com/N4qsp6uH ….

  8. Benito Martín Gutiérrez

    no entendí la explicación hasta que vi la solución que ha dado drick y pensé que podia fallar esa solución.
    la mia es que he creado unas lineas con try y except para que en caso de que no sea ninguna de las dos opciones “basico” o “recibo” salte el error y except se ocupe de darle un valor vacío para que pueda continuar el codigo sin saltarme un error.
    http://pastebin.com/vZfnv2RC

    1. Drick

      Hola Benito.. disculpa.. fijate que estuve probando tu código.. y creo que le diste una solución muy buena al programa.. °°° te aplaudo..°°°.. pero fijate que creo que cometiste el mismo error que tu servidor.. pues el programa solo valida si el argumento 1 no es vació, y por lo tanto si el usuario ingresa 3 0 más argumentos el programa lo tomará como válido (cosa que no es)….

  9. Peter Blond

    Buen día Eugenia, felicidades por tu curso y experiencia con la herramienta alternativa a los inflados Java y Net.

    Me puedes orientar acerca de que herramientas tendria que usar para desarrollar un software de escritorio (ventanas) que sea funcional tanto en windows como en linux ?

    La base de datos que tengo que usar desde ya es MySql.

    Gracias.

  10. Antonio

    Hola buen dia Eugenia, muchas gracias por tomar un poco de tu tiempo para compartrnos un tus conocimientos apartir de este cuso :D del cual he estado aprendiedo mucho, ademas de que lo haz hecho muy entretenido, asi que intente responder el reto

    Para le primer reto tomando en cuenta tu sugerencia de la ejecucion de modulos hice lo siguiente:
    http://pastebin.com/dQLkrLrX

    Para el segundo reto la idea que se me ocurre es generar un metodo a la clase madre el cual se llame guardar_archivo(_self, nombre_archivo, extension)

  11. Matias

    Hola Eugenia, para el reto #2 opino que se podría implementar un método en la colección de métodos helpers.py e importarlo en la clase recibo.

Los comentarios de este post están cerrados. Si quieres seguir la discusión, debatir, criticar, sugerir o expandir el tema te invitamos a hacerlo en tu propio blog, en twitter o donde puedas publicar. No olvides enlazar a este post para que sigamos la conversación y se genere un trackback.

Python Si tienes dudas visita nuestro Foro Python en Foros del web