Variables de clase y métodos de clase

Beginner

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

Introducción

En este laboratorio, aprenderás sobre variables de clase y métodos de clase en Python. Comprenderás su propósito y uso, y aprenderás cómo definir y utilizar métodos de clase de manera efectiva.

Además, implementarás constructores alternativos utilizando métodos de clase, explorarás la relación entre variables de clase y herencia, y crearás utilidades flexibles para la lectura de datos. Los archivos stock.py y reader.py se modificarán durante este laboratorio.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 100%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Comprensión de Variables de Clase y Métodos de Clase

En este primer paso, vamos a profundizar en los conceptos de variables de clase y métodos de clase en Python. Estos son conceptos importantes que te ayudarán a escribir código más eficiente y organizado. Antes de comenzar a trabajar con variables de clase y métodos de clase, primero echemos un vistazo a cómo se crean actualmente las instancias de nuestra clase Stock. Esto nos dará una comprensión básica y nos mostrará dónde podemos hacer mejoras.

¿Qué son las Variables de Clase?

Las variables de clase son un tipo especial de variables en Python. Son compartidas entre todas las instancias de una clase. Para entender esto mejor, comparemoslas con las variables de instancia. Las variables de instancia son únicas para cada instancia de una clase. Por ejemplo, si tienes múltiples instancias de una clase, cada instancia puede tener su propio valor para una variable de instancia. Por otro lado, las variables de clase se definen a nivel de clase. Esto significa que todas las instancias de esa clase pueden acceder y compartir el mismo valor de la variable de clase.

¿Qué son los Métodos de Clase?

Los métodos de clase son métodos que trabajan en la propia clase, no en instancias individuales de la clase. Están vinculados a la clase, lo que significa que se pueden llamar directamente en la clase sin crear una instancia. Para definir un método de clase en Python, usamos el decorador @classmethod. Y en lugar de tomar la instancia (self) como primer parámetro, los métodos de clase toman la clase (cls) como su primer parámetro. Esto les permite operar en datos a nivel de clase y realizar acciones relacionadas con la clase en su conjunto.

Enfoque Actual para Crear Instancias de Stock

Primero, veamos cómo se crean actualmente las instancias de la clase Stock. Abre el archivo stock.py en el editor para observar la implementación actual:

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

Las instancias de esta clase se crean típicamente de una de estas maneras:

  1. Inicialización directa con valores:

    s = Stock('GOOG', 100, 490.1)

    Aquí, estamos creando directamente una instancia de la clase Stock proporcionando los valores para los atributos name, shares y price. Esta es una forma sencilla de crear una instancia cuando conoces los valores de antemano.

  2. Creación a partir de datos leídos de un archivo CSV:

    import csv
    with open('portfolio.csv') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Saltar el encabezado
        row = next(rows)      ## Obtener la primera fila de datos
        s = Stock(row[0], int(row[1]), float(row[2]))

    Cuando leemos datos de un archivo CSV, los valores están inicialmente en formato de cadena. Entonces, cuando creamos una instancia de Stock a partir de datos CSV, necesitamos convertir manualmente los valores de cadena a los tipos adecuados. Por ejemplo, el valor de shares debe convertirse a un entero, y el valor de price debe convertirse a un flotante.

Intentemos esto. Crea un nuevo archivo de Python llamado test_stock.py en el directorio ~/project con el siguiente contenido:

## test_stock.py
from stock import Stock
import csv

## Método 1: Creación directa
s1 = Stock('GOOG', 100, 490.1)
print(f"Stock: {s1.name}, Shares: {s1.shares}, Price: {s1.price}")
print(f"Cost: {s1.cost()}")

## Método 2: Creación a partir de una fila CSV
with open('portfolio.csv') as f:
    rows = csv.reader(f)
    headers = next(rows)  ## Saltar el encabezado
    row = next(rows)      ## Obtener la primera fila de datos
    s2 = Stock(row[0], int(row[1]), float(row[2]))
    print(f"\nStock from CSV: {s2.name}, Shares: {s2.shares}, Price: {s2.price}")
    print(f"Cost: {s2.cost()}")

Ejecuta este archivo para ver los resultados:

cd ~/project
python test_stock.py

Deberías ver una salida similar a:

Stock: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0

Stock from CSV: AA, Shares: 100, Price: 32.2
Cost: 3220.0

Esta conversión manual funciona, pero tiene algunos inconvenientes. Necesitamos conocer el formato exacto de los datos y debemos realizar las conversiones cada vez que creamos una instancia a partir de datos CSV. Esto puede ser propenso a errores y consumir mucho tiempo. En el siguiente paso, crearemos una solución más elegante utilizando métodos de clase.

Implementación de Constructores Alternativos con Métodos de Clase

En este paso, aprenderemos cómo implementar un constructor alternativo utilizando un método de clase. Esto nos permitirá crear objetos Stock a partir de datos de filas CSV de una manera más elegante.

¿Qué es un Constructor Alternativo?

En Python, un constructor alternativo es un patrón útil. Por lo general, creamos objetos utilizando el método estándar __init__. Sin embargo, un constructor alternativo nos da una forma adicional de crear objetos. Los métodos de clase son muy adecuados para implementar constructores alternativos porque pueden acceder a la propia clase.

Implementación del Método de Clase from_row()

Agregaremos una variable de clase types y un método de clase from_row() a nuestra clase Stock. Esto simplificará el proceso de creación de instancias de Stock a partir de datos CSV.

Modifiquemos el archivo stock.py agregando el código resaltado:

## stock.py

class Stock:
    types = (str, int, float)  ## Conversiones de tipo a aplicar a los datos CSV

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

    @classmethod
    def from_row(cls, row):
        """
        Crea una instancia de Stock a partir de una fila de datos CSV.

        Argumentos:
            row: Una lista de cadenas [nombre, acciones, precio]

        Devuelve:
            Una nueva instancia de Stock
        """
        values = [func(val) for func, val in zip(cls.types, row)]
        return cls(*values)

## El resto del archivo permanece sin cambios

Ahora, entendamos paso a paso lo que está sucediendo en este código:

  1. Definimos una variable de clase types. Es una tupla que contiene funciones de conversión de tipo (str, int, float). Estas funciones se utilizarán para convertir los datos de la fila CSV a los tipos adecuados.
  2. Agregamos un método de clase from_row(). El decorador @classmethod marca este método como un método de clase.
  3. El primer parámetro de este método es cls, que es una referencia a la propia clase. En métodos normales, usamos self para referirnos a una instancia de la clase, pero aquí usamos cls porque es un método de clase.
  4. La función zip() se utiliza para emparejar cada función de conversión de tipo en types con el valor correspondiente en la lista row.
  5. Usamos una comprensión de lista para aplicar cada función de conversión al valor correspondiente en la lista row. De esta manera, convertimos los datos de cadena de la fila CSV a los tipos adecuados.
  6. Finalmente, creamos una nueva instancia de la clase Stock utilizando los valores convertidos y la devolvemos.

Prueba del Constructor Alternativo

Ahora, crearemos un nuevo archivo llamado test_class_method.py para probar nuestro nuevo método de clase. Esto nos ayudará a verificar que el constructor alternativo funcione como se espera.

## test_class_method.py
from stock import Stock

## Prueba el método de clase from_row()
row = ['AA', '100', '32.20']
s = Stock.from_row(row)

print(f"Stock: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost()}")

## Prueba con una fila diferente
row2 = ['GOOG', '50', '1120.50']
s2 = Stock.from_row(row2)

print(f"\nStock: {s2.name}")
print(f"Shares: {s2.shares}")
print(f"Price: {s2.price}")
print(f"Cost: {s2.cost()}")

Para ver los resultados, ejecuta los siguientes comandos en tu terminal:

cd ~/project
python test_class_method.py

Deberías ver una salida similar a esta:

Stock: AA
Shares: 100
Price: 32.2
Cost: 3220.0

Stock: GOOG
Shares: 50
Price: 1120.5
Cost: 56025.0

Observa que ahora podemos crear instancias de Stock directamente a partir de datos de cadena sin tener que realizar manualmente conversiones de tipo fuera de la clase. Esto hace que nuestro código sea más limpio y asegura que la responsabilidad de la conversión de datos se maneje dentro de la propia clase.

Variables de Clase y Herencia

En este paso, exploraremos cómo las variables de clase interactúan con la herencia y cómo pueden servir como un mecanismo de personalización. En Python, la herencia permite que una subclase herede atributos y métodos de una clase base. Las variables de clase son variables que pertenecen a la propia clase, no a ninguna instancia específica de la clase. Comprender cómo funcionan juntas es crucial para crear código flexible y mantenible.

Variables de Clase en la Herencia

Cuando una subclase hereda de una clase base, automáticamente obtiene acceso a las variables de clase de la clase base. Sin embargo, una subclase tiene la capacidad de anular estas variables de clase. Al hacerlo, la subclase puede cambiar su comportamiento sin afectar a la clase base. Esta es una característica muy poderosa, ya que te permite personalizar el comportamiento de una subclase según tus necesidades específicas.

Creación de una Clase Stock Especializada

Creemos una subclase de la clase Stock. La llamaremos DStock, que significa Decimal Stock (Stock decimal). La principal diferencia entre DStock y la clase Stock regular es que DStock utilizará el tipo Decimal para los valores de precio en lugar de float. En cálculos financieros, la precisión es extremadamente importante, y el tipo Decimal proporciona una aritmética decimal más precisa en comparación con float.

Para crear esta subclase, crearemos un nuevo archivo llamado decimal_stock.py. Aquí está el código que debes poner en este archivo:

## decimal_stock.py
from decimal import Decimal
from stock import Stock

class DStock(Stock):
    """
    Una versión especializada de Stock que utiliza Decimal para los precios
    """
    types = (str, int, Decimal)  ## Anular la variable de clase types

## Prueba la subclase
if __name__ == "__main__":
    ## Crea un DStock a partir de datos de fila
    row = ['AA', '100', '32.20']
    ds = DStock.from_row(row)

    print(f"DStock: {ds.name}")
    print(f"Shares: {ds.shares}")
    print(f"Price: {ds.price} (type: {type(ds.price).__name__})")
    print(f"Cost: {ds.cost()} (type: {type(ds.cost()).__name__})")

    ## Para comparación, crea un Stock regular a partir de los mismos datos
    s = Stock.from_row(row)
    print(f"\nRegular Stock: {s.name}")
    print(f"Shares: {s.shares}")
    print(f"Price: {s.price} (type: {type(s.price).__name__})")
    print(f"Cost: {s.cost()} (type: {type(s.cost()).__name__})")

Después de crear el archivo decimal_stock.py con el código anterior, debes ejecutarlo para ver los resultados. Abre tu terminal y sigue estos pasos:

cd ~/project
python decimal_stock.py

Deberías ver una salida similar a esta:

DStock: AA
Shares: 100
Price: 32.20 (type: Decimal)
Cost: 3220.0 (type: Decimal)

Regular Stock: AA
Shares: 100
Price: 32.2 (type: float)
Cost: 3220.0 (type: float)

Puntos Clave sobre Variables de Clase y Herencia

A partir de este ejemplo, podemos sacar varias conclusiones importantes:

  1. La clase DStock hereda todos los métodos de la clase Stock, como el método cost(), sin tener que redefinirlos. Esta es una de las principales ventajas de la herencia, ya que te ahorra escribir código redundante.
  2. Al simplemente anular la variable de clase types, hemos cambiado cómo se convierten los datos al crear nuevas instancias de DStock. Esto muestra cómo se pueden utilizar las variables de clase para personalizar el comportamiento de una subclase.
  3. La clase base, Stock, permanece sin cambios y sigue funcionando con valores float. Esto significa que los cambios que hicimos en la subclase no afectan a la clase base, lo cual es un buen principio de diseño.
  4. El método de clase from_row() funciona correctamente tanto con las clases Stock como DStock. Esto demuestra el poder de la herencia, ya que el mismo método se puede utilizar con diferentes subclases.

Este ejemplo muestra claramente cómo se pueden utilizar las variables de clase como un mecanismo de configuración. Las subclases pueden anular estas variables para personalizar su comportamiento sin tener que reescribir los métodos.

Discusión sobre el Diseño

Consideremos un enfoque alternativo en el que colocamos las conversiones de tipo en el método __init__:

class Stock:
    def __init__(self, name, shares, price):
        self.name = str(name)
        self.shares = int(shares)
        self.price = float(price)

Con este enfoque, podemos crear un objeto Stock a partir de una fila de datos de esta manera:

row = ['AA', '100', '32.20']
s = Stock(*row)

Aunque este enfoque puede parecer más sencillo a primera vista, tiene varios inconvenientes:

  1. Combina dos preocupaciones diferentes: la inicialización del objeto y la conversión de datos. Esto hace que el código sea más difícil de entender y mantener.
  2. El método __init__ se vuelve menos flexible porque siempre convierte las entradas, incluso si ya están en el tipo correcto.
  3. Restringe cómo las subclases pueden personalizar el proceso de conversión. Las subclases tendrían más dificultades para cambiar la lógica de conversión si está incrustada en el método __init__.
  4. El código se vuelve más frágil. Si alguna de las conversiones falla, el objeto no se puede crear, lo que puede provocar errores en tu programa.

Por otro lado, el enfoque del método de clase separa estas preocupaciones. Esto hace que el código sea más mantenible y flexible, ya que cada parte del código tiene una única responsabilidad.

Creación de un Lector de CSV de Propósito General

En este último paso, crearemos una función de propósito general. Esta función será capaz de leer archivos CSV y crear objetos de cualquier clase que haya implementado el método de clase from_row(). Esto nos muestra el poder de utilizar métodos de clase como una interfaz uniforme. Una interfaz uniforme significa que diferentes clases pueden utilizarse de la misma manera, lo que hace que nuestro código sea más flexible y fácil de gestionar.

Modificación de la Función read_portfolio()

Primero, actualizaremos la función read_portfolio() en el archivo stock.py. Utilizaremos nuestro nuevo método de clase from_row(). Abre el archivo stock.py y cambia la función read_portfolio() de la siguiente manera:

def read_portfolio(filename):
    '''
    Lee un archivo de cartera de acciones en una lista de instancias de Stock
    '''
    import csv
    portfolio = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Omite la cabecera
        for row in rows:
            portfolio.append(Stock.from_row(row))
    return portfolio

Esta nueva versión de la función es más sencilla. Le da la responsabilidad de la conversión de tipos a la clase Stock, donde realmente pertenece. La conversión de tipos significa cambiar los datos de un tipo a otro, como convertir una cadena en un entero. Al hacer esto, hacemos que nuestro código sea más organizado y fácil de entender.

Creación de un Lector de CSV de Propósito General

Ahora, crearemos una función de propósito más general en el archivo reader.py. Esta función puede leer datos CSV y crear instancias de cualquier clase que tenga un método de clase from_row().

Abre el archivo reader.py y agrega la siguiente función:

def read_csv_as_instances(filename, cls):
    '''
    Lee un archivo CSV en una lista de instancias de la clase dada.

    Argumentos:
        filename: Nombre del archivo CSV
        cls: Clase a instanciar (debe tener el método de clase from_row)

    Devuelve:
        Lista de instancias de clase
    '''
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Omite la cabecera
        for row in rows:
            records.append(cls.from_row(row))
    return records

Esta función toma dos entradas: un nombre de archivo y una clase. Luego devuelve una lista de instancias de esa clase, creadas a partir de los datos en el archivo CSV. Esto es muy útil porque podemos usarlo con diferentes clases, siempre que tengan el método from_row().

Prueba del Lector de CSV de Propósito General

Creemos un archivo de prueba para ver cómo funciona nuestro lector de propósito general. Crea un archivo llamado test_csv_reader.py con el siguiente contenido:

## test_csv_reader.py
from reader import read_csv_as_instances
from stock import Stock
from decimal_stock import DStock

## Lee la cartera como instancias de Stock
portfolio = read_csv_as_instances('portfolio.csv', Stock)
print(f"La cartera contiene {len(portfolio)} acciones")
print(f"Primera acción: {portfolio[0].name}, {portfolio[0].shares} acciones a ${portfolio[0].price}")

## Lee la cartera como instancias de DStock (con precios en Decimal)
decimal_portfolio = read_csv_as_instances('portfolio.csv', DStock)
print(f"\nLa cartera decimal contiene {len(decimal_portfolio)} acciones")
print(f"Primera acción: {decimal_portfolio[0].name}, {decimal_portfolio[0].shares} acciones a ${decimal_portfolio[0].price}")

## Define una nueva clase para leer los datos de autobús
class BusRide:
    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

    @classmethod
    def from_row(cls, row):
        return cls(row[0], row[1], row[2], int(row[3]))

## Lee algunos datos de autobús (solo los primeros 5 registros por brevedad)
print("\nLeyendo datos de autobús...")
import csv
with open('ctabus.csv') as f:
    rows = csv.reader(f)
    headers = next(rows)  ## Omite la cabecera
    bus_rides = []
    for i, row in enumerate(rows):
        if i >= 5:  ## Solo lee 5 registros para el ejemplo
            break
        bus_rides.append(BusRide.from_row(row))

## Muestra los datos de autobús
for ride in bus_rides:
    print(f"Ruta: {ride.route}, Fecha: {ride.date}, Tipo: {ride.daytype}, Viajes: {ride.rides}")

Ejecuta este archivo para ver los resultados. Abre tu terminal y utiliza los siguientes comandos:

cd ~/project
python test_csv_reader.py

Deberías ver una salida que muestre los datos de la cartera cargados como instancias de Stock y DStock, y los datos de la ruta de autobús cargados como instancias de BusRide. Esto demuestra que nuestro lector de propósito general funciona con diferentes clases.

Principales Beneficios de Este Enfoque

Este enfoque muestra varios conceptos poderosos:

  1. Separación de preocupaciones: Leer datos está separado de crear objetos. Esto significa que el código para leer el archivo CSV no se mezcla con el código para crear objetos. Hace que el código sea más fácil de entender y mantener.
  2. Polimorfismo: El mismo código puede funcionar con diferentes clases que sigan la misma interfaz. En nuestro caso, siempre que una clase tenga el método from_row(), nuestro lector de propósito general puede utilizarla.
  3. Flexibilidad: Podemos cambiar fácilmente cómo se convierten los datos utilizando diferentes clases. Por ejemplo, podemos usar Stock o DStock para manejar los datos de la cartera de manera diferente.
  4. Extensibilidad: Podemos agregar nuevas clases que funcionen con nuestro lector sin cambiar el código del lector. Esto hace que nuestro código sea más resistente al paso del tiempo.

Este es un patrón común en Python que hace que el código sea más modular, reutilizable y mantenible.

Notas Finales sobre Métodos de Clase

Los métodos de clase se utilizan a menudo como constructores alternativos en Python. Por lo general, puedes distinguirlos porque sus nombres a menudo tienen la palabra "from" en ellos. Por ejemplo:

## Algunos ejemplos de tipos integrados de Python
dict.fromkeys(['a', 'b', 'c'], 0)  ## Crea un diccionario con valores predeterminados
datetime.datetime.fromtimestamp(1627776000)  ## Crea una fecha y hora a partir de una marca de tiempo
int.from_bytes(b'\x00\x01', byteorder='big')  ## Crea un entero a partir de bytes

Siguiendo esta convención, haces que tu código sea más legible y consistente con las bibliotecas integradas de Python. Esto ayuda a otros desarrolladores a entender tu código más fácilmente.

Resumen

En este laboratorio, has aprendido sobre dos características cruciales de Python: las variables de clase y los métodos de clase. Las variables de clase se comparten entre todas las instancias de la clase y se pueden utilizar para la configuración. Los métodos de clase operan sobre la propia clase, marcados con el decorador @classmethod. Los constructores alternativos, un uso común de los métodos de clase, ofrecen diferentes formas de crear objetos. La herencia con variables de clase permite que las subclases personalicen el comportamiento al anularlas, y el uso de métodos de clase puede lograr un diseño de código flexible.

Estos conceptos son poderosos para crear código Python bien organizado y flexible. Al colocar las conversiones de tipo dentro de la clase y proporcionar una interfaz uniforme a través de métodos de clase, puedes escribir utilidades de propósito más general. Para ampliar tus conocimientos, puedes explorar más casos de uso, crear jerarquías de clases y construir complejas tuberías de procesamiento de datos utilizando métodos de clase.