Programas extensibles a través de la herencia

Beginner

This tutorial is from open-source community. Access the source code

Introducción

La herencia es una herramienta comúnmente utilizada para escribir programas extensibles. Esta sección explora esa idea.

Herencia

La herencia se utiliza para especializar objetos existentes:

class Parent:
  ...

class Child(Parent):
  ...

La nueva clase Child se llama clase derivada o subclase. La clase Parent se conoce como clase base o superclase. Parent se especifica en () después del nombre de la clase, class Child(Parent):.

Extendiendo

Con la herencia, estás tomando una clase existente y:

  • Agregando nuevos métodos
  • Redefiniendo algunos de los métodos existentes
  • Agregando nuevos atributos a las instancias

Al final, estás extendiendo el código existente.

Ejemplo

Supongamos que esta es tu clase inicial:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Puedes cambiar cualquier parte de esto a través de la herencia.

Agregar un nuevo método

class MyStock(Stock):
    def panic(self):
        self.sell(self.shares)

Ejemplo de uso.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>

Redefiniendo un método existente

class MyStock(Stock):
    def cost(self):
        return 1.25 * self.shares * self.price

Ejemplo de uso.

>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>

El nuevo método reemplaza al antiguo. Los otros métodos no se ven afectados. Es genial.

Sobreescritura

A veces una clase extiende un método existente, pero desea utilizar la implementación original dentro de la redefinición. Para esto, use super():

class Stock:
  ...
    def cost(self):
        return self.shares * self.price
  ...

class MyStock(Stock):
    def cost(self):
        ## Comprueba la llamada a `super`
        actual_cost = super().cost()
        return 1.25 * actual_cost

Use super() para llamar a la versión anterior.

Advertencia: En Python 2, la sintaxis era más detallada.

actual_cost = super(MyStock, self).cost()

__init__ y herencia

Si se redefine __init__, es esencial inicializar al padre.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class MyStock(Stock):
    def __init__(self, name, shares, price, factor):
        ## Comprueba la llamada a `super` y `__init__`
        super().__init__(name, shares, price)
        self.factor = factor

    def cost(self):
        return self.factor * super().cost()

Debes llamar al método __init__() en el super que es la forma de llamar a la versión anterior como se mostró anteriormente.

Usando Herencia

La herencia a veces se utiliza para organizar objetos relacionados.

class Shape:
 ...

class Circle(Shape):
 ...

class Rectangle(Shape):
 ...

Piensa en una jerarquía lógica o taxonomía. Sin embargo, un uso más común (y práctico) está relacionado con hacer código reutilizable o extensible. Por ejemplo, un framework podría definir una clase base y te instruiría para personalizarla.

class CustomHandler(TCPHandler):
    def handle_request(self):
     ...
        ## Procesamiento personalizado

La clase base contiene un código de uso general. Tu clase hereda y personaliza partes específicas.

Relación "es un"

La herencia establece una relación de tipo.

class Shape:
...

class Circle(Shape):
...

Comprueba la instancia de objeto.

>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>

Importante: Idealmente, cualquier código que funcione con instancias de la clase padre también funcionará con instancias de la clase hija.

Clase base object

Si una clase no tiene un padre, a veces se ve que se utiliza object como base.

class Shape(object):
...

object es el padre de todos los objetos en Python.

*Nota: técnicamente no es necesario, pero a menudo se ve especificado como un vestigio de su uso obligatorio en Python 2. Si se omite, la clase todavía hereda implícitamente de object.

Herencia múltiple

Puedes heredar de múltiples clases especificándolas en la definición de la clase.

class Mother:
...

class Father:
...

class Child(Mother, Father):
...

La clase Child hereda características de ambos padres. Hay algunos detalles bastante complicados. No lo hagas a menos que sepas lo que estás haciendo. Se dará más información en la siguiente sección, pero no vamos a utilizar la herencia múltiple más adelante en este curso.

Un uso principal de la herencia es escribir código que se pretende extender o personalizar de varias maneras, especialmente en bibliotecas o marcos. Para ilustrar, considera la función print_report() en tu programa report.py. Debería verse más o menos así:

def print_report(reportdata):
    '''
    Imprime una tabla bien formateada a partir de una lista de tuplas (nombre, acciones, precio, cambio).
    '''
    headers = ('Nombre','Acciones','Precio','Cambio')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 +' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

Cuando ejecutes tu programa de informe, deberías obtener una salida como esta:

>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Nombre     Acciones      Precio     Cambio
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

Ejercicio 4.5: Un problema de extensibilidad

Supongamos que quieres modificar la función print_report() para que soporte una variedad de formatos de salida diferentes, como texto plano, HTML, CSV o XML. Para hacer esto, podrías intentar escribir una función gigantesca que hiciera todo. Sin embargo, hacer eso probablemente llevaría a un desastre inmanejable. En cambio, esta es una oportunidad perfecta para usar la herencia en su lugar.

Para comenzar, centra en los pasos que se involucran en la creación de una tabla. En la parte superior de la tabla hay un conjunto de encabezados de tabla. Después de eso, aparecen filas de datos de tabla. Tomemos esos pasos y los pongamos en su propia clase. Crea un archivo llamado tableformat.py y define la siguiente clase:

## tableformat.py

class TableFormatter:
    def headings(self, headers):
        '''
        Emite los encabezados de la tabla.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emite una sola fila de datos de tabla.
        '''
        raise NotImplementedError()

Esta clase no hace nada, pero sirve como una especie de especificación de diseño para clases adicionales que se definirán en breve. Una clase como esta a veces se llama "clase base abstracta".

Modifica la función print_report() de modo que acepte un objeto TableFormatter como entrada e invoque métodos en él para producir la salida. Por ejemplo, así:

## report.py
...

def print_report(reportdata, formatter):
    '''
    Imprime una tabla bien formateada a partir de una lista de tuplas (nombre, acciones, precio, cambio).
    '''
    formatter.headings(['Nombre','Acciones','Precio','Cambio'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

Dado que agregaste un argumento a print_report(), también tendrás que modificar la función portfolio_report(). Cambiala de modo que cree un TableFormatter como este:

## report.py

import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    '''
    Hace un informe de acciones a partir de archivos de datos de cartera y precios.
    '''
    ## Lee los archivos de datos
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Crea los datos del informe
    report = make_report_data(portfolio, prices)

    ## Imprime los datos
    formatter = tableformat.TableFormatter()
    print_report(report, formatter)

Ejecuta este nuevo código:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
... se detiene con error...

Debería detenerse inmediatamente con una excepción NotImplementedError. Eso no es muy emocionante, pero es exactamente lo que esperábamos. Continúa con la siguiente parte.

Ejercicio 4.6: Usar la herencia para producir diferentes salidas

La clase TableFormatter que definiste en la parte (a) está destinada a ser extendida a través de la herencia. De hecho, esa es la idea principal. Para ilustrar, define una clase TextTableFormatter de la siguiente manera:

## tableformat.py
...
class TextTableFormatter(TableFormatter):
    '''
    Emite una tabla en formato de texto plano
    '''
    def headings(self, headers):
        for h in headers:
            print(f'{h:>10s}', end=' ')
        print()
        print(('-'*10 +' ')*len(headers))

    def row(self, rowdata):
        for d in rowdata:
            print(f'{d:>10s}', end=' ')
        print()

Modifica la función portfolio_report() de la siguiente manera y pruébalo:

## report.py
...
def portfolio_report(portfoliofile, pricefile):
    '''
    Hace un informe de acciones a partir de archivos de datos de cartera y precios.
    '''
    ## Lee los archivos de datos
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Crea los datos del informe
    report = make_report_data(portfolio, prices)

    ## Imprime los datos
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

Esto debería producir la misma salida que antes:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
      Nombre     Acciones      Precio     Cambio
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Sin embargo, cambiemos la salida a algo diferente. Define una nueva clase CSVTableFormatter que produce una salida en formato CSV:

## tableformat.py
...
class CSVTableFormatter(TableFormatter):
    '''
    Salida de datos de cartera en formato CSV.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(rowdata))

Modifica tu programa principal de la siguiente manera:

def portfolio_report(portfoliofile, pricefile):
    '''
    Hace un informe de acciones a partir de archivos de datos de cartera y precios.
    '''
    ## Lee los archivos de datos
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Crea los datos del informe
    report = make_report_data(portfolio, prices)

    ## Imprime los datos
    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

Ahora deberías ver una salida CSV como esta:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('portfolio.csv', 'prices.csv')
Nombre,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84

Usando una idea similar, define una clase HTMLTableFormatter que produce una tabla con la siguiente salida:

<tr><th>Nombre</th><th>Acciones</th><th>Precio</th><th>Cambio</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>

Prueba tu código modificando el programa principal para crear un objeto HTMLTableFormatter en lugar de un objeto CSVTableFormatter.

Ejercicio 4.7: El polimorfismo en acción

Una característica principal de la programación orientada a objetos es que puedes insertar un objeto en un programa y funcionará sin necesidad de cambiar ningún código existente. Por ejemplo, si escribes un programa que espera utilizar un objeto TableFormatter, funcionará sin importar de qué tipo de TableFormatter se le proporcione en realidad. Este comportamiento a veces se conoce como "polimorfismo".

Un problema potencial es determinar cómo permitir que el usuario elija el formateador que desea. El uso directo de los nombres de clase como TextTableFormatter a menudo es molesto. Por lo tanto, es posible que consideres un enfoque simplificado. Tal vez insertes una instrucción if en el código como esta:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Hace un informe de acciones a partir de archivos de datos de cartera y precios.
    '''
    ## Lee los archivos de datos
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Crea los datos del informe
    report = make_report_data(portfolio, prices)

    ## Imprime los datos
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Formato desconocido {fmt}')
    print_report(report, formatter)

En este código, el usuario especifica un nombre simplificado como 'txt' o 'csv' para elegir un formato. Sin embargo, ¿es poner una gran instrucción if en la función portfolio_report() de esa manera la mejor idea? Tal vez sea mejor mover ese código a una función de uso general en otro lugar.

En el archivo tableformat.py, agrega una función create_formatter(name) que permita a un usuario crear un formateador dado un nombre de salida como 'txt', 'csv' o 'html'. Modifica portfolio_report() de modo que se vea así:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    '''
    Hace un informe de acciones a partir de archivos de datos de cartera y precios.
    '''
    ## Lee los archivos de datos
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    ## Crea los datos del informe
    report = make_report_data(portfolio, prices)

    ## Imprime los datos
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

Intenta llamar a la función con diferentes formatos para asegurarte de que funcione.

Ejercicio 4.8: Combinando todo

Modifica el programa report.py de modo que la función portfolio_report() tome un argumento opcional que especifique el formato de salida. Por ejemplo:

>>> report.portfolio_report('portfolio.csv', 'prices.csv', 'txt')
      Nombre     Acciones      Precio     Cambio
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Modifica el programa principal para que se pueda especificar un formato en la línea de comandos:

$ python3 report.py portfolio.csv prices.csv csv
Nombre,Acciones,Precio,Cambio
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
$

Discusión

Escribir código extensible es una de las usos más comunes de la herencia en bibliotecas y marcos. Por ejemplo, un marco puede instruirte a definir tu propio objeto que herede de una clase base proporcionada. Luego se te pide que completes varios métodos que implementen varios aspectos de la funcionalidad.

Otro concepto un poco más profundo es la idea de "ser dueño de tus abstracciones". En los ejercicios, definimos nuestra propia clase para formatear una tabla. Es posible que mires tu código y te digas a ti mismo: "Debería usar una biblioteca de formateo o algo que alguien más ya haya hecho en lugar de esto". No, debes usar TANTO tu clase como una biblioteca. Usar tu propia clase promueve el desacoplamiento y es más flexible. Siempre y cuando tu aplicación utilice la interfaz de programación de tu clase, puedes cambiar la implementación interna para que funcione de cualquier manera que desees. Puedes escribir código completamente personalizado. Puedes usar un paquete de terceros. Puedes intercambiar un paquete de terceros por un paquete diferente cuando encuentres uno mejor. No importa, ningún código de tu aplicación se romperá siempre y cuando conserves la interfaz. Esa es una idea poderosa y es una de las razones por las que podrías considerar la herencia para algo así.

Dicho esto, el diseño de programas orientados a objetos puede ser extremadamente difícil. Para obtener más información, probablemente deberías buscar libros sobre el tema de los patrones de diseño (aunque entender lo que sucedió en este ejercicio te llevará bastante lejos en términos de usar objetos de manera prácticamente útil).

Resumen

¡Felicitaciones! Has completado el laboratorio de Herencia. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.