4. Clases y Objetos

Introduciremos el concepto de clases y objetos. Aprenderemos sobre la declaración class que nos permite crear nuevos objetos. También introcimos el concepto de inherencia, unaherramienta que es comunmente utilizada para construir programar extensibles. Finalmente, veremos otras características de clases como los métodos especiales, búsqueda dinamica de atributos, y la definición de nuevas excepciones.

4.1 Clases

Esta sección presenta la declaración de clase y la idea de crear nuevos objetos.

4.1.1 Programación orientada a objetos (OO)

Una técnica de programación en la que el código se organiza como una colección de objetos .

Un objeto consta de:

  • Datos. Atributos
  • Comportamiento. Métodos que son funciones aplicadas al objeto.

Ya ha estado utilizando OO durante este curso.

Por ejemplo, manipular una lista.

>>> nums = [1, 2, 3]
>>> nums.append(4)      # Metodo
>>> nums.insert(1,10)   # Metodo 
>>> nums
[1, 10, 2, 3, 4]        # Data 
>>>

nums es una instancia de una lista.

Los métodos (append() e insert()) se adjuntan a la instancia (nums).

La declaración class

Utilice la declaración class para definir un nuevo objeto.

class Player:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.health = 100

    def move(self, dx, dy):
        self.x += dx
        self.y += dy

    def damage(self, pts):
        self.health -= pts

En pocas palabras, una clase es un conjunto de funciones que realizan varias operaciones en las denominadas instancias.

4.1.2 Instancias

Las instancias son los objetos reales que manipula en su programa.

Se crean llamando a la clase como función.

>>> a = Player(2, 3)
>>> b = Player(10, 20)
>>>

a y b son instancias de Player.

Importante: La declaración de clase es solo la definición (no hace nada por sí misma). Similar a la definición de una función.

4.1.3 Datos de instancia

Cada instancia tiene sus propios datos locales.

>>> a.x
2
>>> b.x
10

Estos datos son inicializados por __init__().

class Player:
    def __init__(self, x, y):
        # Cualquier valor guardado en `self` es data de la instancia         
        self.x = x
        self.y = y
        self.health = 100

No hay restricciones sobre el número total o el tipo de atributos almacenados.

4.1.4 Métodos de instancia

Los métodos de instancia son funciones aplicadas a instancias de un objeto.

class Player:
    ...
    # `move` es un metodo     
    def move(self, dx, dy):
        self.x += dx
        self.y += dy

El objeto en sí siempre se pasa como primer argumento.

>>> a.move(1, 2)

# empareja `a` a `self` 
# empareja `1` a `dx` 
# empareja `2` a `dy` 
def move(self, dx, dy):

Por convención, se llama la instancia self. Sin embargo, el nombre real utilizado no es importante. El objeto siempre se pasa como primer argumento. Es simplemente estilo de programación Python para llamar a este argumento self.

4.1.5 Alcance de la clase

Las clases no definen un ámbito / espacio de nombres.

class Player:
    ...
    def move(self, dx, dy):
        self.x += dx
        self.y += dy

    def left(self, amt):
        move(-amt, 0)       # NO. Llama función global `move`         
        self.move(-amt, 0)  # SI. Llama metodo `move` de arriba. 

Si desea operar en una instancia, siempre se refiere a ella explícitamente (por ejemplo, self).

4.1.6 Ejercicios

A partir de este conjunto de ejercicios, comenzamos a realizar una serie de cambios en el código existente de las secciones anteriores. Es fundamental que tenga una versión funcional del ejercicio 3.18 para comenzar. Si no la tiene, trabaje con el código de solución que se encuentra en el directorio Solutions/3_18. Está bien copiarlo.

4.1.7 Ejercicio 4.1: Objetos como estructuras de datos

En la sección 2 y 3, trabajamos con datos representados como tuplas y diccionarios. Por ejemplo, una tenencia de acciones podría representarse como una tupla como esta:

s = ('GOOG',100,490.10)

o como un diccionario como este:

s = { 'name'   : 'GOOG',
      'shares' : 100,
      'price'  : 490.10
}

Incluso puede escribir funciones para manipular dichos datos.

Por ejemplo:

def cost(s):
    return s['shares'] * s['price']

Sin embargo, a medida que su programa crece, es posible que desee crear un mejor sentido de organización. Por lo tanto, otro enfoque para representar datos sería definir una clase. Cree un archivo llamado stock.py y defina una clase Stock que represente una sola tenencia de acciones. Las instancias de Stock deben tener los atributos de name, shares y price. Por ejemplo:

>>> import stock
>>> a = stock.Stock('GOOG',100,490.10)
>>> a.name
'GOOG'
>>> a.shares
100
>>> a.price
490.1
>>>

Cree algunos objetos Stock más y manipúlelos. Por ejemplo:

>>> b = stock.Stock('AAPL', 50, 122.34)
>>> c = stock.Stock('IBM', 75, 91.75)
>>> b.shares * b.price
6117.0
>>> c.shares * c.price
6881.25
>>> stocks = [a, b, c]
>>> stocks
[<stock.Stock object at 0x37d0b0>, <stock.Stock object at 0x37d110>, 
<stock.Stock object at 0x37d050>]
>>> for s in stocks:
     print(f'{s.name:>10s} {s.shares:>10d} {s.price:>10.2f}')

... mire la salida ...
>>>

Una cosa a enfatizar aquí es que la clase Stock actúa como una fábrica para crear instancias de objetos. Básicamente, la llamas como una función y crea un nuevo objeto para ti. Además, debe enfatizarse que cada objeto es distinto: cada uno tiene sus propios datos que están separados de otros objetos que se han creado.

Un objeto definido por una clase es algo similar a un diccionario, solo que con una sintaxis algo diferente. Por ejemplo, en lugar de escribir s['name'] o s['price'], ahora escribe s.name y s.price.

Ejercicio 4.2: Adición de algunos métodos

Con las clases, puede adjuntar funciones a sus objetos. Estos se conocen como métodos y son funciones que operan sobre los datos almacenados dentro de un objeto. Agregue un método cost() y sell() a su objeto Stock. Deberían funcionar así:

>>> import stock
>>> s = stock.Stock('GOOG', 100, 490.10)
>>> s.cost()
49010.0
>>> s.shares
100
>>> s.sell(25)
>>> s.shares
75
>>> s.cost()
36757.5
>>>

Ejercicio 4.3: Creando una lista de instancias

Pruebe estos pasos para hacer una lista de instancias de Stock a partir de una lista de diccionarios. Luego calcule el costo total:

>>> import fileparse
>>> with open('Data/portfolio.csv') as lines:
...     portdicts = fileparse.parse_csv(lines, select=['name','shares','price'], types=[str,int,float])
...
>>> portfolio = [ stock.Stock(d['name'], d['shares'], d['price']) for d in portdicts]
>>> portfolio
[<stock.Stock object at 0x10c9e2128>, <stock.Stock object at 0x10c9e2048>, <stock.Stock object at 0x10c9e2080>,
 <stock.Stock object at 0x10c9e25f8>, <stock.Stock object at 0x10c9e2630>, <stock.Stock object at 0x10ca6f748>,
 <stock.Stock object at 0x10ca6f7b8>]
>>> sum([s.cost() for s in portfolio])
44671.15
>>>

Ejercicio 4.4: Usar tu clase

Modifique la función read_portfolio() en el programa report.py para que lea un portafolio en una lista de instancias Stock como se muestra en el ejercicio 4.3. Una vez que haya hecho eso, modifique todo el código en report.py y pcost.py para que funcione con Stock casos en lugar de los diccionarios.

Sugerencia: no debería tener que realizar cambios importantes en el código. Principalmente, cambiará el acceso al diccionario, como s['shares'] en s.shares.

Debería poder ejecutar sus funciones de la misma manera que antes:

>>> import pcost
>>> pcost.portfolio_cost('Data/portfolio.csv')
44671.15
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
      Name     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
>>>

4.2 Herencia

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

4.2.1 Introducción

La herencia se usa para especializar objetos existentes:

class Parent:
    ...

class Child(Parent):
    ...

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

4.2.2 Extensión

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á ampliando el código existente.

4.2.3 Ejemplo

Suponga que esta es su 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

Puede cambiar cualquier parte de esto mediante herencia.

4.2.4 Agregando 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
>>>

4.2.5 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.

4.2.6 Comportamiento Primordial

A veces, una clase extiende un método existente, pero quiere usar la implementación original dentro de la redefinición. Para esto, haga uso de super():

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

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

Haga uso de super() para llamar a la versión anterior.

Precaución: en Python 2, la sintaxis era más detallada.

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

4.2.7 __init__ y herencia

Si __init__ se redefine, es esencial inicializar la clase 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):
        # revise el llamado a `super` y `__init__`         
        super().__init__(name, shares, price)
        self.factor = factor

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

Debe llamar el método __init__() en super cuál es la forma de llamar a la versión anterior como se mostró anteriormente.

4.2.8 Usando la herencia

La herencia se usa a veces para organizar objetos relacionados.

class Shape:
    ...

class Circle(Shape):
    ...

class Rectangle(Shape):
    ...

Piense 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 marco puede definir una clase base e indicarle que la personalice.

class CustomHandler(TCPHandler):
    def handle_request(self):
        ...
        # Custom processing 

La clase base contiene algún código de propósito general. Tu clase hereda y personaliza partes específicas.

4.2.9 Relación "es un"

La herencia establece una relación de tipo.

class Shape:
    ...

class Circle(Shape):
    ...

Compruebe la instancia del objeto.

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

Importante: lo ideal es que cualquier código que funcione con instancias de la clase principal también funcionará con instancias de la clase secundaria.

4.2.10 clase base object

Si una clase no tiene padre, a veces se usa object como base.

class Shape(object):
    ...

object es la raíz de todos los objetos en Python.

Nota: no es técnicamente necesario, pero a menudo se lo ve especificado como una retención de su uso obligatorio en Python 2. Si se omite, la clase aún hereda implícitamente object.

4.2.11 Herencia múltiple

Puede heredar de varias 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 haga a menos que sepa lo que está haciendo. Se proporcionará más información en la siguiente sección, pero no vamos a utilizar más la herencia múltiple en este curso.

4.2.12 Ejercicios

Un uso importante de la herencia es la escritura de código que debe ampliarse o personalizarse de diversas formas, especialmente en bibliotecas o marcos de trabajo (frameworks). Para ilustrarlo, considere la print_report() función en su programa report.py . Debería verse algo como esto:

def print_report(reportdata):
    ''' Print a nicely formated table from a list of (name, shares, price, change) tuples. '''
    headers = ('Name','Shares','Price','Change')
    print('%10s %10s %10s %10s' % headers)
    print(('-'*10 + ' ')*len(headers))
    for row in reportdata:
        print('%10s %10d %10.2f %10.2f' % row)

Cuando ejecuta su programa de informes, debería obtener un resultado como este:

>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
      Name     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

Ejercicio 4.5: un problema de extensibilidad

Suponga que desea modificar la función print_report() para admitir una variedad de formatos de salida diferentes, como texto sin formato, HTML, CSV o XML. Para hacer esto, podría intentar escribir una función gigantesca que lo hiciera todo. Sin embargo, hacerlo probablemente conduciría a un desorden insostenible. En cambio, esta es una oportunidad perfecta para usar la herencia.

Para comenzar, concéntrese en los pasos involucrados 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 la tabla. Tomemos esos pasos y pongámoslos en su propia clase. Cree un archivo llamado tableformat.py y defina la siguiente clase:

# tableformat.py 
class TableFormatter:
    def headings(self, headers):
        ''' Emite el encabezado de la tabla '''
    raise NotImplementedError()

    def row(self, rowdata):
        ''' Emite una fila de data 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 denomina "clase base abstracta".

Modifique la función print_report() para 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):
    ''' Imprima una tabla con un formato agradable a partir de una lista de tuplas (nombre, acciones, precio, cambio). '''
    formatter.headings(['Name','Shares','Price','Change'])
    for name, shares, price, change in reportdata:
        rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
        formatter.row(rowdata)

Toda vez que agregó un argumento a print_report(), también necesitará modificar la portfolio_report()función. Cámbielo para que cree un aspecto TableFormatter como este:

# report.py 
import tableformat

...
def portfolio_report(portfoliofile, pricefile):
    ''' Make a stock report given portfolio and price data files. '''
    # Read data files     portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    # Create the report data     report = make_report_data(portfolio, prices)

    # Print it out     formatter = tableformat.TableFormatter()
    print_report(report, formatter)

Ejecute este nuevo código:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
... el programa se bloquea ...

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

Ejercicio 4.6: Uso de la herencia para producir resultados diferentes

La clase TableFormatter que definió en la parte (a) está destinada a ampliarse mediante herencia. De hecho, esa es toda la idea. Para ilustrar, defina una clase TextTableFormatter como esta:

# tableformat.py ...
class TextTableFormatter(TableFormatter):
    ''' Emite una tabla de texto sin formato '''
    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()

Modifique la función portfolio_report() así y pruébela:

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

    # Crea el reporte de data    
    report = make_report_data(portfolio, prices)

    # Lo imprime   
    formatter = tableformat.TextTableFormatter()
    print_report(report, formatter)

Esto debería producir el mismo resultado que antes:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
      Name     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
>>>

Sin embargo, cambiemos la salida a otra cosa. Defina una nueva clase CSVTableFormatter que produzca resultados en formato CSV:

# tableformat.py ...
class CSVTableFormatter(TableFormatter):
    ''' Output de data de portfolio en formato CSV. '''
    def headings(self, headers):
        print(','.join(headers))

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

Modifique su programa principal de la siguiente manera:

def portfolio_report(portfoliofile, pricefile):
    ''' Hace un informe de acciones en función de los archivos de datos de precios y cartera. '''  
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    report = make_report_data(portfolio, prices)

    formatter = tableformat.CSVTableFormatter()
    print_report(report, formatter)

Ahora debería ver una salida CSV como esta:

>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name,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

Con una idea similar, defina una clase HTMLTableFormatter que produzca una tabla con el siguiente resultado:

<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</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>

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

Ejercicio 4.7: Polimorfismo en acción

Una característica importante de la programación orientada a objetos es que puede conectar un objeto a un programa y funcionará sin tener que cambiar el código existente. Por ejemplo, si escribiera un programa que esperaba usar un objeto TableFormatter, funcionaría sin importar qué tipo de TableFormatter le haya dado. Este comportamiento a veces se denomina "polimorfismo".

Un problema potencial es averiguar cómo permitir que un usuario elija el formateador que desee. El uso directo de los nombres de las clases TextTableFormatter es a menudo molesto. Por lo tanto, podría considerar algún enfoque simplificado. Quizás incruste una declaración-if en el código como esta:

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    ''' Hace un informe de acciones en función de los archivos de datos de precios y cartera. '''
    # Lea los archivos     
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    # Crea el reporte   
    report = make_report_data(portfolio, prices)

    # Lo imprime    
    if fmt == 'txt':
        formatter = tableformat.TextTableFormatter()
    elif fmt == 'csv':
        formatter = tableformat.CSVTableFormatter()
    elif fmt == 'html':
        formatter = tableformat.HTMLTableFormatter()
    else:
        raise RuntimeError(f'Unknown format {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 la mejor idea poner una enorme declaración if en la función portfolio_report()? Podría ser mejor mover ese código a una función de propósito general en otro lugar.

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

def portfolio_report(portfoliofile, pricefile, fmt='txt'):
    ''' Hace un informe de acciones en función de los archivos de datos de precios y cartera. '''
    # Lea los archivos     
    portfolio = read_portfolio(portfoliofile)
    prices = read_prices(pricefile)

    # Crea el reporte      
    report = make_report_data(portfolio, prices)

    # Lo imprime     
    formatter = tableformat.create_formatter(fmt)
    print_report(report, formatter)

Intente llamar a la función con diferentes formatos para asegurarse de que esté funcionando.

Ejercicio 4.8: Poniendo todo junto

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

>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv', 'txt')
      Name     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
>>>

Modifique el programa principal para que se pueda dar un formato en la línea de comando:

$ python report.py Data/portfolio.csv Data/prices.csv csv
Name,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
bash $

Discusión

Escribir código extensible es uno de los usos más comunes de la herencia en bibliotecas y marcos de desarrollo. Por ejemplo, un marco puede indicarle que defina su propio objeto que hereda de una clase base proporcionada. Luego se le indica que complete varios métodos que implementan varios bits de funcionalidad.

Otro concepto algo más profundo es la idea de "ser dueño de sus abstracciones". En los ejercicios, definimos nuestra propia clase para formatear una tabla. Puede mirar su código y decirse a sí mismo: "¡Debería usar una biblioteca de formato o algo que ya haya creado otra persona!" No, debe usar AMBOS su clase y una biblioteca. El uso de su propia clase promueve un acoplamiento flexible y es más flexible. Siempre que su aplicación utilice la interfaz de programación de su clase, puede cambiar la implementación interna para que funcione de la forma que desee. Puede escribir código totalmente personalizado. Puede utilizar el paquete de terceros de alguien. Cambie un paquete de terceros por un paquete diferente cuando encuentre uno mejor. No importa, ninguno de los códigos de su aplicación se romperá mientras conserve la interfaz. Esa es una idea poderosa y es una de las razones por las que podría considerar la herencia para algo como esto.

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

4.3 Métodos especiales

Varias partes del comportamiento de Python se pueden personalizar a través de métodos especiales o llamados "mágicos". Esta sección presenta esa idea. Además, se analizan el acceso a atributos dinámicos y los métodos vinculados.

4.3.1 Introducción

Las clases pueden definir métodos especiales. Estos tienen un significado especial para el intérprete de Python. Siempre van precedidos y seguidos de __. Por ejemplo __init__.

class Stock(object):
    def __init__(self):
        ...
    def __repr__(self):
        ...

Hay docenas de métodos especiales, pero solo veremos algunos ejemplos específicos.

4.3.2 Métodos especiales para conversiones de cadenas

Los objetos tienen dos representaciones de cadenas.

>>> from datetime import date
>>> d = date(2012, 12, 21)
>>> print(d)
2012-12-21
>>> d
datetime.date(2012, 12, 21)
>>>

La función str() se usa para crear una buena salida imprimible:

>>> str(d)
'2012-12-21'
>>>

La función repr() se utiliza para crear una representación más detallada para los programadores.

>>> repr(d)
'datetime.date(2012, 12, 21)'
>>>

Esas funciones str() y repr() usan un par de métodos especiales en la clase para producir la cadena que se mostrará.

class Date(object):
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    # Utilicelo con `str()`     
    def __str__(self):
        return f'{self.year}-{self.month}-{self.day}'

    # Utilicelo con `repr()`     
    def __repr__(self):
        return f'Date({self.year},{self.month},{self.day})'

Nota: La convención para repr()es devolver una cadena que, cuando se alimenta eval(), recreará el objeto subyacente. Si esto no es posible, se utiliza en su lugar algún tipo de representación fácilmente legible.

4.3.3 Métodos especiales para matemáticas

Los operadores matemáticos implican llamadas a los siguientes métodos.

a + b       a.__add__(b)
a - b       a.__sub__(b)
a * b       a.__mul__(b)
a / b       a.__truediv__(b)
a // b      a.__floordiv__(b)
a % b       a.__mod__(b)
a << b      a.__lshift__(b)
a >> b      a.__rshift__(b)
a & b       a.__and__(b)
a | b       a.__or__(b)
a ^ b       a.__xor__(b)
a ** b      a.__pow__(b)
-a          a.__neg__()
~a          a.__invert__()
abs(a)      a.__abs__()

4.3.4 Métodos especiales para acceder a los elementos

Estos son los métodos para implementar contenedores.

len(x)      x.__len__()
x[a]        x.__getitem__(a)
x[a] = v    x.__setitem__(a,v)
del x[a]    x.__delitem__(a)

Puedes usarlos en tus clases.

class Sequence:
    def __len__(self):
        ...
    def __getitem__(self,a):
        ...
    def __setitem__(self,a,v):
        ...
    def __delitem__(self,a):
        ...

4.3.5 Invocación de método

Invocar un método es un proceso de dos pasos.

  • Búsqueda: el .operador
  • Llamada de método: el operador ()
>>> s = Stock('GOOG',100,490.10)
>>> c = s.cost  # Lookup >>> c
<bound method Stock.cost of <Stock object at 0x590d0>>
>>> c()         # Method call 49010.0
>>>

4.3.6 Métodos vinculados

Un método que aún no ha sido invocado por el operador de llamada de función () se conoce como método vinculado. Opera en la instancia donde se originó.

>>> s = Stock('GOOG', 100, 490.10)
>>> s
<Stock object at 0x590d0>
>>> c = s.cost
>>> c
<bound method Stock.cost of <Stock object at 0x590d0>>
>>> c()
49010.0
>>>

Los métodos vinculados son a menudo una fuente de errores no evidentes por descuido. Por ejemplo:

>>> s = Stock('GOOG', 100, 490.10)
>>> print('Cost : %0.2f' % s.cost)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float argument required
>>>

O comportamiento tortuoso que es difícil de depurar.

f = open(filename, 'w')
...
f.close     # Oops. `f` todavía abierto. 

En ambos casos, el error se debe a que se olvidó de incluir los paréntesis finales. Por ejemplo, s.cost() o f.close().

4.3.7 Acceso a atributos

Existe una forma alternativa de acceder, manipular y gestionar atributos.

getattr(obj, 'name')          # Igual a obj.name 
setattr(obj, 'name', value)   # Igual a obj.name = value 
delattr(obj, 'name')          # Igual a del obj.name 
hasattr(obj, 'name')          # Comprueba si el atributo existe 

Ejemplo:

if hasattr(obj, 'x'):
    x = getattr(obj, 'x'):
else:
    x = None

Nota: getattr() también tiene un valor predeterminado útil *arg.

x = getattr(obj, 'x', None)

4.3.8 Ejercicios

Ejercicio 4.9: Mejor resultado para imprimir objetos

Modifique el objeto Stock que definió stock.py para que el método __repr__() produzca una salida más útil. Por ejemplo:

>>> goog = Stock('GOOG', 100, 490.1)
>>> goog
Stock('GOOG', 100, 490.1)
>>>

Vea lo que sucede cuando lee una cartera de acciones y vea la lista resultante después de haber realizado estos cambios. Por ejemplo:

>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> portfolio
... see what the output is ...
>>>

Ejercicio 4.10: un ejemplo de uso de getattr()

getattr() es un mecanismo alternativo para leer atributos. Se puede usar para escribir código extremadamente flexible. Para empezar, prueba este ejemplo:

>>> import stock
>>> s = stock.Stock('GOOG', 100, 490.1)
>>> columns = ['name', 'shares']
>>> for colname in columns:
        print(colname, '=', getattr(s, colname))

name = GOOG
shares = 100
>>>

Observe con atención que los datos de salida están determinados por completo por los nombres de los atributos enumerados en la variable columns.

En el archivo tableformat.py, tome esta idea y amplíela en una función generalizada print_table()que imprima una tabla que muestra los atributos especificados por el usuario de una lista de objetos arbitrarios. Al igual que con la función print_report() anterior, print_table() también debería aceptar una instancia de TableFormatter para controlar el formato de salida. Así es como debería funcionar:

>>> import report
>>> portfolio = report.read_portfolio('Data/portfolio.csv')
>>> from tableformat import create_formatter, print_table
>>> formatter = create_formatter('txt')
>>> print_table(portfolio, ['name','shares'], formatter)
      name     shares
---------- ----------
        AA        100
       IBM         50
       CAT        150
      MSFT        200
        GE         95
      MSFT         50
       IBM        100

>>> print_table(portfolio, ['name','shares','price'], formatter)
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44
>>>

4.4 Definición de excepciones

Las excepciones definidas por el usuario se definen por clases.

class NetworkError(Exception):
    pass

Las excepciones siempre heredan de Exception.

Usualmente suelen ser clases vacías. Haga uso de pass para el cuerpo.

También puede hacer una jerarquía de sus excepciones.

class AuthenticationError(NetworkError):
     pass

class ProtocolError(NetworkError):
    pass

4.4.1. Ejercicios

Ejercicio 4.11: Definición de una excepción personalizada

A menudo, es una buena práctica que las bibliotecas definan sus propias excepciones.

Esto hace que sea más fácil distinguir entre las excepciones de Python generadas en respuesta a errores de programación comunes frente a las excepciones generadas intencionalmente por una biblioteca para señalar un problema de uso específico.

Modifique la función create_formatter() del último ejercicio para que genere una excepción FormatError personalizada cuando el usuario proporcione un nombre de formato incorrecto.

Por ejemplo:

>>> from tableformat import create_formatter
>>> formatter = create_formatter('xls')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "tableformat.py", line 71, in create_formatter
    raise FormatError('Unknown table format %s' % name)
FormatError: Unknown table format xls
>>>