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