Introducción
En este laboratorio, aprenderás cómo personalizar el comportamiento de los objetos redefiniendo métodos especiales. También cambiarás la forma en que se imprimen los objetos definidos por el usuario y harás que los objetos sean comparables.
Además, aprenderás a crear un gestor de contexto (context manager). El archivo a modificar en este laboratorio es stock.py.
Mejorando la representación de objetos con __repr__
En Python, los objetos se pueden representar como cadenas de dos maneras diferentes. Estas representaciones tienen diferentes propósitos y son útiles en diversos escenarios.
El primer tipo es la representación como cadena. Esta es creada por la función str(), que se llama automáticamente cuando se utiliza la función print(). La representación como cadena está diseñada para ser legible por humanos. Presenta el objeto en un formato que es fácil de entender e interpretar.
El segundo tipo es la representación como código. Esta es generada por la función repr(). La representación como código muestra el código que se necesitaría escribir para recrear el objeto. Se trata más de proporcionar una forma precisa y sin ambigüedades de representar el objeto en código.
Veamos un ejemplo concreto utilizando la clase date incorporada en Python. Esto te ayudará a ver la diferencia entre las representaciones como cadena y como código en acción.
>>> from datetime import date
>>> d = date(2008, 7, 5)
>>> print(d) ## Uses str()
2008-07-05
>>> d ## Uses repr()
datetime.date(2008, 7, 5)
En este ejemplo, cuando usamos print(d), Python llama a la función str() en el objeto date d, y obtenemos una fecha legible por humanos en el formato YYYY-MM-DD. Cuando simplemente escribimos d en la shell interactiva, Python llama a la función repr(), y vemos el código necesario para recrear el objeto date.
Puedes obtener explícitamente la cadena de repr() de varias maneras. Aquí hay algunos ejemplos:
>>> print('The date is', repr(d))
The date is datetime.date(2008, 7, 5)
>>> print(f'The date is {d!r}')
The date is datetime.date(2008, 7, 5)
>>> print('The date is %r' % d)
The date is datetime.date(2008, 7, 5)
Ahora, apliquemos este concepto a nuestra clase Stock. Vamos a mejorar la clase implementando el método __repr__. Este método especial es llamado por Python cuando necesita la representación como código de un objeto.
Para hacer esto, abre el archivo stock.py en tu editor. Luego, agrega el método __repr__ a la clase Stock. El método __repr__ debe devolver una cadena que muestre el código necesario para recrear el objeto Stock.
def __repr__(self):
return f"Stock('{self.name}', {self.shares}, {self.price})"
Después de agregar el método __repr__, tu clase Stock completa debería verse así:
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, shares):
self.shares -= shares
def __repr__(self):
return f"Stock('{self.name}', {self.shares}, {self.price})"
Ahora, probemos nuestra clase Stock modificada. Abre una shell interactiva de Python ejecutando el siguiente comando en tu terminal:
python3
Una vez que la shell interactiva esté abierta, prueba los siguientes comandos:
>>> import stock
>>> goog = stock.Stock('GOOG', 100, 490.10)
>>> goog
Stock('GOOG', 100, 490.1)
También puedes ver cómo funciona el método __repr__ con una cartera de acciones. Aquí hay un ejemplo:
>>> import reader
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> portfolio
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44), Stock('MSFT', 200, 51.23), Stock('GE', 95, 40.37), Stock('MSFT', 50, 65.1), Stock('IBM', 100, 70.44)]
Como puedes ver, el método __repr__ ha hecho que nuestros objetos Stock sean mucho más informativos cuando se muestran en la shell interactiva o en el depurador. Ahora muestra el código necesario para recrear cada objeto, lo cual es muy útil para depurar y entender el estado de los objetos.
Cuando hayas terminado de probar, puedes salir del intérprete de Python ejecutando el siguiente comando:
>>> exit()
Haciendo objetos comparables con __eq__
En Python, cuando se utiliza el operador == para comparar dos objetos, Python en realidad llama al método especial __eq__. Por defecto, este método compara las identidades de los objetos, lo que significa que verifica si se almacenan en la misma dirección de memoria, en lugar de comparar su contenido.
Veamos un ejemplo. Supongamos que tenemos una clase Stock, y creamos dos objetos Stock con los mismos valores. Luego intentamos compararlos utilizando el operador ==. Así es como se puede hacer en el intérprete de Python:
Primero, inicia el intérprete de Python ejecutando el siguiente comando en tu terminal:
python3
Luego, en el intérprete de Python, ejecuta el siguiente código:
>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
False
Como se puede ver, aunque los dos objetos Stock a y b tienen los mismos valores para sus atributos (name, shares y price), Python los considera objetos diferentes porque se almacenan en diferentes ubicaciones de memoria.
Para solucionar este problema, podemos implementar el método __eq__ en nuestra clase Stock. Este método se llamará cada vez que se utilice el operador == en objetos de la clase Stock.
Ahora, abre el archivo stock.py nuevamente. Dentro de la clase Stock, agrega el siguiente método __eq__:
def __eq__(self, other):
return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
(other.name, other.shares, other.price))
Analicemos lo que hace este método:
- Primero, utiliza la función
isinstancepara verificar si el objetootheres una instancia de la claseStock. Esto es importante porque solo queremos comparar objetosStockcon otros objetosStock. - Si
otheres un objetoStock, luego compara los atributos (name,sharesyprice) de ambos objetos,selfyother. - Devuelve
Truesolo si ambos objetos son instancias deStocky sus atributos son idénticos.
Después de agregar el método __eq__, tu clase Stock completa debería verse así:
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, shares):
self.shares -= shares
def __repr__(self):
return f"Stock('{self.name}', {self.shares}, {self.price})"
def __eq__(self, other):
return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
(other.name, other.shares, other.price))
Ahora, probemos nuestra clase Stock mejorada. Inicia el intérprete de Python nuevamente:
python3
Luego, ejecuta el siguiente código en el intérprete de Python:
>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
True
>>> c = stock.Stock('GOOG', 200, 490.1)
>>> a == c
False
¡Genial! Ahora nuestros objetos Stock se pueden comparar adecuadamente según su contenido, en lugar de sus direcciones de memoria.
La comprobación isinstance en el método __eq__ es crucial. Asegura que solo estemos comparando objetos Stock. Si no tuviéramos esta comprobación, comparar un objeto Stock con algo que no es un objeto Stock podría generar errores.
Cuando hayas terminado de probar, puedes salir del intérprete de Python ejecutando el siguiente comando:
>>> exit()
Creando un gestor de contexto (context manager)
Un gestor de contexto es un tipo especial de objeto en Python. En Python, los objetos pueden tener diferentes métodos que definen su comportamiento. Un gestor de contexto define específicamente dos métodos importantes: __enter__ y __exit__. Estos métodos trabajan en conjunto con la declaración with. La declaración with se utiliza para establecer un contexto específico para un bloque de código. Piénsalo como crear un pequeño entorno donde ocurren ciertas cosas, y cuando el bloque de código termina, el gestor de contexto se encarga de la limpieza.
En este paso, vamos a crear un gestor de contexto que tiene una función muy útil. Redirigirá temporalmente la salida estándar (sys.stdout). La salida estándar es donde va la salida normal de tu programa de Python, generalmente la consola. Al redirigirla, podemos enviar la salida a un archivo en lugar de la consola. Esto es útil cuando quieres guardar la salida que de otro modo solo se mostraría en la consola.
Primero, necesitamos crear un nuevo archivo para escribir el código de nuestro gestor de contexto. Llamaremos a este archivo redirect.py. Puedes crearlo utilizando el siguiente comando en la terminal:
touch /home/labex/project/redirect.py
Ahora que el archivo está creado, ábrelo en un editor. Una vez abierto, agrega el siguiente código de Python al archivo:
import sys
class redirect_stdout:
def __init__(self, out_file):
self.out_file = out_file
def __enter__(self):
self.stdout = sys.stdout
sys.stdout = self.out_file
return self.out_file
def __exit__(self, ty, val, tb):
sys.stdout = self.stdout
Analicemos lo que hace este gestor de contexto:
__init__: Este es el método de inicialización. Cuando creamos una instancia de la claseredirect_stdout, pasamos un objeto de archivo. Este método almacena ese objeto de archivo en la variable de instanciaself.out_file. Así, recuerda a dónde queremos redirigir la salida.__enter__:- Primero, guarda el
sys.stdoutactual. Esto es importante porque necesitamos restaurarlo más tarde. - Luego, reemplaza el
sys.stdoutactual con nuestro objeto de archivo. A partir de este momento, cualquier salida que normalmente iría a la consola irá al archivo en su lugar. - Finalmente, devuelve el objeto de archivo. Esto es útil porque es posible que queramos usar el objeto de archivo dentro del bloque
with.
- Primero, guarda el
__exit__:- Este método restaura el
sys.stdoutoriginal. Así, después de que el bloquewithtermine, la salida volverá a la consola como de costumbre. - Toma tres parámetros: tipo de excepción (
ty), valor de excepción (val) y traza de pila (tb). Estos parámetros son requeridos por el protocolo de gestores de contexto. Se utilizan para manejar cualquier excepción que pueda ocurrir dentro del bloquewith.
- Este método restaura el
Ahora, probemos nuestro gestor de contexto. Lo usaremos para redirigir la salida de una tabla a un archivo. Primero, inicia el intérprete de Python:
python3
Luego, ejecuta el siguiente código de Python en el intérprete:
>>> import stock, reader, tableformat
>>> from redirect import redirect_stdout
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> formatter = tableformat.create_formatter('text')
>>> with redirect_stdout(open('out.txt', 'w')) as file:
... tableformat.print_table(portfolio, ['name','shares','price'], formatter)
... file.close()
...
>>> ## Let's check the content of the output file
>>> print(open('out.txt').read())
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
¡Genial! Nuestro gestor de contexto funcionó como se esperaba. Redirigió con éxito la salida de la tabla al archivo out.txt.
Los gestores de contexto son una característica muy poderosa en Python. Te ayudan a manejar recursos adecuadamente. Aquí hay algunos casos de uso comunes para los gestores de contexto:
- Operaciones de archivos: Cuando abres un archivo, un gestor de contexto puede asegurarse de que el archivo se cierre correctamente, incluso si ocurre un error.
- Conexiones a bases de datos: Puede garantizar que la conexión a la base de datos se cierre después de que hayas terminado de usarla.
- Bloqueos en programas con hilos: Los gestores de contexto pueden manejar el bloqueo y desbloqueo de recursos de manera segura.
- Cambio temporal de configuraciones de entorno: Puedes cambiar algunas configuraciones para un bloque de código y luego restaurarlas automáticamente.
Este patrón es muy importante porque asegura que los recursos se limpien adecuadamente, incluso si ocurre una excepción dentro del bloque with.
Cuando hayas terminado de probar, puedes salir del intérprete de Python:
>>> exit()
Resumen
En este laboratorio (lab), has aprendido cómo personalizar la representación en cadena de objetos utilizando el método __repr__, hacer que los objetos sean comparables con el método __eq__ y crear un gestor de contexto (context manager) utilizando los métodos __enter__ y __exit__. Estos métodos especiales "dunder" son la piedra angular de las características orientadas a objetos de Python.
Implementar estos métodos en tus clases permite que tus objetos se comporten como tipos integrados (built - in types) y se integren sin problemas con las características del lenguaje Python. Los métodos especiales permiten diversas funcionalidades, como representaciones de cadena personalizadas, comparación de objetos y gestión de contextos. A medida que avanzes en Python, descubrirás más métodos especiales para aprovechar su potente modelo de objetos.