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