Redefinición de métodos especiales

PythonPythonBeginner
Practicar Ahora

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

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

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.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/build_in_functions("Build-in Functions") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") python/FileHandlingGroup -.-> python/file_opening_closing("Opening and Closing Files") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/AdvancedTopicsGroup -.-> python/context_managers("Context Managers") subgraph Lab Skills python/build_in_functions -.-> lab-132496{{"Redefinición de métodos especiales"}} python/classes_objects -.-> lab-132496{{"Redefinición de métodos especiales"}} python/encapsulation -.-> lab-132496{{"Redefinición de métodos especiales"}} python/file_opening_closing -.-> lab-132496{{"Redefinición de métodos especiales"}} python/file_operations -.-> lab-132496{{"Redefinición de métodos especiales"}} python/context_managers -.-> lab-132496{{"Redefinición de métodos especiales"}} end

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()
✨ Revisar Solución y Practicar

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:

  1. Primero, utiliza la función isinstance para verificar si el objeto other es una instancia de la clase Stock. Esto es importante porque solo queremos comparar objetos Stock con otros objetos Stock.
  2. Si other es un objeto Stock, luego compara los atributos (name, shares y price) de ambos objetos, self y other.
  3. Devuelve True solo si ambos objetos son instancias de Stock y 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()
✨ Revisar Solución y Practicar

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:

  1. __init__: Este es el método de inicialización. Cuando creamos una instancia de la clase redirect_stdout, pasamos un objeto de archivo. Este método almacena ese objeto de archivo en la variable de instancia self.out_file. Así, recuerda a dónde queremos redirigir la salida.
  2. __enter__:
    • Primero, guarda el sys.stdout actual. Esto es importante porque necesitamos restaurarlo más tarde.
    • Luego, reemplaza el sys.stdout actual 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.
  3. __exit__:
    • Este método restaura el sys.stdout original. Así, después de que el bloque with termine, 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 bloque with.

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.